diff --git a/.github/actions/consolidate-comment/action.yml b/.github/actions/consolidate-comment/action.yml deleted file mode 100644 index 7b9b3fe..0000000 --- a/.github/actions/consolidate-comment/action.yml +++ /dev/null @@ -1,507 +0,0 @@ -name: Consolidate Review Comment -description: Consolidates all review results into a single PR comment with dropdowns - -inputs: - review_results: - description: 'JSON string containing all review results' - required: true - github_token: - description: 'GitHub token for API access' - required: true - -outputs: - comment_id: - description: 'ID of the created or updated comment' - value: ${{ steps.comment.outputs.comment_id }} - comment_url: - description: 'URL of the comment' - value: ${{ steps.comment.outputs.comment_url }} - -runs: - using: composite - steps: - - name: Find and update or create comment - id: comment - shell: bash - env: - GITHUB_TOKEN: ${{ inputs.github_token }} - REVIEW_RESULTS: ${{ inputs.review_results }} - run: | - # Constants - COMMENT_MARKER="" - - # Parse review results - echo "$REVIEW_RESULTS" > /tmp/review_results.json - - # Extract individual review data with defaults - REQUIREMENTS=$(echo "$REVIEW_RESULTS" | jq -r '.requirements // {}') - RULES=$(echo "$REVIEW_RESULTS" | jq -r '.rules // {}') - PROJECT_MEMORY=$(echo "$REVIEW_RESULTS" | jq -r '.project_memory // {}') - AGENTS=$(echo "$REVIEW_RESULTS" | jq -r '.agents // {}') - SKILLS=$(echo "$REVIEW_RESULTS" | jq -r '.skills // {}') - UI=$(echo "$REVIEW_RESULTS" | jq -r '.ui // {}') - - # Get commit SHA - COMMIT_SHA="${{ github.sha }}" - SHORT_SHA="${COMMIT_SHA:0:7}" - - # Count passed/total reviews - TOTAL=0 - PASSED=0 - - count_review() { - local review="$1" - local status=$(echo "$review" | jq -r '.status // "skipped"') - if [ "$status" != "skipped" ]; then - TOTAL=$((TOTAL + 1)) - if [ "$status" = "passed" ]; then - PASSED=$((PASSED + 1)) - fi - fi - } - - count_review "$REQUIREMENTS" - count_review "$RULES" - count_review "$PROJECT_MEMORY" - count_review "$AGENTS" - count_review "$SKILLS" - count_review "$UI" - - # Helper function to generate status indicator - get_status_indicator() { - local status="$1" - local confidence="$2" - - case "$status" in - "passed") - echo "PASSED | Confidence: $confidence" - ;; - "failed") - echo "FAILED | Confidence: $confidence" - ;; - "skipped") - echo "Skipped" - ;; - *) - echo "Unknown" - ;; - esac - } - - # Helper function to generate dropdown section - generate_dropdown() { - local title="$1" - local review_json="$2" - local skip_reason="$3" - - local status=$(echo "$review_json" | jq -r '.status // "skipped"') - local confidence=$(echo "$review_json" | jq -r '.confidence // 0') - local summary=$(echo "$review_json" | jq -r '.summary // ""') - local content=$(echo "$review_json" | jq -r '.content // ""') - - if [ "$status" = "skipped" ]; then - cat < - $title -- Skipped - - ${skip_reason:-No relevant files were modified.} - - - DROPDOWN - else - local status_icon - if [ "$status" = "passed" ]; then - status_icon="PASSED" - else - status_icon="FAILED" - fi - - cat < - $title $status_icon | Confidence: $confidence - - ### Summary - $summary - - $content - - - DROPDOWN - fi - } - - # Generate requirements section content - generate_requirements_content() { - local review_json="$1" - local status=$(echo "$review_json" | jq -r '.status // "skipped"') - - if [ "$status" = "skipped" ]; then - return - fi - - local met=$(echo "$review_json" | jq -r '.requirements_met // []') - local missing=$(echo "$review_json" | jq -r '.requirements_missing // []') - - local content="" - - if [ "$met" != "[]" ] && [ "$met" != "null" ]; then - content="${content} - ### Requirements Met" - while IFS= read -r req; do - if [ -n "$req" ] && [ "$req" != "null" ]; then - content="${content} - - $req" - fi - done < <(echo "$met" | jq -r '.[]') - fi - - if [ "$missing" != "[]" ] && [ "$missing" != "null" ]; then - content="${content} - - ### Requirements Missing" - while IFS= read -r req; do - if [ -n "$req" ] && [ "$req" != "null" ]; then - content="${content} - - $req" - fi - done < <(echo "$missing" | jq -r '.[]') - fi - - echo "$content" - } - - # Generate UI section content with scores table - generate_ui_content() { - local review_json="$1" - local status=$(echo "$review_json" | jq -r '.status // "skipped"') - - if [ "$status" = "skipped" ]; then - return - fi - - local bp=$(echo "$review_json" | jq -r '.best_practices_evaluation // {}') - local critical=$(echo "$review_json" | jq -r '.total_critical // 0') - local major=$(echo "$review_json" | jq -r '.total_major // 0') - local minor=$(echo "$review_json" | jq -r '.total_minor // 0') - local recs=$(echo "$review_json" | jq -r '.top_recommendations // []') - - local content=" - ### Issue Summary - | Level | Count | - |-------|-------| - | Critical | $critical | - | Major | $major | - | Minor | $minor |" - - if [ "$bp" != "{}" ] && [ "$bp" != "null" ]; then - local visual=$(echo "$bp" | jq -r '.visual_design // "N/A"') - local layout=$(echo "$bp" | jq -r '.layout_responsiveness // "N/A"') - local a11y=$(echo "$bp" | jq -r '.accessibility // "N/A"') - local ux=$(echo "$bp" | jq -r '.user_experience // "N/A"') - local elegance=$(echo "$bp" | jq -r '.modern_elegance // "N/A"') - - content="${content} - - ### Best Practices Scores - | Category | Score | - |----------|-------| - | Visual Design | ${visual}/10 | - | Layout & Responsiveness | ${layout}/10 | - | Accessibility | ${a11y}/10 | - | User Experience | ${ux}/10 | - | Modern Elegance | ${elegance}/10 |" - fi - - if [ "$recs" != "[]" ] && [ "$recs" != "null" ]; then - content="${content} - - ### Top Recommendations" - local i=1 - while IFS= read -r rec; do - if [ -n "$rec" ] && [ "$rec" != "null" ]; then - content="${content} - $i. $rec" - i=$((i + 1)) - fi - done < <(echo "$recs" | jq -r '.[]') - fi - - echo "$content" - } - - # Build the complete comment body - build_comment_body() { - # Requirements section - local req_status=$(echo "$REQUIREMENTS" | jq -r '.status // "skipped"') - local req_confidence=$(echo "$REQUIREMENTS" | jq -r '.confidence // 0') - local req_summary=$(echo "$REQUIREMENTS" | jq -r '.summary // ""') - local req_content=$(generate_requirements_content "$REQUIREMENTS") - - local requirements_section - if [ "$req_status" = "skipped" ]; then - requirements_section="
- Requirements Review -- Skipped - - No linked issue found or no requirements to verify. - -
" - else - local req_icon - if [ "$req_status" = "passed" ]; then - req_icon="PASSED" - else - req_icon="FAILED" - fi - requirements_section="
- Requirements Review $req_icon | Confidence: $req_confidence - - ### Summary - $req_summary - $req_content - -
" - fi - - # Rules section - local rules_status=$(echo "$RULES" | jq -r '.status // "skipped"') - local rules_confidence=$(echo "$RULES" | jq -r '.confidence // 0') - local rules_summary=$(echo "$RULES" | jq -r '.summary // ""') - local rules_content=$(echo "$RULES" | jq -r '.content // ""') - - local rules_section - if [ "$rules_status" = "skipped" ]; then - rules_section="
- Rules Review -- Skipped - - No rules files found or no applicable rules. - -
" - else - local rules_icon - if [ "$rules_status" = "passed" ]; then - rules_icon="PASSED" - else - rules_icon="FAILED" - fi - rules_section="
- Rules Review $rules_icon | Confidence: $rules_confidence - - ### Summary - $rules_summary - - $rules_content - -
" - fi - - # Project Memory section - local pm_status=$(echo "$PROJECT_MEMORY" | jq -r '.status // "skipped"') - local pm_confidence=$(echo "$PROJECT_MEMORY" | jq -r '.confidence // 0') - local pm_summary=$(echo "$PROJECT_MEMORY" | jq -r '.summary // ""') - local pm_content=$(echo "$PROJECT_MEMORY" | jq -r '.content // ""') - - local pm_section - if [ "$pm_status" = "skipped" ]; then - pm_section="
- Project Memory Review -- Skipped - - No CLAUDE.md files were modified. - -
" - else - local pm_icon - if [ "$pm_status" = "passed" ]; then - pm_icon="PASSED" - else - pm_icon="FAILED" - fi - pm_section="
- Project Memory Review $pm_icon | Confidence: $pm_confidence - - ### Summary - $pm_summary - - $pm_content - -
" - fi - - # Agents section - local agents_status=$(echo "$AGENTS" | jq -r '.status // "skipped"') - local agents_confidence=$(echo "$AGENTS" | jq -r '.confidence // 0') - local agents_summary=$(echo "$AGENTS" | jq -r '.summary // ""') - local agents_content=$(echo "$AGENTS" | jq -r '.content // ""') - - local agents_section - if [ "$agents_status" = "skipped" ]; then - agents_section="
- Agents Review -- Skipped - - No agent files were modified. - -
" - else - local agents_icon - if [ "$agents_status" = "passed" ]; then - agents_icon="PASSED" - else - agents_icon="FAILED" - fi - agents_section="
- Agents Review $agents_icon | Confidence: $agents_confidence - - ### Summary - $agents_summary - - $agents_content - -
" - fi - - # Skills section - local skills_status=$(echo "$SKILLS" | jq -r '.status // "skipped"') - local skills_confidence=$(echo "$SKILLS" | jq -r '.confidence // 0') - local skills_summary=$(echo "$SKILLS" | jq -r '.summary // ""') - local skills_content=$(echo "$SKILLS" | jq -r '.content // ""') - - local skills_section - if [ "$skills_status" = "skipped" ]; then - skills_section="
- Skills Review -- Skipped - - No skill files were modified. - -
" - else - local skills_icon - if [ "$skills_status" = "passed" ]; then - skills_icon="PASSED" - else - skills_icon="FAILED" - fi - skills_section="
- Skills Review $skills_icon | Confidence: $skills_confidence - - ### Summary - $skills_summary - - $skills_content - -
" - fi - - # UI section - local ui_status=$(echo "$UI" | jq -r '.status // "skipped"') - local ui_confidence=$(echo "$UI" | jq -r '.confidence // 0') - local ui_summary=$(echo "$UI" | jq -r '.summary // ""') - local ui_content=$(generate_ui_content "$UI") - - local ui_section - if [ "$ui_status" = "skipped" ]; then - ui_section="
- UI Review -- Skipped - - No UI files were modified. - -
" - else - local ui_icon - if [ "$ui_status" = "passed" ]; then - ui_icon="PASSED" - else - ui_icon="FAILED" - fi - ui_section="
- UI Review $ui_icon | Confidence: $ui_confidence - - ### Summary - $ui_summary - $ui_content - -
" - fi - - # Combine all sections - cat < /tmp/comment_body.md - - # Get PR number - if [ "${{ github.event_name }}" = "pull_request" ]; then - PR_NUMBER="${{ github.event.pull_request.number }}" - else - # Try to find PR for this commit - PR_NUMBER=$(gh pr list --state open --json number,headRefName --jq ".[] | select(.headRefName == \"${{ github.ref_name }}\") | .number" 2>/dev/null || echo "") - fi - - if [ -z "$PR_NUMBER" ]; then - echo "No PR found, skipping comment" - echo "comment_id=" >> $GITHUB_OUTPUT - echo "comment_url=" >> $GITHUB_OUTPUT - exit 0 - fi - - # Find existing comment with marker - EXISTING_COMMENT_ID=$(gh api \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \ - --jq ".[] | select(.body | contains(\"$COMMENT_MARKER\")) | .id" \ - 2>/dev/null | head -1) - - if [ -n "$EXISTING_COMMENT_ID" ]; then - # Update existing comment - RESPONSE=$(gh api \ - --method PATCH \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "/repos/${{ github.repository }}/issues/comments/${EXISTING_COMMENT_ID}" \ - -f body="$(cat /tmp/comment_body.md)") - - COMMENT_ID="$EXISTING_COMMENT_ID" - COMMENT_URL=$(echo "$RESPONSE" | jq -r '.html_url') - echo "Updated existing comment: $COMMENT_ID" - else - # Create new comment - RESPONSE=$(gh api \ - --method POST \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \ - -f body="$(cat /tmp/comment_body.md)") - - COMMENT_ID=$(echo "$RESPONSE" | jq -r '.id') - COMMENT_URL=$(echo "$RESPONSE" | jq -r '.html_url') - echo "Created new comment: $COMMENT_ID" - fi - - echo "comment_id=$COMMENT_ID" >> $GITHUB_OUTPUT - echo "comment_url=$COMMENT_URL" >> $GITHUB_OUTPUT diff --git a/.github/ci-config.yml b/.github/ci-config.yml index 3e98835..2f9d366 100644 --- a/.github/ci-config.yml +++ b/.github/ci-config.yml @@ -1,38 +1,26 @@ # CI Configuration -# Controls which CI workflows run in this repository +# Template for Claude Code repos ci: - # Basic CI (always enabled) - basic: - enabled: true + # Set to true if this repo has an app (enables E2E + UI review) + has_app: false + + # Static Analysis: Lint (ESLint) + Types (TypeScript) + static_analysis: lint: true typecheck: true - vitest: true - # E2E Testing - e2e: - enabled: true - framework: playwright + # Tests: Unit (Vitest) + E2E (Playwright) + # E2E only runs if has_app: true + tests: + unit: true + e2e: true - # Code Reviews (runs on every commit) + # Reviews: Requirements, Code Quality, Context, UI + # Requirements must pass before others run + # UI only runs if has_app: true and screenshots exist reviews: - enabled: true requirements: true - rules: true - project_memory: true - agents: true - skills: true - playwright_ui: true - - # Deployment CI (PR only) - deployment: - enabled: false - - vercel: - enabled: false - - supabase: - enabled: false - - cloudflare: - enabled: false + code_quality: true + context: true + ui: true diff --git a/.github/workflows/basic-ci.yml b/.github/workflows/basic-ci.yml deleted file mode 100644 index 179665e..0000000 --- a/.github/workflows/basic-ci.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Basic CI - -on: - push: - branches: ['**'] - pull_request: - branches: ['**'] - merge_group: - -concurrency: - group: basic-ci-${{ github.ref }} - cancel-in-progress: true - -jobs: - changed-files: - name: Detect Changed Files - runs-on: ubuntu-latest - outputs: - has_ts_files: ${{ steps.filter.outputs.has_ts_files }} - has_test_files: ${{ steps.filter.outputs.has_test_files }} - ts_files: ${{ steps.filter.outputs.ts_files }} - test_files: ${{ steps.filter.outputs.test_files }} - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Get changed files - id: filter - uses: ./.github/actions/changed-files - with: - pattern: '\.(ts|tsx|js|jsx|test\.ts|test\.tsx)$' - - lint: - needs: changed-files - if: needs.changed-files.outputs.has_ts_files == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: constellos/.github/actions/lint@main - with: - files: ${{ needs.changed-files.outputs.ts_files }} - - typecheck: - needs: changed-files - if: needs.changed-files.outputs.has_ts_files == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: constellos/.github/actions/typecheck@main - with: - files: ${{ needs.changed-files.outputs.ts_files }} - - vitest: - needs: changed-files - if: needs.changed-files.outputs.has_test_files == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: constellos/.github/actions/unit-tests@main - with: - files: ${{ needs.changed-files.outputs.test_files }} diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 3fb6b72..cb5ec5b 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -1,892 +1,620 @@ -name: CI Pipeline - -# Unified CI pipeline that orchestrates the entire CI flow -# Replaces fragmented workflows with a single dependency chain +name: CI on: - push: - branches: ['**'] pull_request: branches: ['**'] permissions: contents: read pull-requests: write - issues: write - statuses: write + issues: read concurrency: - # Use head_ref for PRs, ref_name for pushes - ensures same branch cancels previous runs - group: ci-pipeline-${{ github.head_ref || github.ref_name }} + group: ci-${{ github.head_ref || github.ref_name }} cancel-in-progress: true +env: + NODE_VERSION: '22' + jobs: - # ============================================================================= - # STAGE 1: Configuration - # ============================================================================= - config: - name: Read CI Config + # ============================================ + # STATIC ANALYSIS + # ============================================ + lint: + name: "Static Analysis / Lint (ESLint)" runs-on: ubuntu-latest outputs: - basic_enabled: ${{ steps.config.outputs.basic_enabled }} - lint_enabled: ${{ steps.config.outputs.lint_enabled }} - typecheck_enabled: ${{ steps.config.outputs.typecheck_enabled }} - vitest_enabled: ${{ steps.config.outputs.vitest_enabled }} - e2e_enabled: ${{ steps.config.outputs.e2e_enabled }} - reviews_enabled: ${{ steps.config.outputs.reviews_enabled }} - requirements_review: ${{ steps.config.outputs.requirements_review }} - rules_review: ${{ steps.config.outputs.rules_review }} - project_memory_review: ${{ steps.config.outputs.project_memory_review }} - agents_review: ${{ steps.config.outputs.agents_review }} - skills_review: ${{ steps.config.outputs.skills_review }} - playwright_ui_review: ${{ steps.config.outputs.playwright_ui_review }} - deployment_enabled: ${{ steps.config.outputs.deployment_enabled }} - + has_app: ${{ steps.detect.outputs.has_app }} steps: - uses: actions/checkout@v4 - - name: Install yq + - name: Detect environment + id: detect run: | - sudo wget -q https://github.com/mikefarah/yq/releases/download/v4.35.1/yq_linux_amd64 -O /usr/local/bin/yq - sudo chmod +x /usr/local/bin/yq + # Detect package manager + PM="npm" + [ -f "bun.lockb" ] || [ -f "bun.lock" ] && PM="bun" + [ -f "pnpm-lock.yaml" ] && PM="pnpm" + [ -f "yarn.lock" ] && PM="yarn" + echo "pm=$PM" >> $GITHUB_OUTPUT + + # Detect app + HAS_APP=false + [ -f "next.config.js" ] && HAS_APP=true + [ -f "next.config.mjs" ] && HAS_APP=true + [ -f "next.config.ts" ] && HAS_APP=true + [ -f "vite.config.ts" ] && HAS_APP=true + [ -f "vite.config.js" ] && HAS_APP=true + [ -f "playwright.config.ts" ] && HAS_APP=true + ls apps/*/package.json >/dev/null 2>&1 && HAS_APP=true + [ -d "src/app" ] && HAS_APP=true + echo "has_app=$HAS_APP" >> $GITHUB_OUTPUT + + - uses: actions/setup-node@v4 + if: steps.detect.outputs.pm != 'bun' + with: + node-version: ${{ env.NODE_VERSION }} + + - uses: oven-sh/setup-bun@v2 + if: steps.detect.outputs.pm == 'bun' - - name: Read CI config - id: config + - name: Install dependencies run: | - CONFIG_FILE=".github/ci-config.yml" - - if [ -f "$CONFIG_FILE" ]; then - # Basic CI - echo "basic_enabled=$(yq '.ci.basic.enabled // true' $CONFIG_FILE)" >> $GITHUB_OUTPUT - echo "lint_enabled=$(yq '.ci.basic.lint // true' $CONFIG_FILE)" >> $GITHUB_OUTPUT - echo "typecheck_enabled=$(yq '.ci.basic.typecheck // true' $CONFIG_FILE)" >> $GITHUB_OUTPUT - echo "vitest_enabled=$(yq '.ci.basic.vitest // true' $CONFIG_FILE)" >> $GITHUB_OUTPUT - - # E2E - echo "e2e_enabled=$(yq '.ci.e2e.enabled // false' $CONFIG_FILE)" >> $GITHUB_OUTPUT - - # Reviews - echo "reviews_enabled=$(yq '.ci.reviews.enabled // false' $CONFIG_FILE)" >> $GITHUB_OUTPUT - echo "requirements_review=$(yq '.ci.reviews.requirements // false' $CONFIG_FILE)" >> $GITHUB_OUTPUT - echo "rules_review=$(yq '.ci.reviews.rules // false' $CONFIG_FILE)" >> $GITHUB_OUTPUT - echo "project_memory_review=$(yq '.ci.reviews.project_memory // false' $CONFIG_FILE)" >> $GITHUB_OUTPUT - echo "agents_review=$(yq '.ci.reviews.agents // false' $CONFIG_FILE)" >> $GITHUB_OUTPUT - echo "skills_review=$(yq '.ci.reviews.skills // false' $CONFIG_FILE)" >> $GITHUB_OUTPUT - echo "playwright_ui_review=$(yq '.ci.reviews.playwright_ui // false' $CONFIG_FILE)" >> $GITHUB_OUTPUT - - # Deployment - echo "deployment_enabled=$(yq '.ci.deployment.enabled // false' $CONFIG_FILE)" >> $GITHUB_OUTPUT - else - # Defaults when no config file - echo "basic_enabled=true" >> $GITHUB_OUTPUT - echo "lint_enabled=true" >> $GITHUB_OUTPUT - echo "typecheck_enabled=true" >> $GITHUB_OUTPUT - echo "vitest_enabled=true" >> $GITHUB_OUTPUT - echo "e2e_enabled=false" >> $GITHUB_OUTPUT - echo "reviews_enabled=false" >> $GITHUB_OUTPUT - echo "requirements_review=false" >> $GITHUB_OUTPUT - echo "rules_review=false" >> $GITHUB_OUTPUT - echo "project_memory_review=false" >> $GITHUB_OUTPUT - echo "agents_review=false" >> $GITHUB_OUTPUT - echo "skills_review=false" >> $GITHUB_OUTPUT - echo "playwright_ui_review=false" >> $GITHUB_OUTPUT - echo "deployment_enabled=false" >> $GITHUB_OUTPUT - fi + case "${{ steps.detect.outputs.pm }}" in + bun) bun install --frozen-lockfile ;; + pnpm) corepack enable && pnpm install --frozen-lockfile ;; + yarn) yarn --frozen-lockfile ;; + *) npm ci ;; + esac + + - name: Lint + run: | + PM="${{ steps.detect.outputs.pm }}" + $PM run lint - # ============================================================================= - # STAGE 2: Basic CI (Lint, Typecheck, Unit Tests) - # ============================================================================= - changed-files: - name: Detect Changed Files - needs: config - if: needs.config.outputs.basic_enabled == 'true' + types: + name: "Static Analysis / Types (TypeScript)" runs-on: ubuntu-latest - outputs: - has_ts_files: ${{ steps.filter.outputs.has_ts_files }} - has_test_files: ${{ steps.filter.outputs.has_test_files }} - ts_files: ${{ steps.filter.outputs.ts_files }} - test_files: ${{ steps.filter.outputs.test_files }} - steps: - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Get changed files - id: filter - uses: ./.github/actions/changed-files + - name: Detect package manager + id: detect + run: | + PM="npm" + [ -f "bun.lockb" ] || [ -f "bun.lock" ] && PM="bun" + [ -f "pnpm-lock.yaml" ] && PM="pnpm" + [ -f "yarn.lock" ] && PM="yarn" + echo "pm=$PM" >> $GITHUB_OUTPUT + + - uses: actions/setup-node@v4 + if: steps.detect.outputs.pm != 'bun' with: - pattern: '\.(ts|tsx|js|jsx|test\.ts|test\.tsx)$' + node-version: ${{ env.NODE_VERSION }} - lint: - name: CI / Lint - needs: [config, changed-files] - if: | - always() && - needs.config.result == 'success' && - needs.changed-files.result == 'success' && - needs.config.outputs.lint_enabled == 'true' && - needs.changed-files.outputs.has_ts_files == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: constellos/.github/actions/lint@main - with: - files: ${{ needs.changed-files.outputs.ts_files }} - - typecheck: - name: CI / Typecheck - needs: [config, changed-files] - if: | - always() && - needs.config.result == 'success' && - needs.changed-files.result == 'success' && - needs.config.outputs.typecheck_enabled == 'true' && - needs.changed-files.outputs.has_ts_files == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: constellos/.github/actions/typecheck@main - with: - files: ${{ needs.changed-files.outputs.ts_files }} - - unit-tests: - name: CI / Unit Tests - needs: [config, changed-files] - if: | - always() && - needs.config.result == 'success' && - needs.changed-files.result == 'success' && - needs.config.outputs.vitest_enabled == 'true' && - needs.changed-files.outputs.has_test_files == 'true' + - uses: oven-sh/setup-bun@v2 + if: steps.detect.outputs.pm == 'bun' + + - name: Install dependencies + run: | + case "${{ steps.detect.outputs.pm }}" in + bun) bun install --frozen-lockfile ;; + pnpm) corepack enable && pnpm install --frozen-lockfile ;; + yarn) yarn --frozen-lockfile ;; + *) npm ci ;; + esac + + - name: Typecheck + run: | + PM="${{ steps.detect.outputs.pm }}" + $PM run typecheck || npx tsc --noEmit + + # ============================================ + # TESTS (requires Static Analysis) + # ============================================ + unit: + name: "Tests / Unit (Vitest)" + needs: [lint, types] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: constellos/.github/actions/unit-tests@main + + - name: Detect package manager + id: detect + run: | + PM="npm" + [ -f "bun.lockb" ] || [ -f "bun.lock" ] && PM="bun" + [ -f "pnpm-lock.yaml" ] && PM="pnpm" + [ -f "yarn.lock" ] && PM="yarn" + echo "pm=$PM" >> $GITHUB_OUTPUT + + - uses: actions/setup-node@v4 + if: steps.detect.outputs.pm != 'bun' with: - files: ${{ needs.changed-files.outputs.test_files }} + node-version: ${{ env.NODE_VERSION }} - basic-ci-complete: - name: Basic CI Complete - needs: [config, changed-files, lint, typecheck, unit-tests] - if: always() - runs-on: ubuntu-latest - outputs: - success: ${{ steps.check.outputs.success }} - steps: - - name: Check basic CI results - id: check + - uses: oven-sh/setup-bun@v2 + if: steps.detect.outputs.pm == 'bun' + + - name: Install dependencies + run: | + case "${{ steps.detect.outputs.pm }}" in + bun) bun install --frozen-lockfile ;; + pnpm) corepack enable && pnpm install --frozen-lockfile ;; + yarn) yarn --frozen-lockfile ;; + *) npm ci ;; + esac + + - name: Run unit tests run: | - # Check if all enabled checks passed or were skipped - LINT_RESULT="${{ needs.lint.result }}" - TYPECHECK_RESULT="${{ needs.typecheck.result }}" - UNIT_TESTS_RESULT="${{ needs.unit-tests.result }}" - - echo "Lint: $LINT_RESULT" - echo "Typecheck: $TYPECHECK_RESULT" - echo "Unit Tests: $UNIT_TESTS_RESULT" - - # Success if all are success or skipped - if [[ "$LINT_RESULT" == "failure" ]] || \ - [[ "$TYPECHECK_RESULT" == "failure" ]] || \ - [[ "$UNIT_TESTS_RESULT" == "failure" ]]; then - echo "success=false" >> $GITHUB_OUTPUT - echo "::error::Basic CI failed" - exit 1 + PM="${{ steps.detect.outputs.pm }}" + if [ -f "vitest.config.ts" ] || [ -f "vitest.config.js" ]; then + $PM run test -- --passWithNoTests || npx vitest run --passWithNoTests + else + echo "No vitest config found, skipping" fi - echo "success=true" >> $GITHUB_OUTPUT - - # ============================================================================= - # STAGE 3: E2E Tests (conditional on e2e.enabled and basic-ci passing) - # ============================================================================= - e2e-tests: - name: E2E Tests - needs: [config, basic-ci-complete] - if: | - always() && - needs.config.outputs.e2e_enabled == 'true' && - needs.basic-ci-complete.result == 'success' && - needs.basic-ci-complete.outputs.success == 'true' + e2e: + name: "Tests / E2E (Playwright)" + needs: [lint, unit] + if: needs.lint.outputs.has_app == 'true' runs-on: ubuntu-latest - outputs: - tests_passed: ${{ steps.result.outputs.passed }} - screenshot_count: ${{ steps.screenshots.outputs.count }} - steps: - - name: Checkout code - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 + - name: Detect package manager + id: detect + run: | + PM="npm" + [ -f "bun.lockb" ] || [ -f "bun.lock" ] && PM="bun" + [ -f "pnpm-lock.yaml" ] && PM="pnpm" + [ -f "yarn.lock" ] && PM="yarn" + echo "pm=$PM" >> $GITHUB_OUTPUT + + - uses: actions/setup-node@v4 + if: steps.detect.outputs.pm != 'bun' with: - node-version: '22' - - - name: Install dependencies - run: npm ci + node-version: ${{ env.NODE_VERSION }} - - name: Install Playwright browsers - run: npx playwright install chromium --with-deps + - uses: oven-sh/setup-bun@v2 + if: steps.detect.outputs.pm == 'bun' - - name: Wait for deployment - if: needs.config.outputs.deployment_enabled == 'true' + - name: Install dependencies run: | - echo "Waiting for deployment to be ready..." - # Deployment monitoring would be integrated here - # For now, we proceed without waiting - sleep 5 + case "${{ steps.detect.outputs.pm }}" in + bun) bun install --frozen-lockfile ;; + pnpm) corepack enable && pnpm install --frozen-lockfile ;; + yarn) yarn --frozen-lockfile ;; + *) npm ci ;; + esac - - name: Run Playwright tests - id: playwright - continue-on-error: true - run: | - mkdir -p .playwright-screenshots - npx playwright test \ - --project=chromium \ - --reporter=list,html \ - --screenshot=on \ - --output=.playwright-screenshots \ - 2>&1 | tee playwright-output.txt - - - name: Collect screenshots - id: screenshots + - run: npx playwright install chromium --with-deps + + - name: Run E2E tests run: | mkdir -p .claude/screenshots + npx playwright test --project=chromium --screenshot=on - find .playwright-screenshots -name "*.png" -type f 2>/dev/null | while read -r file; do - UNIQUE_NAME=$(echo "$file" | sed 's/[\/\.]/_/g' | sed 's/^_//') - cp "$file" ".claude/screenshots/${UNIQUE_NAME}.png" - done - - if [ -d "test-results" ]; then - find test-results -name "*.png" -type f 2>/dev/null | while read -r file; do - UNIQUE_NAME=$(echo "$file" | sed 's/[\/\.]/_/g' | sed 's/^_//') - cp "$file" ".claude/screenshots/${UNIQUE_NAME}.png" 2>/dev/null || true - done - fi - - COUNT=$(ls -1 .claude/screenshots/*.png 2>/dev/null | wc -l || echo "0") - echo "count=$COUNT" >> $GITHUB_OUTPUT - - ls -la .claude/screenshots/ > .claude/screenshot-manifest.txt 2>/dev/null || echo "No screenshots" > .claude/screenshot-manifest.txt - - - name: Upload screenshots artifact - if: steps.screenshots.outputs.count > 0 - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v4 + if: always() with: name: playwright-screenshots-${{ github.sha }} path: .claude/screenshots/ retention-days: 7 - - name: Upload test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: playwright-report-${{ github.sha }} - path: playwright-report/ - retention-days: 7 - - - name: Set result - id: result - run: | - if [ "${{ steps.playwright.outcome }}" = "success" ]; then - echo "passed=true" >> $GITHUB_OUTPUT - else - echo "passed=false" >> $GITHUB_OUTPUT - echo "::error::E2E tests failed" - exit 1 - fi - - # ============================================================================= - # STAGE 4: Code Reviews (runs after prerequisites pass) - # Only runs on pull_request events to avoid duplicates - # ============================================================================= - reviews: - name: Code Reviews - needs: [config, basic-ci-complete, e2e-tests] - if: | - always() && - github.event_name == 'pull_request' && - needs.config.outputs.reviews_enabled == 'true' && - needs.basic-ci-complete.result == 'success' && - needs.basic-ci-complete.outputs.success == 'true' && - (needs.e2e-tests.result == 'success' || needs.e2e-tests.result == 'skipped') + # ============================================ + # REVIEWS (requires Tests, PR only) + # ============================================ + requirements: + name: "Reviews / Requirements" + needs: [unit, e2e] + if: github.event_name == 'pull_request' && !failure() && !cancelled() runs-on: ubuntu-latest outputs: - requirements_passed: ${{ steps.requirements.outputs.passed }} - requirements_summary: ${{ steps.requirements.outputs.summary }} - rules_passed: ${{ steps.rules.outputs.passed }} - rules_summary: ${{ steps.rules.outputs.summary }} - project_memory_passed: ${{ steps.project-memory.outputs.passed }} - project_memory_summary: ${{ steps.project-memory.outputs.summary }} - agents_passed: ${{ steps.agents.outputs.passed }} - agents_summary: ${{ steps.agents.outputs.summary }} - skills_passed: ${{ steps.skills.outputs.passed }} - skills_summary: ${{ steps.skills.outputs.summary }} - ui_passed: ${{ steps.ui.outputs.passed }} - ui_summary: ${{ steps.ui.outputs.summary }} - + passed: ${{ steps.extract.outputs.passed }} + result: ${{ steps.extract.outputs.result }} steps: - - name: Checkout code - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Extract context + - name: Get context id: context env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - BRANCH_NAME="${{ github.head_ref || github.ref_name }}" - echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT - - # Extract issue number from branch - ISSUE_NUMBER="" - if [[ $BRANCH_NAME =~ ^(feature|fix|bugfix|hotfix)/([0-9]+) ]]; then - ISSUE_NUMBER="${BASH_REMATCH[2]}" - elif [[ $BRANCH_NAME =~ ^([0-9]+)- ]]; then - ISSUE_NUMBER="${BASH_REMATCH[1]}" - elif [[ $BRANCH_NAME =~ issue-([0-9]+) ]]; then - ISSUE_NUMBER="${BASH_REMATCH[1]}" - fi - echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT - - # Get changed files - if [ "${{ github.event_name }}" = "pull_request" ]; then - CHANGED_FILES=$(gh pr view ${{ github.event.pull_request.number }} --json files --jq '.files[].path' | tr '\n' ' ') - else - CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD 2>/dev/null | tr '\n' ' ' || echo "") - fi - echo "changed_files=$CHANGED_FILES" >> $GITHUB_OUTPUT - echo "$CHANGED_FILES" | tr ' ' '\n' > /tmp/changed_files.txt - - - name: Get issue context - if: steps.context.outputs.issue_number != '' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - ISSUE_NUM="${{ steps.context.outputs.issue_number }}" - gh issue view $ISSUE_NUM --json title,body,comments > /tmp/issue.json 2>/dev/null || echo "{}" > /tmp/issue.json + BRANCH="${{ github.head_ref }}" + echo "branch=$BRANCH" >> $GITHUB_OUTPUT - ISSUE_TITLE=$(jq -r '.title // ""' /tmp/issue.json) - ISSUE_BODY=$(jq -r '.body // ""' /tmp/issue.json) - echo "$ISSUE_TITLE" > /tmp/issue_title.txt - echo "$ISSUE_BODY" > /tmp/issue_body.txt - jq -r '.comments[]?.body // ""' /tmp/issue.json > /tmp/issue_comments.txt 2>/dev/null || echo "" > /tmp/issue_comments.txt + # 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 - - name: Get PR context - if: github.event_name == 'pull_request' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PR_NUM="${{ github.event.pull_request.number }}" - gh pr view $PR_NUM --json comments,reviews > /tmp/pr_context.json 2>/dev/null || echo "{}" > /tmp/pr_context.json + # Get changed files + gh pr view ${{ github.event.pull_request.number }} --json files --jq '.files[].path' > /tmp/changed.txt - jq -r '.comments[]?.body // ""' /tmp/pr_context.json > /tmp/pr_comments.txt 2>/dev/null || echo "" > /tmp/pr_comments.txt - jq -r '.reviews[]?.body // ""' /tmp/pr_context.json > /tmp/review_comments.txt 2>/dev/null || echo "" > /tmp/review_comments.txt + # Get issue context + if [ -n "$ISSUE" ]; then + gh issue view $ISSUE --json title,body > /tmp/issue.json 2>/dev/null || echo "{}" > /tmp/issue.json + fi - # Requirements Review - - name: Requirements Review - id: requirements - if: needs.config.outputs.requirements_review == 'true' + - name: Run review + id: review uses: anthropics/claude-code-base-action@beta with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - allowed_tools: "View,GlobTool,Grep,Bash(git diff:*),Bash(git log:*),Bash(cat:*)" + allowed_tools: "View,GlobTool,Grep,Bash(git diff:*),Bash(cat:*)" max_turns: 15 prompt: | - # Requirements Review Agent - - Review code changes against linked issue requirements. - - ## Context - - Branch: ${{ steps.context.outputs.branch_name }} - - Issue: ${{ steps.context.outputs.issue_number }} - - Changed Files: ${{ steps.context.outputs.changed_files }} - - ## Task - 1. Read issue context from /tmp/issue_title.txt, /tmp/issue_body.txt, /tmp/issue_comments.txt - 2. Read PR context from /tmp/pr_comments.txt, /tmp/review_comments.txt - 3. Review git diff for changed files - 4. Compare requirements vs implementation - 5. Output JSON with: passed, confidence, summary, requirements_met, requirements_missing - - Output MUST end with ```json block containing {passed, confidence, summary}. - - - name: Extract requirements result - id: requirements-extract - if: needs.config.outputs.requirements_review == 'true' + # Requirements Review + Check if changes align with issue requirements. + - Changed files: /tmp/changed.txt + - Issue context: /tmp/issue.json + Output ```json {"passed": bool, "summary": "..."} ``` + + - name: Extract result + id: extract run: | - EXEC_FILE="${{ steps.requirements.outputs.execution_file }}" - if [ -f "$EXEC_FILE" ]; then - LAST_TEXT=$(jq -r '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text] | last // ""' "$EXEC_FILE" 2>/dev/null) - OUTPUT=$(echo "$LAST_TEXT" | sed -n '/```json/,/```/{/```json/d;/```/d;p;}' | tr -d '\r') - if echo "$OUTPUT" | jq . >/dev/null 2>&1 && [ -n "$OUTPUT" ]; then - echo "$OUTPUT" > /tmp/requirements_output.json - else - echo '{"passed":true,"confidence":0.8,"summary":"Review completed"}' > /tmp/requirements_output.json + 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 - else - echo '{"passed":true,"confidence":0.8,"summary":"Review completed"}' > /tmp/requirements_output.json fi + echo "passed=true" >> $GITHUB_OUTPUT + echo 'result={"passed":true,"summary":"Review completed"}' >> $GITHUB_OUTPUT - echo "passed=$(jq -r '.passed // true' /tmp/requirements_output.json)" >> $GITHUB_OUTPUT - echo "summary=$(jq -r '.summary // "No summary"' /tmp/requirements_output.json | head -c 200)" >> $GITHUB_OUTPUT - - # Rules Review - - name: Rules Review - id: rules - if: needs.config.outputs.rules_review == 'true' - uses: anthropics/claude-code-base-action@beta - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - allowed_tools: "View,GlobTool,Grep,Bash(git diff:*),Bash(git log:*),Bash(cat:*),Bash(find:*)" - max_turns: 20 - prompt: | - # Rules Review Agent - - Review code changes against .claude/rules/*.md files. - - ## Context - - Branch: ${{ steps.context.outputs.branch_name }} - - Changed Files: Read /tmp/changed_files.txt - - ## Task - 1. Find all rules in .claude/rules/ - 2. Match changed files to rules based on frontmatter path patterns - 3. Check compliance with matched rules - 4. Output JSON with: passed, confidence, summary, rule_evaluations - - Output MUST end with ```json block containing {passed, confidence, summary}. - - - name: Extract rules result - id: rules-extract - if: needs.config.outputs.rules_review == 'true' + - name: Update review comment + if: always() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - EXEC_FILE="${{ steps.rules.outputs.execution_file }}" - if [ -f "$EXEC_FILE" ]; then - LAST_TEXT=$(jq -r '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text] | last // ""' "$EXEC_FILE" 2>/dev/null) - OUTPUT=$(echo "$LAST_TEXT" | sed -n '/```json/,/```/{/```json/d;/```/d;p;}' | tr -d '\r') - if echo "$OUTPUT" | jq . >/dev/null 2>&1 && [ -n "$OUTPUT" ]; then - echo "$OUTPUT" > /tmp/rules_output.json - else - echo '{"passed":true,"confidence":0.8,"summary":"Review completed"}' > /tmp/rules_output.json - fi + 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="$NEW_BODY + $ROW" + gh api --method PATCH "/repos/${{ github.repository }}/issues/comments/$COMMENT_ID" -f body="$NEW_BODY" else - echo '{"passed":true,"confidence":0.8,"summary":"Review completed"}' > /tmp/rules_output.json - fi + # Create new comment + COMMENT="$MARKER + ## Reviews - echo "passed=$(jq -r '.passed // true' /tmp/rules_output.json)" >> $GITHUB_OUTPUT - echo "summary=$(jq -r '.summary // "No summary"' /tmp/rules_output.json | head -c 200)" >> $GITHUB_OUTPUT + | Review | Status | Summary | + |--------|--------|---------| + $ROW - # Project Memory Review - - name: Check memory files changed - id: memory-filter - run: | - if grep -qE '(^|/)CLAUDE\.md$|^\.claude/' /tmp/changed_files.txt 2>/dev/null; then - echo "has_memory_changes=true" >> $GITHUB_OUTPUT - else - echo "has_memory_changes=false" >> $GITHUB_OUTPUT + --- + *Claude Code CI*" + gh pr comment "$PR" --body "$COMMENT" fi - - name: Project Memory Review - id: project-memory - if: needs.config.outputs.project_memory_review == 'true' && steps.memory-filter.outputs.has_memory_changes == 'true' + code-quality: + name: "Reviews / Code Quality" + needs: [requirements] + if: github.event_name == 'pull_request' && needs.requirements.outputs.passed == 'true' + runs-on: ubuntu-latest + outputs: + passed: ${{ steps.extract.outputs.passed }} + result: ${{ steps.extract.outputs.result }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get changed files + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh pr view ${{ github.event.pull_request.number }} --json files --jq '.files[].path' > /tmp/changed.txt + + - name: Run review + id: review uses: anthropics/claude-code-base-action@beta with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - allowed_tools: "View,GlobTool,Grep,Bash(cat:*),Bash(find:*),Bash(git diff:*)" - max_turns: 20 + allowed_tools: "View,GlobTool,Grep,Bash(git diff:*),Bash(cat:*)" + max_turns: 15 prompt: | - # Project Memory Review Agent - - Review changes to CLAUDE.md files for compliance. + # Code Quality Review + Evaluate DRY, YAGNI, modularity for files in /tmp/changed.txt + Output ```json {"passed": bool, "summary": "..."} ``` - ## Context - - Branch: ${{ steps.context.outputs.branch_name }} - - Changed Files: Read /tmp/changed_files.txt - - ## Task - 1. Find relevant CLAUDE.md files - 2. Check changes comply with documented rules - 3. Identify if updates are needed based on issue context - 4. Output JSON with: passed, confidence, summary - - Output MUST end with ```json block containing {passed, confidence, summary}. - - - name: Extract project memory result - id: project-memory-extract - if: needs.config.outputs.project_memory_review == 'true' && steps.memory-filter.outputs.has_memory_changes == 'true' + - name: Extract result + id: extract run: | - EXEC_FILE="${{ steps.project-memory.outputs.execution_file }}" - if [ -f "$EXEC_FILE" ]; then - LAST_TEXT=$(jq -r '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text] | last // ""' "$EXEC_FILE" 2>/dev/null) - OUTPUT=$(echo "$LAST_TEXT" | sed -n '/```json/,/```/{/```json/d;/```/d;p;}' | tr -d '\r') - if echo "$OUTPUT" | jq . >/dev/null 2>&1 && [ -n "$OUTPUT" ]; then - echo "$OUTPUT" > /tmp/project_memory_output.json - else - echo '{"passed":true,"confidence":0.8,"summary":"Review completed"}' > /tmp/project_memory_output.json + 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 - else - echo '{"passed":true,"confidence":0.8,"summary":"Review completed"}' > /tmp/project_memory_output.json fi + echo "passed=true" >> $GITHUB_OUTPUT + echo 'result={"passed":true,"summary":"Review completed"}' >> $GITHUB_OUTPUT - echo "passed=$(jq -r '.passed // true' /tmp/project_memory_output.json)" >> $GITHUB_OUTPUT - echo "summary=$(jq -r '.summary // "No summary"' /tmp/project_memory_output.json | head -c 200)" >> $GITHUB_OUTPUT - - # Agents Review - - name: Check agent files changed - id: agents-filter + - name: Update review comment + if: always() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - if grep -qE '(agents/.*\.md|AGENT\.md)' /tmp/changed_files.txt 2>/dev/null; then - echo "has_agent_changes=true" >> $GITHUB_OUTPUT + 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 - echo "has_agent_changes=false" >> $GITHUB_OUTPUT - fi - - - name: Agents Review - id: agents - if: needs.config.outputs.agents_review == 'true' && steps.agents-filter.outputs.has_agent_changes == 'true' - uses: anthropics/claude-code-base-action@beta - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - allowed_tools: "View,GlobTool,Grep,Bash(cat:*),Bash(find:*)" - max_turns: 15 - prompt: | - # Agents Review Agent - - Review .claude/agents/*.md files for quality and context optimization. - - ## Context - - Changed Files: Read /tmp/changed_files.txt - - ## Task - 1. Read changed agent files - 2. Check frontmatter requirements - 3. Evaluate context efficiency, clarity, focus - 4. Output JSON with: passed, confidence, summary, agent_evaluations + # Create new comment (shouldn't happen as requirements runs first) + COMMENT="$MARKER + ## Reviews - Output MUST end with ```json block containing {passed, confidence, summary}. + | Review | Status | Summary | + |--------|--------|---------| + $ROW - - name: Extract agents result - id: agents-extract - if: needs.config.outputs.agents_review == 'true' && steps.agents-filter.outputs.has_agent_changes == 'true' - run: | - EXEC_FILE="${{ steps.agents.outputs.execution_file }}" - if [ -f "$EXEC_FILE" ]; then - LAST_TEXT=$(jq -r '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text] | last // ""' "$EXEC_FILE" 2>/dev/null) - OUTPUT=$(echo "$LAST_TEXT" | sed -n '/```json/,/```/{/```json/d;/```/d;p;}' | tr -d '\r') - if echo "$OUTPUT" | jq . >/dev/null 2>&1 && [ -n "$OUTPUT" ]; then - echo "$OUTPUT" > /tmp/agents_output.json - else - echo '{"passed":true,"confidence":0.8,"summary":"Review completed"}' > /tmp/agents_output.json - fi - else - echo '{"passed":true,"confidence":0.8,"summary":"Review completed"}' > /tmp/agents_output.json + --- + *Claude Code CI*" + gh pr comment "$PR" --body "$COMMENT" fi - echo "passed=$(jq -r '.passed // true' /tmp/agents_output.json)" >> $GITHUB_OUTPUT - echo "summary=$(jq -r '.summary // "No summary"' /tmp/agents_output.json | head -c 200)" >> $GITHUB_OUTPUT + context: + name: "Reviews / Context" + needs: [requirements] + if: github.event_name == 'pull_request' && needs.requirements.outputs.passed == 'true' + runs-on: ubuntu-latest + outputs: + passed: ${{ steps.extract.outputs.passed }} + result: ${{ steps.extract.outputs.result }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 - # Skills Review - - name: Check skill files changed - id: skills-filter + - name: Find parent CLAUDE.md files + id: find + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - if grep -qE '(skills/.*\.md|SKILL\.md)' /tmp/changed_files.txt 2>/dev/null; then - echo "has_skill_changes=true" >> $GITHUB_OUTPUT - else - echo "has_skill_changes=false" >> $GITHUB_OUTPUT - fi + gh pr view ${{ github.event.pull_request.number }} --json files --jq '.files[].path' > /tmp/changed.txt + + > /tmp/claude_files.txt + while IFS= read -r file; do + dir=$(dirname "$file") + while [ "$dir" != "." ]; do + [ -f "$dir/CLAUDE.md" ] && echo "$dir/CLAUDE.md" >> /tmp/claude_files.txt + dir=$(dirname "$dir") + done + [ -f "CLAUDE.md" ] && echo "CLAUDE.md" >> /tmp/claude_files.txt + done < /tmp/changed.txt - - name: Skills Review - id: skills - if: needs.config.outputs.skills_review == 'true' && steps.skills-filter.outputs.has_skill_changes == 'true' + sort -u /tmp/claude_files.txt -o /tmp/claude_files.txt + COUNT=$(wc -l < /tmp/claude_files.txt | tr -d ' ') + echo "count=$COUNT" >> $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: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - allowed_tools: "View,GlobTool,Grep,Bash(cat:*),Bash(find:*)" + allowed_tools: "View,GlobTool,Grep,Bash(cat:*)" max_turns: 15 prompt: | - # Skills Review Agent - - Review .claude/skills/**/SKILL.md files for quality and context efficiency. - - ## Context - - Changed Files: Read /tmp/changed_files.txt + # Context Review + Check changes against CLAUDE.md files listed in /tmp/claude_files.txt + Changed files: /tmp/changed.txt + Output ```json {"passed": bool, "summary": "..."} ``` - ## Task - 1. Read changed skill files - 2. Check frontmatter requirements - 3. Evaluate context efficiency, structure, documentation links - 4. Output JSON with: passed, confidence, summary, skill_evaluations - - Output MUST end with ```json block containing {passed, confidence, summary}. - - - name: Extract skills result - id: skills-extract - if: needs.config.outputs.skills_review == 'true' && steps.skills-filter.outputs.has_skill_changes == 'true' + - name: Extract result + id: extract run: | - EXEC_FILE="${{ steps.skills.outputs.execution_file }}" - if [ -f "$EXEC_FILE" ]; then - LAST_TEXT=$(jq -r '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text] | last // ""' "$EXEC_FILE" 2>/dev/null) - OUTPUT=$(echo "$LAST_TEXT" | sed -n '/```json/,/```/{/```json/d;/```/d;p;}' | tr -d '\r') - if echo "$OUTPUT" | jq . >/dev/null 2>&1 && [ -n "$OUTPUT" ]; then - echo "$OUTPUT" > /tmp/skills_output.json - else - echo '{"passed":true,"confidence":0.8,"summary":"Review completed"}' > /tmp/skills_output.json - fi - else - echo '{"passed":true,"confidence":0.8,"summary":"Review completed"}' > /tmp/skills_output.json + 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 - echo "passed=$(jq -r '.passed // true' /tmp/skills_output.json)" >> $GITHUB_OUTPUT - echo "summary=$(jq -r '.summary // "No summary"' /tmp/skills_output.json | head -c 200)" >> $GITHUB_OUTPUT + 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 - # Playwright UI Review (only if deployable) - - name: Check UI files changed - id: ui-filter + - name: Update review comment + if: always() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - if grep -qE '\.(tsx|css|scss)$' /tmp/changed_files.txt 2>/dev/null; then - echo "has_ui_changes=true" >> $GITHUB_OUTPUT + 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 - echo "has_ui_changes=false" >> $GITHUB_OUTPUT + # Create new comment (shouldn't happen as requirements runs first) + COMMENT="$MARKER + ## Reviews + + | Review | Status | Summary | + |--------|--------|---------| + $ROW + + --- + *Claude Code CI*" + gh pr comment "$PR" --body "$COMMENT" fi - - name: Download screenshots - if: | - needs.config.outputs.playwright_ui_review == 'true' && - steps.ui-filter.outputs.has_ui_changes == 'true' - uses: actions/download-artifact@v4 + ui: + name: "Reviews / UI" + needs: [lint, requirements, e2e] + if: github.event_name == 'pull_request' && needs.lint.outputs.has_app == 'true' && needs.requirements.outputs.passed == 'true' + runs-on: ubuntu-latest + outputs: + passed: ${{ steps.extract.outputs.passed }} + result: ${{ steps.extract.outputs.result }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 with: name: playwright-screenshots-${{ github.sha }} path: .claude/screenshots/ continue-on-error: true - - name: UI Review - id: ui - if: | - needs.config.outputs.playwright_ui_review == 'true' && - steps.ui-filter.outputs.has_ui_changes == 'true' + - name: Check screenshots + id: check + run: | + COUNT=$(ls -1 .claude/screenshots/*.png 2>/dev/null | wc -l || echo "0") + echo "count=$COUNT" >> $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: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - allowed_tools: "View,GlobTool,Grep,Bash(cat:*),Bash(find:*),Bash(ls:*)" - max_turns: 25 + allowed_tools: "View,GlobTool,Bash(ls:*)" + max_turns: 20 model: claude-sonnet-4-5-20250929 prompt: | - # Playwright UI Review Agent - - Review UI screenshots against best practices. - - ## Context - - Screenshots: Check .claude/screenshots/ - - Changed Files: Read /tmp/changed_files.txt + # UI Review + Review screenshots in .claude/screenshots/ + Evaluate design, accessibility, UX + Output ```json {"passed": bool, "summary": "..."} ``` - ## Task - 1. View screenshots in .claude/screenshots/ - 2. Evaluate visual design, layout, accessibility, UX, elegance - 3. Identify critical/major/minor issues - 4. Output JSON with: passed, confidence, summary, best_practices_evaluation - - Output MUST end with ```json block containing {passed, confidence, summary}. - - - name: Extract UI result - id: ui-extract - if: | - needs.config.outputs.playwright_ui_review == 'true' && - steps.ui-filter.outputs.has_ui_changes == 'true' + - name: Extract result + id: extract run: | - EXEC_FILE="${{ steps.ui.outputs.execution_file }}" - if [ -f "$EXEC_FILE" ]; then - LAST_TEXT=$(jq -r '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text] | last // ""' "$EXEC_FILE" 2>/dev/null) - OUTPUT=$(echo "$LAST_TEXT" | sed -n '/```json/,/```/{/```json/d;/```/d;p;}' | tr -d '\r') - if echo "$OUTPUT" | jq . >/dev/null 2>&1 && [ -n "$OUTPUT" ]; then - echo "$OUTPUT" > /tmp/ui_output.json - else - echo '{"passed":true,"confidence":0.8,"summary":"Review completed"}' > /tmp/ui_output.json - fi - else - echo '{"passed":true,"confidence":0.8,"summary":"Review completed"}' > /tmp/ui_output.json + 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 - echo "passed=$(jq -r '.passed // true' /tmp/ui_output.json)" >> $GITHUB_OUTPUT - echo "summary=$(jq -r '.summary // "No summary"' /tmp/ui_output.json | head -c 200)" >> $GITHUB_OUTPUT - - # Collect all results - - name: Collect review outputs - id: collect - run: | - # Set outputs from extract steps (with defaults for skipped reviews) - echo "requirements_passed=${{ steps.requirements-extract.outputs.passed || 'skipped' }}" >> $GITHUB_OUTPUT - echo "requirements_summary=${{ steps.requirements-extract.outputs.summary || 'Skipped' }}" >> $GITHUB_OUTPUT - echo "rules_passed=${{ steps.rules-extract.outputs.passed || 'skipped' }}" >> $GITHUB_OUTPUT - echo "rules_summary=${{ steps.rules-extract.outputs.summary || 'Skipped' }}" >> $GITHUB_OUTPUT - echo "project_memory_passed=${{ steps.project-memory-extract.outputs.passed || 'skipped' }}" >> $GITHUB_OUTPUT - echo "project_memory_summary=${{ steps.project-memory-extract.outputs.summary || 'Skipped' }}" >> $GITHUB_OUTPUT - echo "agents_passed=${{ steps.agents-extract.outputs.passed || 'skipped' }}" >> $GITHUB_OUTPUT - echo "agents_summary=${{ steps.agents-extract.outputs.summary || 'Skipped' }}" >> $GITHUB_OUTPUT - echo "skills_passed=${{ steps.skills-extract.outputs.passed || 'skipped' }}" >> $GITHUB_OUTPUT - echo "skills_summary=${{ steps.skills-extract.outputs.summary || 'Skipped' }}" >> $GITHUB_OUTPUT - echo "ui_passed=${{ steps.ui-extract.outputs.passed || 'skipped' }}" >> $GITHUB_OUTPUT - echo "ui_summary=${{ steps.ui-extract.outputs.summary || 'Skipped' }}" >> $GITHUB_OUTPUT - - # ============================================================================= - # STAGE 5: Post Comment (consolidate all review results) - # ============================================================================= - post-comment: - name: Post Review Summary - needs: [config, basic-ci-complete, e2e-tests, reviews] - if: | - always() && - github.event_name == 'pull_request' && - needs.config.outputs.reviews_enabled == 'true' - runs-on: ubuntu-latest + 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 - steps: - - name: Consolidate and post comment + - name: Update review comment + if: always() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - # Gather all results - BASIC_CI="${{ needs.basic-ci-complete.outputs.success }}" - E2E_RESULT="${{ needs.e2e-tests.result }}" - E2E_PASSED="${{ needs.e2e-tests.outputs.tests_passed }}" - - REQ_PASSED="${{ needs.reviews.outputs.requirements_passed }}" - REQ_SUMMARY="${{ needs.reviews.outputs.requirements_summary }}" - RULES_PASSED="${{ needs.reviews.outputs.rules_passed }}" - RULES_SUMMARY="${{ needs.reviews.outputs.rules_summary }}" - MEMORY_PASSED="${{ needs.reviews.outputs.project_memory_passed }}" - MEMORY_SUMMARY="${{ needs.reviews.outputs.project_memory_summary }}" - AGENTS_PASSED="${{ needs.reviews.outputs.agents_passed }}" - AGENTS_SUMMARY="${{ needs.reviews.outputs.agents_summary }}" - SKILLS_PASSED="${{ needs.reviews.outputs.skills_passed }}" - SKILLS_SUMMARY="${{ needs.reviews.outputs.skills_summary }}" - UI_PASSED="${{ needs.reviews.outputs.ui_passed }}" - UI_SUMMARY="${{ needs.reviews.outputs.ui_summary }}" - - # Determine overall status - OVERALL_PASS=true - if [[ "$BASIC_CI" != "true" ]]; then - OVERALL_PASS=false - fi - if [[ "$E2E_RESULT" == "failure" ]]; then - OVERALL_PASS=false - fi - if [[ "$REQ_PASSED" == "false" ]] || [[ "$RULES_PASSED" == "false" ]] || \ - [[ "$MEMORY_PASSED" == "false" ]] || [[ "$AGENTS_PASSED" == "false" ]] || \ - [[ "$SKILLS_PASSED" == "false" ]] || [[ "$UI_PASSED" == "false" ]]; then - OVERALL_PASS=false - fi - - # Build status icon function - get_icon() { - case "$1" in - true) echo ":white_check_mark:" ;; - false) echo ":x:" ;; - skipped) echo ":white_circle:" ;; - *) echo ":grey_question:" ;; - esac - } - - if [ "$OVERALL_PASS" = "true" ]; then - OVERALL_ICON=":white_check_mark:" - OVERALL_STATUS="All Checks Passed" + 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 - OVERALL_ICON=":x:" - OVERALL_STATUS="Some Checks Failed" - fi + # Create new comment (shouldn't happen as requirements runs first) + COMMENT="$MARKER + ## Reviews - # Build comment - COMMENT="## CI Pipeline Summary ${OVERALL_ICON} - - **Status:** ${OVERALL_STATUS} - **Commit:** \`${{ github.sha }}\` - - ### Basic CI - | Check | Status | - |-------|--------| - | Lint | $(get_icon $BASIC_CI) | - | Typecheck | $(get_icon $BASIC_CI) | - | Unit Tests | $(get_icon $BASIC_CI) | - - ### E2E Tests - | Check | Status | - |-------|--------| - | Playwright | $(get_icon $E2E_PASSED) | - " - - # Add reviews section if any ran - if [[ "${{ needs.config.outputs.reviews_enabled }}" == "true" ]]; then - COMMENT="${COMMENT} - ### Code Reviews | Review | Status | Summary | |--------|--------|---------| - | Requirements | $(get_icon $REQ_PASSED) | ${REQ_SUMMARY:-N/A} | - | Rules | $(get_icon $RULES_PASSED) | ${RULES_SUMMARY:-N/A} | - | Project Memory | $(get_icon $MEMORY_PASSED) | ${MEMORY_SUMMARY:-N/A} | - | Agents | $(get_icon $AGENTS_PASSED) | ${AGENTS_SUMMARY:-N/A} | - | Skills | $(get_icon $SKILLS_PASSED) | ${SKILLS_SUMMARY:-N/A} | - | UI | $(get_icon $UI_PASSED) | ${UI_SUMMARY:-N/A} | - " - fi + $ROW - COMMENT="${COMMENT} --- - *Automated CI Pipeline by Claude Code* - [View Full Run](/${{ github.repository }}/actions/runs/${{ github.run_id }})" - - # Find and update existing comment or create new one - COMMENT_ID=$(gh api \ - -H "Accept: application/vnd.github+json" \ - "/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \ - --jq '.[] | select(.body | contains("CI Pipeline Summary")) | .id' | head -1) - - if [ -n "$COMMENT_ID" ]; then - # Update existing comment - gh api \ - -X PATCH \ - -H "Accept: application/vnd.github+json" \ - "/repos/${{ github.repository }}/issues/comments/$COMMENT_ID" \ - -f body="$COMMENT" - else - # Create new comment - gh pr comment ${{ github.event.pull_request.number }} --body "$COMMENT" + *Claude Code CI*" + gh pr comment "$PR" --body "$COMMENT" fi - - # ============================================================================= - # Final Status Check - # ============================================================================= - ci-complete: - name: CI Complete - needs: [config, basic-ci-complete, e2e-tests, reviews, post-comment] - if: always() - runs-on: ubuntu-latest - steps: - - name: Check final status - run: | - BASIC_CI="${{ needs.basic-ci-complete.result }}" - E2E="${{ needs.e2e-tests.result }}" - REVIEWS="${{ needs.reviews.result }}" - - echo "Basic CI: $BASIC_CI" - echo "E2E: $E2E" - echo "Reviews: $REVIEWS" - - # Fail if any required job failed - if [[ "$BASIC_CI" == "failure" ]]; then - echo "::error::Basic CI failed" - exit 1 - fi - - if [[ "$E2E" == "failure" ]]; then - echo "::error::E2E tests failed" - exit 1 - fi - - if [[ "$REVIEWS" == "failure" ]]; then - echo "::error::Code reviews failed" - exit 1 - fi - - echo "All CI checks completed successfully!" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 4635323..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - types: [opened, synchronize, reopened] - merge_group: - -jobs: - Lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: constellos/.github/actions/lint@main - - Typecheck: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: constellos/.github/actions/typecheck@main - - Unit-Tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: constellos/.github/actions/unit-tests@main diff --git a/.github/workflows/deployment-ci.yml b/.github/workflows/deployment-ci.yml deleted file mode 100644 index 8ac1645..0000000 --- a/.github/workflows/deployment-ci.yml +++ /dev/null @@ -1,288 +0,0 @@ -name: Deployment CI - -on: - workflow_run: - workflows: ["Code Reviews"] - types: [completed] - -permissions: - contents: read - pull-requests: write - deployments: write - -concurrency: - group: deployment-${{ github.ref }} - cancel-in-progress: true - -jobs: - check-config: - runs-on: ubuntu-latest - outputs: - has_deployment: ${{ steps.detect.outputs.has_deployment }} - deployment_enabled: ${{ steps.detect.outputs.deployment_enabled }} - has_vercel: ${{ steps.detect.outputs.has_vercel }} - has_supabase: ${{ steps.detect.outputs.has_supabase }} - has_cloudflare: ${{ steps.detect.outputs.has_cloudflare }} - steps: - - uses: actions/checkout@v4 - - - name: Install yq - run: | - sudo wget https://github.com/mikefarah/yq/releases/download/v4.35.1/yq_linux_amd64 -O /usr/local/bin/yq - sudo chmod +x /usr/local/bin/yq - - - name: Detect deployment config - id: detect - run: | - HAS_DEPLOYMENT=false - DEPLOYMENT_ENABLED=false - HAS_VERCEL=false - HAS_SUPABASE=false - HAS_CLOUDFLARE=false - - # Check for config files - [ -f "vercel.json" ] && HAS_VERCEL=true && HAS_DEPLOYMENT=true - [ -d "supabase" ] && HAS_SUPABASE=true && HAS_DEPLOYMENT=true - [ -f "wrangler.toml" ] && HAS_CLOUDFLARE=true && HAS_DEPLOYMENT=true - - # Check .github/ci-config.yml (REQUIRED for enabling deployments) - if [ -f ".github/ci-config.yml" ]; then - DEPLOY_ENABLED=$(yq '.ci.deployment.enabled // false' .github/ci-config.yml) - if [ "$DEPLOY_ENABLED" = "true" ]; then - DEPLOYMENT_ENABLED=true - fi - fi - - echo "has_deployment=$HAS_DEPLOYMENT" >> $GITHUB_OUTPUT - echo "deployment_enabled=$DEPLOYMENT_ENABLED" >> $GITHUB_OUTPUT - echo "has_vercel=$HAS_VERCEL" >> $GITHUB_OUTPUT - echo "has_supabase=$HAS_SUPABASE" >> $GITHUB_OUTPUT - echo "has_cloudflare=$HAS_CLOUDFLARE" >> $GITHUB_OUTPUT - - # Monitor Vercel deployment (triggered by Vercel GitHub integration) - vercel-preview: - name: Monitor Vercel Preview Deployment - needs: check-config - if: needs.check-config.outputs.has_vercel == 'true' && needs.check-config.outputs.deployment_enabled == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - # Vercel deploys automatically via GitHub integration - # We MONITOR the deployment and fail CI if it fails - - - name: Wait for Vercel deployment - uses: UnlyEd/github-action-await-vercel@v1 - id: await-vercel - with: - timeout: 600 # 10 minutes - poll-interval: 5 # seconds - env: - VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} - - - name: Check deployment status - if: steps.await-vercel.outcome != 'success' - run: | - echo "::error::Vercel deployment failed or timed out" - exit 1 - - - name: Comment deployment URL on PR - if: github.event_name == 'pull_request' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - DEPLOY_URL="${{ steps.await-vercel.outputs.url }}" - gh pr comment ${{ github.event.pull_request.number }} \ - --body "### ✅ Vercel Preview Deployed\n\nDeployed to: $DEPLOY_URL" - - # Monitor Supabase branch deployment (triggered by Supabase integration) - supabase-migrations: - name: Monitor Supabase Branch - needs: check-config - if: needs.check-config.outputs.has_supabase == 'true' && needs.check-config.outputs.deployment_enabled == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: supabase/setup-cli@v1 - with: - version: latest - - # Supabase creates preview branches automatically - # We check the branch status to ensure migrations succeeded - - - name: Wait for Supabase branch to be healthy - env: - SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} - run: | - BRANCH_NAME="preview-pr-${{ github.event.pull_request.number }}" - PROJECT_REF="${{ secrets.SUPABASE_PROJECT_ID }}" - - echo "Waiting for Supabase branch '$BRANCH_NAME' to be healthy..." - - TIMEOUT=300 # 5 minutes - POLL_INTERVAL=15 - START_TIME=$(date +%s) - - while true; do - # Get branch status - STATUS=$(supabase branches get "$BRANCH_NAME" --project-ref $PROJECT_REF --output json 2>/dev/null | \ - jq -r '.database.status' || echo "NOT_FOUND") - - echo "Branch status: $STATUS" - - case $STATUS in - ACTIVE_HEALTHY) - echo "✅ Supabase branch is healthy" - exit 0 - ;; - FAILED|REMOVED) - echo "❌ Supabase branch failed or was removed" - exit 1 - ;; - COMING_UP|GOING_DOWN|UPGRADING|RESTORING) - echo "Branch is transitioning..." - ;; - NOT_FOUND) - echo "Waiting for branch to be created..." - ;; - esac - - # Check timeout - CURRENT_TIME=$(date +%s) - ELAPSED=$((CURRENT_TIME - START_TIME)) - if [ $ELAPSED -gt $TIMEOUT ]; then - echo "❌ Branch health check timed out after ${TIMEOUT}s" - exit 1 - fi - - sleep $POLL_INTERVAL - done - - - name: Comment Supabase branch on PR - if: github.event_name == 'pull_request' && success() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - BRANCH_NAME="preview-pr-${{ github.event.pull_request.number }}" - gh pr comment ${{ github.event.pull_request.number }} \ - --body "### ✅ Supabase Branch Healthy\n\nBranch: \`$BRANCH_NAME\`" - - # Monitor Cloudflare Pages deployment (triggered by Cloudflare integration) - cloudflare-deploy: - name: Monitor Cloudflare Pages Deployment - needs: check-config - if: needs.check-config.outputs.has_cloudflare == 'true' && needs.check-config.outputs.deployment_enabled == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - # Cloudflare deploys automatically via GitHub integration - # We poll the Cloudflare API to check deployment status - - - name: Wait for Cloudflare deployment - id: wait - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - CLOUDFLARE_PROJECT_NAME: ${{ secrets.CLOUDFLARE_PROJECT_NAME }} - run: | - # Get latest deployment for this commit - COMMIT_SHA="${{ github.event.pull_request.head.sha || github.sha }}" - - echo "Waiting for Cloudflare Pages deployment of commit $COMMIT_SHA..." - - TIMEOUT=600 - POLL_INTERVAL=10 - START_TIME=$(date +%s) - - while true; do - # Get deployments and find the one for this commit - DEPLOYMENTS=$(curl -s "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/pages/projects/$CLOUDFLARE_PROJECT_NAME/deployments" \ - -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN") - - DEPLOYMENT=$(echo "$DEPLOYMENTS" | jq -r ".result[] | select(.deployment_trigger.metadata.commit_hash == \"$COMMIT_SHA\")" | head -1) - - if [ -n "$DEPLOYMENT" ] && [ "$DEPLOYMENT" != "null" ]; then - DEPLOYMENT_ID=$(echo "$DEPLOYMENT" | jq -r '.id') - STATUS=$(echo "$DEPLOYMENT" | jq -r '.latest_stage.status') - - echo "Deployment status: $STATUS" - - case $STATUS in - success) - echo "✅ Cloudflare Pages deployment succeeded" - DEPLOY_URL=$(echo "$DEPLOYMENT" | jq -r '.url') - echo "deploy_url=$DEPLOY_URL" >> $GITHUB_OUTPUT - exit 0 - ;; - failure) - echo "❌ Cloudflare Pages deployment failed" - exit 1 - ;; - active) - echo "Deployment in progress..." - ;; - esac - else - echo "Waiting for deployment to start..." - fi - - # Check timeout - CURRENT_TIME=$(date +%s) - ELAPSED=$((CURRENT_TIME - START_TIME)) - if [ $ELAPSED -gt $TIMEOUT ]; then - echo "❌ Deployment timed out after ${TIMEOUT}s" - exit 1 - fi - - sleep $POLL_INTERVAL - done - - - name: Comment deployment URL on PR - if: github.event_name == 'pull_request' && success() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - DEPLOY_URL="${{ steps.wait.outputs.deploy_url }}" - gh pr comment ${{ github.event.pull_request.number }} \ - --body "### ✅ Cloudflare Pages Deployed\n\nDeployed to: $DEPLOY_URL" - - # Summary job that checks all deployment results - deployment-summary: - name: Deployment Summary - needs: [check-config, vercel-preview, supabase-migrations, cloudflare-deploy] - if: always() && needs.check-config.outputs.deployment_enabled == 'true' - runs-on: ubuntu-latest - steps: - - name: Check deployment results - run: | - VERCEL_STATUS="${{ needs.vercel-preview.result }}" - SUPABASE_STATUS="${{ needs.supabase-migrations.result }}" - CLOUDFLARE_STATUS="${{ needs.cloudflare-deploy.result }}" - - echo "Deployment Results:" - echo " Vercel: $VERCEL_STATUS" - echo " Supabase: $SUPABASE_STATUS" - echo " Cloudflare: $CLOUDFLARE_STATUS" - - # Fail if any deployment failed - if [ "$VERCEL_STATUS" = "failure" ] || \ - [ "$SUPABASE_STATUS" = "failure" ] || \ - [ "$CLOUDFLARE_STATUS" = "failure" ]; then - echo "::error::One or more deployments failed" - exit 1 - fi - - # No-op job for repos without deployments - no-deployment-needed: - name: No Deployment Configured - needs: check-config - if: needs.check-config.outputs.deployment_enabled != 'true' - runs-on: ubuntu-latest - steps: - - name: Skip deployments - run: | - echo "ℹ️ Deployments are disabled for this repository" - echo "This repo has deployment.enabled: false in .github/ci-config.yml" - echo "Repos without deployments (like claude-code-actions, claude-code-plugins) skip this workflow" diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml deleted file mode 100644 index bc0b46d..0000000 --- a/.github/workflows/e2e-tests.yml +++ /dev/null @@ -1,130 +0,0 @@ -name: E2E Tests - -# Runs Playwright tests when UI files change -# Depends on Basic CI success -# Uploads screenshots and test reports for downstream reviews - -on: - workflow_run: - workflows: ["Basic CI"] - types: [completed] - pull_request: - paths: - - 'apps/**' - - 'packages/ui/**' - - '**/*.tsx' - - '**/*.css' - -concurrency: - group: e2e-${{ github.ref }} - cancel-in-progress: true - -jobs: - check-prerequisites: - runs-on: ubuntu-latest - outputs: - can_run: ${{ steps.check.outputs.can_run }} - steps: - - name: Check Basic CI status - id: check - run: | - if [ "${{ github.event_name }}" = "workflow_run" ]; then - if [ "${{ github.event.workflow_run.conclusion }}" != "success" ]; then - echo "::error::Basic CI must pass before E2E tests" - echo "can_run=false" >> $GITHUB_OUTPUT - exit 1 - fi - fi - echo "can_run=true" >> $GITHUB_OUTPUT - - playwright-tests: - needs: check-prerequisites - if: needs.check-prerequisites.outputs.can_run == 'true' - runs-on: ubuntu-latest - outputs: - tests_passed: ${{ steps.playwright.outcome == 'success' }} - screenshot_count: ${{ steps.screenshots.outputs.count }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - - - name: Install dependencies - run: npm ci - - - name: Install Playwright browsers - run: npx playwright install chromium --with-deps - - - name: Run Playwright tests - id: playwright - continue-on-error: true - run: | - # Create screenshots directory - mkdir -p .playwright-screenshots - - # Run tests with screenshot capture - npx playwright test \ - --project=chromium \ - --reporter=list,html \ - --screenshot=on \ - --output=.playwright-screenshots \ - 2>&1 | tee playwright-output.txt - - # Store exit code - echo "exit_code=$?" >> $GITHUB_OUTPUT - - - name: Collect screenshots - id: screenshots - run: | - # Create consolidated screenshots directory - mkdir -p .claude/screenshots - - # Copy screenshots from test results - find .playwright-screenshots -name "*.png" -type f | while read -r file; do - # Create unique name from path - UNIQUE_NAME=$(echo "$file" | sed 's/[\/\.]/_/g' | sed 's/^_//') - cp "$file" ".claude/screenshots/${UNIQUE_NAME}.png" - done - - # Also check for test-results directory (Playwright default) - if [ -d "test-results" ]; then - find test-results -name "*.png" -type f | while read -r file; do - UNIQUE_NAME=$(echo "$file" | sed 's/[\/\.]/_/g' | sed 's/^_//') - cp "$file" ".claude/screenshots/${UNIQUE_NAME}.png" 2>/dev/null || true - done - fi - - # Count screenshots - COUNT=$(ls -1 .claude/screenshots/*.png 2>/dev/null | wc -l) - echo "count=$COUNT" >> $GITHUB_OUTPUT - - # List screenshots for Claude - ls -la .claude/screenshots/ > .claude/screenshot-manifest.txt 2>/dev/null || echo "No screenshots" > .claude/screenshot-manifest.txt - - - name: Upload screenshots artifact - if: steps.screenshots.outputs.count > 0 - uses: actions/upload-artifact@v4 - with: - name: playwright-screenshots-${{ github.sha }} - path: .claude/screenshots/ - retention-days: 7 - - - name: Upload test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: playwright-report-${{ github.sha }} - path: playwright-report/ - retention-days: 7 - - - name: Check test results - run: | - if [ "${{ steps.playwright.outcome }}" != "success" ]; then - echo "::error::Playwright tests failed. UI review will still proceed with available screenshots." - exit 1 - fi diff --git a/.github/workflows/playwright-ui-review.yml b/.github/workflows/playwright-ui-review.yml deleted file mode 100644 index cea3b3f..0000000 --- a/.github/workflows/playwright-ui-review.yml +++ /dev/null @@ -1,448 +0,0 @@ -name: Playwright UI Review - -# Runs all Playwright tests and sends screenshots to Claude for UI review -# Fails if tests don't pass, then checks UI against rules and best practices -# Advises on UI quality - DOES NOT edit files - -on: - pull_request: - branches: - - '**' - paths: - - 'apps/**' - - 'packages/ui/**' - - '**/*.tsx' - - '**/*.css' - - '**/tailwind.config.*' - -permissions: - contents: read - pull-requests: write - statuses: write - -concurrency: - group: playwright-ui-review-${{ github.ref }} - cancel-in-progress: true - -jobs: - playwright-tests: - runs-on: ubuntu-latest - outputs: - tests_passed: ${{ steps.playwright.outcome == 'success' }} - screenshot_count: ${{ steps.screenshots.outputs.count }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - # cache removed - no package.json in this repo - - - name: Install dependencies - run: npm ci - - - name: Install Playwright browsers - run: npx playwright install chromium --with-deps - - - name: Run Playwright tests - id: playwright - continue-on-error: true - run: | - # Create screenshots directory - mkdir -p .playwright-screenshots - - # Run tests with screenshot capture - npx playwright test \ - --project=chromium \ - --reporter=list,html \ - --screenshot=on \ - --output=.playwright-screenshots \ - 2>&1 | tee playwright-output.txt - - # Store exit code - echo "exit_code=$?" >> $GITHUB_OUTPUT - - - name: Collect screenshots - id: screenshots - run: | - # Create consolidated screenshots directory - mkdir -p .claude/screenshots - - # Copy screenshots from test results - find .playwright-screenshots -name "*.png" -type f | while read -r file; do - # Create unique name from path - UNIQUE_NAME=$(echo "$file" | sed 's/[\/\.]/_/g' | sed 's/^_//') - cp "$file" ".claude/screenshots/${UNIQUE_NAME}.png" - done - - # Also check for test-results directory (Playwright default) - if [ -d "test-results" ]; then - find test-results -name "*.png" -type f | while read -r file; do - UNIQUE_NAME=$(echo "$file" | sed 's/[\/\.]/_/g' | sed 's/^_//') - cp "$file" ".claude/screenshots/${UNIQUE_NAME}.png" 2>/dev/null || true - done - fi - - # Count screenshots - COUNT=$(ls -1 .claude/screenshots/*.png 2>/dev/null | wc -l) - echo "count=$COUNT" >> $GITHUB_OUTPUT - - # List screenshots for Claude - ls -la .claude/screenshots/ > .claude/screenshot-manifest.txt 2>/dev/null || echo "No screenshots" > .claude/screenshot-manifest.txt - - - name: Upload screenshots artifact - if: steps.screenshots.outputs.count > 0 - uses: actions/upload-artifact@v4 - with: - name: playwright-screenshots-${{ github.sha }} - path: .claude/screenshots/ - retention-days: 7 - - - name: Upload test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: playwright-report-${{ github.sha }} - path: playwright-report/ - retention-days: 7 - - - name: Check test results - run: | - if [ "${{ steps.playwright.outcome }}" != "success" ]; then - echo "::error::Playwright tests failed. UI review will still proceed with available screenshots." - fi - - ui-review: - needs: playwright-tests - runs-on: ubuntu-latest - outputs: - review_passed: ${{ steps.review.outputs.passed }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download screenshots - uses: actions/download-artifact@v4 - with: - name: playwright-screenshots-${{ github.sha }} - path: .claude/screenshots/ - continue-on-error: true - - - name: Extract context - id: context - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - BRANCH_NAME="${{ github.head_ref || github.ref_name }}" - echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT - - # Get changed UI files - CHANGED_FILES=$(gh pr view ${{ github.event.pull_request.number }} --json files --jq '.files[].path' | grep -E '\.(tsx|css|scss)$' || echo "") - echo "$CHANGED_FILES" > /tmp/changed_ui_files.txt - - # Check for screenshots - SCREENSHOT_COUNT=$(ls -1 .claude/screenshots/*.png 2>/dev/null | wc -l) - echo "screenshot_count=$SCREENSHOT_COUNT" >> $GITHUB_OUTPUT - - - name: Gather UI rules - id: rules - run: | - # Find UI-related rules - mkdir -p /tmp/ui-rules - - # Look for ui-prefixed skills - find .claude/skills -name "ui-*" -type d 2>/dev/null | while read -r dir; do - if [ -f "$dir/SKILL.md" ]; then - cp "$dir/SKILL.md" "/tmp/ui-rules/$(basename $dir).md" - fi - done - - # Look for UI rules in .claude/rules - find .claude/rules -name "ui-*" -o -name "*-ui-*" 2>/dev/null | while read -r file; do - cp "$file" "/tmp/ui-rules/" 2>/dev/null || true - done - - # Count rules found - RULES_COUNT=$(ls -1 /tmp/ui-rules/*.md 2>/dev/null | wc -l) - echo "rules_count=$RULES_COUNT" >> $GITHUB_OUTPUT - - - name: UI Review with Claude - id: review - uses: anthropics/claude-code-base-action@beta - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - allowed_tools: "View,GlobTool,Grep,Bash(cat:*),Bash(find:*),Bash(ls:*)" - max_turns: 25 - model: claude-sonnet-4-5-20250929 - prompt: | - # Playwright UI Review Agent - - You are a UI review specialist. Your job is to review UI screenshots from Playwright tests and evaluate them against UI rules and modern best practices. - - ## Context - - Branch: ${{ steps.context.outputs.branch_name }} - - Screenshot Count: ${{ steps.context.outputs.screenshot_count }} - - UI Rules Count: ${{ steps.rules.outputs.rules_count }} - - Tests Passed: ${{ needs.playwright-tests.outputs.tests_passed }} - - ## Your Task - - 1. **Check test status**: - - If tests failed, note this as a blocking issue - - 2. **Review screenshots** (in .claude/screenshots/): - - Read the screenshot manifest: .claude/screenshot-manifest.txt - - View each screenshot file - - Note the file path for context on what UI element it shows - - 3. **Check against UI rules** (in /tmp/ui-rules/): - - Read each UI rule/skill file - - Evaluate screenshots against documented standards - - 4. **Evaluate against modern UI best practices**: - - **Visual Design:** - - Clear visual hierarchy - - Consistent spacing (8px grid) - - Accessible color contrast (WCAG 2.1 AA: 4.5:1 minimum) - - Readable typography - - Dark/light mode support if applicable - - **Layout & Responsiveness:** - - Mobile-first approach - - Proper responsive breakpoints - - No layout shifts or overflow issues - - **Accessibility:** - - Visible focus indicators - - Proper touch targets (44x44px minimum) - - Semantic structure visible in UI - - **User Experience:** - - Clear call-to-action buttons - - Logical flow and navigation - - Loading and error states handled - - Empty states designed - - **Modern Elegance:** - - Clean, uncluttered design - - Consistent component styling - - Appropriate use of whitespace - - Professional and polished appearance - - 5. **Categorize issues**: - - **Critical**: Broken layout, missing content, accessibility violations - - **Major**: Poor UX, inconsistent styling, confusing navigation - - **Minor**: Small visual tweaks, optimization opportunities - - 6. **Make your decision**: - - PASS: No critical issues, acceptable visual quality - - FAIL: Critical issues found or tests failed - - ## IMPORTANT RULES - - - **DO NOT edit any files** - you are a reviewer only - - If no screenshots available, note this and skip visual review - - Tests failing is always a blocking issue - - Be specific about which screenshot shows each issue - - Suggest concrete improvements - - ## Output Format - - Provide screenshot-by-screenshot analysis, then output your final decision as a JSON code block. - - Your response MUST end with a JSON code block in this exact format: - - ```json - { - "passed": true/false, - "confidence": 0.0-1.0, - "tests_passed": true/false, - "screenshot_reviews": [ - { - "screenshot_path": "path/to/screenshot.png", - "description": "what this screenshot shows", - "critical_issues": ["critical issues"], - "major_issues": ["major issues"], - "minor_issues": ["minor issues"], - "positive_aspects": ["good things"] - } - ], - "rule_compliance": [ - { - "rule": "rule name", - "compliant": true/false, - "violations": ["violations"] - } - ], - "best_practices_evaluation": { - "visual_design": 1-10, - "layout_responsiveness": 1-10, - "accessibility": 1-10, - "user_experience": 1-10, - "modern_elegance": 1-10 - }, - "total_critical": 0, - "total_major": 0, - "total_minor": 0, - "top_recommendations": ["recommendations"], - "summary": "Brief summary of your decision" - } - ``` - - - name: Extract review result - id: extract - run: | - EXEC_FILE="${{ steps.review.outputs.execution_file }}" - CONCLUSION="${{ steps.review.outputs.conclusion }}" - - if [ "$CONCLUSION" = "success" ]; then - DEFAULT_OUTPUT='{"passed":true,"confidence":0.8,"tests_passed":true,"summary":"Review completed successfully"}' - else - DEFAULT_OUTPUT='{"passed":false,"confidence":0.5,"tests_passed":false,"summary":"Review completed with issues"}' - fi - - if [ -f "$EXEC_FILE" ]; then - LAST_TEXT=$(jq -r '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text] | last // ""' "$EXEC_FILE" 2>/dev/null) - OUTPUT=$(echo "$LAST_TEXT" | sed -n '/```json/,/```/{/```json/d;/```/d;p;}' | tr -d '\r') - if echo "$OUTPUT" | jq . >/dev/null 2>&1 && [ -n "$OUTPUT" ]; then - echo "$OUTPUT" > /tmp/review_output.json - else - echo "$DEFAULT_OUTPUT" > /tmp/review_output.json - fi - else - echo "$DEFAULT_OUTPUT" > /tmp/review_output.json - fi - - PASSED=$(jq -r '.passed // false' /tmp/review_output.json) - SUMMARY=$(jq -r '.summary // "No summary"' /tmp/review_output.json | head -c 200) - TESTS_PASSED=$(jq -r '.tests_passed // false' /tmp/review_output.json) - echo "passed=$PASSED" >> $GITHUB_OUTPUT - echo "summary=$SUMMARY" >> $GITHUB_OUTPUT - echo "tests_passed=$TESTS_PASSED" >> $GITHUB_OUTPUT - - - name: Post review comment - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - OUTPUT=$(cat /tmp/review_output.json) - PASSED=$(echo "$OUTPUT" | jq -r '.passed // false') - CONFIDENCE=$(echo "$OUTPUT" | jq -r '.confidence // 0') - SUMMARY=$(echo "$OUTPUT" | jq -r '.summary // "No summary available"') - TESTS_PASSED=$(echo "$OUTPUT" | jq -r '.tests_passed // false') - - CRITICAL=$(echo "$OUTPUT" | jq -r '.total_critical // 0') - MAJOR=$(echo "$OUTPUT" | jq -r '.total_major // 0') - MINOR=$(echo "$OUTPUT" | jq -r '.total_minor // 0') - - if [ "$PASSED" = "true" ]; then - STATUS_ICON=":white_check_mark:" - STATUS_TEXT="PASSED" - else - STATUS_ICON=":x:" - STATUS_TEXT="FAILED" - fi - - COMMENT_BODY="## Playwright UI Review ${STATUS_ICON} ${STATUS_TEXT} - - **Confidence:** ${CONFIDENCE} - **Tests Passed:** $TESTS_PASSED - - ### Issue Summary - | Level | Count | - |-------|-------| - | :rotating_light: Critical | ${CRITICAL} | - | :warning: Major | ${MAJOR} | - | :information_source: Minor | ${MINOR} | - - ### Summary - ${SUMMARY} - " - - # Add best practices scores - BP=$(echo "$OUTPUT" | jq '.best_practices_evaluation // {}') - if [ "$BP" != "{}" ]; then - VISUAL=$(echo "$BP" | jq -r '.visual_design // "N/A"') - LAYOUT=$(echo "$BP" | jq -r '.layout_responsiveness // "N/A"') - A11Y=$(echo "$BP" | jq -r '.accessibility // "N/A"') - UX=$(echo "$BP" | jq -r '.user_experience // "N/A"') - ELEGANCE=$(echo "$BP" | jq -r '.modern_elegance // "N/A"') - - COMMENT_BODY="${COMMENT_BODY} - ### Best Practices Scores - | Category | Score | - |----------|-------| - | Visual Design | ${VISUAL}/10 | - | Layout & Responsiveness | ${LAYOUT}/10 | - | Accessibility | ${A11Y}/10 | - | User Experience | ${UX}/10 | - | Modern Elegance | ${ELEGANCE}/10 | - " - fi - - # Add top recommendations - RECS=$(echo "$OUTPUT" | jq -r '.top_recommendations // [] | .[]' 2>/dev/null) - if [ -n "$RECS" ]; then - COMMENT_BODY="${COMMENT_BODY} - ### Top Recommendations - $(echo "$RECS" | while read -r rec; do echo "1. $rec"; done) - " - fi - - # Add critical issues if any - if [ "$CRITICAL" -gt 0 ]; then - COMMENT_BODY="${COMMENT_BODY} - ### :rotating_light: Critical Issues - $(echo "$OUTPUT" | jq -r '.screenshot_reviews[]? | select(.critical_issues | length > 0) | "**\(.screenshot_path)**:\n\(.critical_issues | map(\"- \" + .) | join(\"\n\"))"' 2>/dev/null) - " - fi - - COMMENT_BODY="${COMMENT_BODY} - - --- - *Automated Playwright UI Review by Claude Code* - - [View Screenshots Artifact](/${{ github.repository }}/actions/runs/${{ github.run_id }})" - - gh pr comment ${{ github.event.pull_request.number }} --body "$COMMENT_BODY" - - - name: Set commit status - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PASSED="${{ steps.extract.outputs.passed }}" - OUTPUT=$(cat /tmp/review_output.json) - CRITICAL=$(echo "$OUTPUT" | jq -r '.total_critical // 0') - - if [ "$PASSED" = "true" ]; then - STATE="success" - DESCRIPTION="UI review passed" - else - STATE="failure" - DESCRIPTION="UI review failed: ${CRITICAL} critical issues" - fi - - gh api repos/${{ github.repository }}/statuses/${{ github.sha }} \ - -f state="$STATE" \ - -f target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ - -f description="$DESCRIPTION" \ - -f context="Playwright UI Review" - - - name: Fail if review failed - run: | - PASSED="${{ steps.extract.outputs.passed }}" - TESTS_PASSED="${{ needs.playwright-tests.outputs.tests_passed }}" - - if [ "$TESTS_PASSED" != "true" ]; then - echo "::error::Playwright tests failed. Fix tests before UI can be approved." - exit 1 - fi - - if [ "$PASSED" != "true" ]; then - echo "::error::UI review failed. See PR comment for details." - exit 1 - fi diff --git a/.github/workflows/reviews.yml b/.github/workflows/reviews.yml deleted file mode 100644 index d823520..0000000 --- a/.github/workflows/reviews.yml +++ /dev/null @@ -1,1788 +0,0 @@ -name: Code Reviews - -# Orchestrates all 6 Claude Code review types -# Runs on every commit (any branch) -# Depends on E2E Tests passing or skipping - -on: - workflow_run: - workflows: ["E2E Tests", "Basic CI"] - types: [completed] - push: - branches: ['**'] - pull_request: - branches: ['**'] - -permissions: - contents: read - pull-requests: write - issues: write - statuses: write - -concurrency: - group: reviews-${{ github.ref }} - cancel-in-progress: true - -jobs: - check-prerequisites: - runs-on: ubuntu-latest - outputs: - can_proceed: ${{ steps.check.outputs.can_proceed }} - steps: - - name: Verify E2E passed or skipped - id: check - run: | - if [ "${{ github.event_name }}" = "workflow_run" ]; then - CONCLUSION="${{ github.event.workflow_run.conclusion }}" - if [ "$CONCLUSION" != "success" ] && [ "$CONCLUSION" != "skipped" ]; then - echo "::error::Prerequisites failed" - echo "can_proceed=false" >> $GITHUB_OUTPUT - exit 1 - fi - fi - echo "can_proceed=true" >> $GITHUB_OUTPUT - - requirements-review: - needs: check-prerequisites - if: needs.check-prerequisites.outputs.can_proceed == 'true' - runs-on: ubuntu-latest - outputs: - review_passed: ${{ steps.review.outputs.passed }} - structured_output: ${{ steps.review.outputs.structured_output }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Extract branch context - id: context - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - BRANCH_NAME="${{ github.head_ref || github.ref_name }}" - echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT - - # Extract issue number from branch name patterns: - # feature/123-description, fix/issue-456, 123-my-feature - ISSUE_NUMBER="" - if [[ $BRANCH_NAME =~ ^(feature|fix|bugfix|hotfix)/([0-9]+) ]]; then - ISSUE_NUMBER="${BASH_REMATCH[2]}" - elif [[ $BRANCH_NAME =~ ^([0-9]+)- ]]; then - ISSUE_NUMBER="${BASH_REMATCH[1]}" - elif [[ $BRANCH_NAME =~ issue-([0-9]+) ]]; then - ISSUE_NUMBER="${BASH_REMATCH[1]}" - fi - - echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT - - # Get changed files - if [ "${{ github.event_name }}" = "pull_request" ]; then - CHANGED_FILES=$(gh pr view ${{ github.event.pull_request.number }} --json files --jq '.files[].path' | tr '\n' ' ') - else - CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD 2>/dev/null | tr '\n' ' ' || echo "") - fi - echo "changed_files=$CHANGED_FILES" >> $GITHUB_OUTPUT - - - name: Get issue content - id: issue - if: steps.context.outputs.issue_number != '' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - ISSUE_NUM="${{ steps.context.outputs.issue_number }}" - - # Get issue details - ISSUE_TITLE=$(gh issue view $ISSUE_NUM --json title --jq '.title' 2>/dev/null || echo "") - ISSUE_BODY=$(gh issue view $ISSUE_NUM --json body --jq '.body' 2>/dev/null || echo "") - - # Get issue comments - ISSUE_COMMENTS=$(gh issue view $ISSUE_NUM --json comments --jq '.comments[].body' 2>/dev/null | head -c 5000 || echo "") - - # Save to files for Claude to read - echo "$ISSUE_TITLE" > /tmp/issue_title.txt - echo "$ISSUE_BODY" > /tmp/issue_body.txt - echo "$ISSUE_COMMENTS" > /tmp/issue_comments.txt - - echo "has_issue=true" >> $GITHUB_OUTPUT - - - name: Get PR comments - id: pr_comments - if: github.event_name == 'pull_request' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PR_NUM="${{ github.event.pull_request.number }}" - - # Get PR comments - PR_COMMENTS=$(gh pr view $PR_NUM --json comments --jq '.comments[].body' 2>/dev/null | head -c 5000 || echo "") - - # Get review comments - REVIEW_COMMENTS=$(gh pr view $PR_NUM --json reviews --jq '.reviews[].body' 2>/dev/null | head -c 3000 || echo "") - - echo "$PR_COMMENTS" > /tmp/pr_comments.txt - echo "$REVIEW_COMMENTS" > /tmp/review_comments.txt - - echo "has_pr_comments=true" >> $GITHUB_OUTPUT - - - name: Requirements Review with Claude - id: review - uses: anthropics/claude-code-base-action@beta - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - allowed_tools: "View,GlobTool,Grep,Bash(git diff:*),Bash(git log:*),Bash(cat:*)" - max_turns: 15 - prompt: | - # Requirements Review Agent - - You are a requirements review agent. Your job is to verify that code changes align with the linked issue requirements and any comments/feedback. - - ## Context - - Branch: ${{ steps.context.outputs.branch_name }} - - Issue Number: ${{ steps.context.outputs.issue_number }} - - Changed Files: ${{ steps.context.outputs.changed_files }} - - Event: ${{ github.event_name }} - - PR Number: ${{ github.event.pull_request.number || 'N/A' }} - - ## Your Task - - 1. **Read the linked issue** (if available): - - Issue title: Read /tmp/issue_title.txt - - Issue body: Read /tmp/issue_body.txt - - Issue comments: Read /tmp/issue_comments.txt - - 2. **Read PR/branch comments** (if available): - - PR comments: Read /tmp/pr_comments.txt - - Review comments: Read /tmp/review_comments.txt - - 3. **Analyze the code changes**: - - Review git diff for the changed files - - Understand what was implemented - - 4. **Compare requirements vs implementation**: - - Does the code address all requirements from the issue? - - Are there any comments that indicate needed changes? - - Are there any scope creep issues (implementation beyond requirements)? - - Are there missing requirements? - - 5. **Make your decision**: - - PASS: All requirements addressed, comments resolved - - FAIL: Requirements not met, unresolved feedback, or scope issues - - ## IMPORTANT RULES - - - **DO NOT edit any files** - you are a reviewer only - - **Prioritize comments** over original issue if they indicate requirement changes - - If there's no linked issue, focus on PR comments and code quality - - If no issue AND no comments, provide general assessment and PASS - - ## Output Format - - Provide a detailed analysis, then output your final decision as a JSON code block. - - Your response MUST end with a JSON code block in this exact format: - - ```json - { - "passed": true/false, - "confidence": 0.0-1.0, - "requirements_met": ["list of met requirements"], - "requirements_missing": ["list of missing requirements"], - "unresolved_comments": ["list of unresolved feedback"], - "scope_issues": ["list of scope issues"], - "summary": "Brief summary of your decision", - "recommended_actions": ["list of actions if failed"] - } - ``` - - - name: Extract review result - id: extract - run: | - # Read execution file and extract JSON from Claude's response - EXEC_FILE="${{ steps.review.outputs.execution_file }}" - CONCLUSION="${{ steps.review.outputs.conclusion }}" - - # Default output based on conclusion - if [ "$CONCLUSION" = "success" ]; then - DEFAULT_OUTPUT='{"passed":true,"confidence":0.8,"summary":"Review completed successfully"}' - else - DEFAULT_OUTPUT='{"passed":false,"confidence":0.5,"summary":"Review completed with issues"}' - fi - - if [ -f "$EXEC_FILE" ]; then - # Extract the last assistant message text and find JSON - LAST_TEXT=$(jq -r '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text] | last // ""' "$EXEC_FILE" 2>/dev/null) - - # Try to extract JSON block - OUTPUT=$(echo "$LAST_TEXT" | sed -n '/```json/,/```/{/```json/d;/```/d;p;}' | tr -d '\r') - - # Validate and use or fallback - if echo "$OUTPUT" | jq . >/dev/null 2>&1 && [ -n "$OUTPUT" ]; then - echo "$OUTPUT" > /tmp/review_output.json - else - echo "$DEFAULT_OUTPUT" > /tmp/review_output.json - fi - else - echo "$DEFAULT_OUTPUT" > /tmp/review_output.json - fi - - # Set simple outputs - PASSED=$(jq -r '.passed // false' /tmp/review_output.json) - SUMMARY=$(jq -r '.summary // "No summary"' /tmp/review_output.json | head -c 200) - echo "passed=$PASSED" >> $GITHUB_OUTPUT - echo "summary=$SUMMARY" >> $GITHUB_OUTPUT - - - name: Post review comment - if: github.event_name == 'pull_request' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - OUTPUT=$(cat /tmp/review_output.json) - PASSED=$(echo "$OUTPUT" | jq -r '.passed // false') - CONFIDENCE=$(echo "$OUTPUT" | jq -r '.confidence // 0') - SUMMARY=$(echo "$OUTPUT" | jq -r '.summary // "No summary available"') - - if [ "$PASSED" = "true" ]; then - STATUS_ICON=":white_check_mark:" - STATUS_TEXT="PASSED" - else - STATUS_ICON=":x:" - STATUS_TEXT="FAILED" - fi - - # Build comment body - COMMENT_BODY="## Requirements Review ${STATUS_ICON} ${STATUS_TEXT} - - **Confidence:** ${CONFIDENCE} - - ### Summary - ${SUMMARY} - " - - # Add requirements met - REQ_MET=$(echo "$OUTPUT" | jq -r '.requirements_met // [] | .[]' 2>/dev/null) - if [ -n "$REQ_MET" ]; then - COMMENT_BODY="${COMMENT_BODY} - ### Requirements Met - $(echo "$REQ_MET" | while read -r req; do echo "- :white_check_mark: $req"; done) - " - fi - - # Add missing requirements - REQ_MISSING=$(echo "$OUTPUT" | jq -r '.requirements_missing // [] | .[]' 2>/dev/null) - if [ -n "$REQ_MISSING" ]; then - COMMENT_BODY="${COMMENT_BODY} - ### Requirements Missing - $(echo "$REQ_MISSING" | while read -r req; do echo "- :warning: $req"; done) - " - fi - - # Add unresolved comments - UNRESOLVED=$(echo "$OUTPUT" | jq -r '.unresolved_comments // [] | .[]' 2>/dev/null) - if [ -n "$UNRESOLVED" ]; then - COMMENT_BODY="${COMMENT_BODY} - ### Unresolved Comments - $(echo "$UNRESOLVED" | while read -r comment; do echo "- :speech_balloon: $comment"; done) - " - fi - - # Add recommended actions - ACTIONS=$(echo "$OUTPUT" | jq -r '.recommended_actions // [] | .[]' 2>/dev/null) - if [ -n "$ACTIONS" ]; then - COMMENT_BODY="${COMMENT_BODY} - ### Recommended Actions - $(echo "$ACTIONS" | while read -r action; do echo "1. $action"; done) - " - fi - - COMMENT_BODY="${COMMENT_BODY} - - --- - *Automated Requirements Review by Claude Code*" - - # Post or update comment - gh pr comment ${{ github.event.pull_request.number }} --body "$COMMENT_BODY" - - - name: Set commit status - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PASSED="${{ steps.extract.outputs.passed }}" - SUMMARY="${{ steps.extract.outputs.summary }}" - - if [ "$PASSED" = "true" ]; then - STATE="success" - DESCRIPTION="Requirements review passed" - else - STATE="failure" - DESCRIPTION="Requirements review failed: $SUMMARY" - fi - - gh api repos/${{ github.repository }}/statuses/${{ github.sha }} \ - -f state="$STATE" \ - -f target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ - -f description="$DESCRIPTION" \ - -f context="Requirements Review" - - - name: Fail if review failed - run: | - PASSED="${{ steps.extract.outputs.passed }}" - - if [ "$PASSED" != "true" ]; then - echo "::error::Requirements review failed. See PR comment for details." - exit 1 - fi - - rules-review: - needs: [check-prerequisites, requirements-review] - if: needs.check-prerequisites.outputs.can_proceed == 'true' - runs-on: ubuntu-latest - outputs: - review_passed: ${{ steps.review.outputs.passed }} - structured_output: ${{ steps.review.outputs.structured_output }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Extract branch context - id: context - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - BRANCH_NAME="${{ github.head_ref || github.ref_name }}" - echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT - - # Extract issue number from branch - ISSUE_NUMBER="" - if [[ $BRANCH_NAME =~ ^(feature|fix|bugfix|hotfix)/([0-9]+) ]]; then - ISSUE_NUMBER="${BASH_REMATCH[2]}" - elif [[ $BRANCH_NAME =~ ^([0-9]+)- ]]; then - ISSUE_NUMBER="${BASH_REMATCH[1]}" - elif [[ $BRANCH_NAME =~ issue-([0-9]+) ]]; then - ISSUE_NUMBER="${BASH_REMATCH[1]}" - fi - echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT - - # Get changed files - CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD 2>/dev/null | tr '\n' ' ' || echo "") - echo "changed_files=$CHANGED_FILES" >> $GITHUB_OUTPUT - - # Save changed files to file for Claude - git diff --name-only HEAD~1 HEAD 2>/dev/null > /tmp/changed_files.txt || echo "" > /tmp/changed_files.txt - - - name: Get issue context - id: issue - if: steps.context.outputs.issue_number != '' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - ISSUE_NUM="${{ steps.context.outputs.issue_number }}" - - # Get issue details - gh issue view $ISSUE_NUM --json title,body,comments > /tmp/issue.json 2>/dev/null || echo "{}" > /tmp/issue.json - - echo "has_issue=true" >> $GITHUB_OUTPUT - - - name: Discover rules - id: rules - run: | - # Find all rules files - find .claude/rules -name "*.md" -type f 2>/dev/null > /tmp/rules_files.txt || echo "" > /tmp/rules_files.txt - - RULES_COUNT=$(wc -l < /tmp/rules_files.txt | tr -d ' ') - echo "rules_count=$RULES_COUNT" >> $GITHUB_OUTPUT - - # Also check for project CLAUDE.md rules - if [ -f "CLAUDE.md" ]; then - echo "CLAUDE.md" >> /tmp/rules_files.txt - fi - - - name: Rules Review with Claude - id: review - uses: anthropics/claude-code-base-action@beta - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - allowed_tools: "View,GlobTool,Grep,Bash(git diff:*),Bash(git log:*),Bash(cat:*),Bash(find:*)" - max_turns: 20 - prompt: | - # Rules Review Agent - - You are a rules compliance review agent. Your job is to verify that code changes comply with the project's rules defined in .claude/rules/*.md files. - - ## Context - - Branch: ${{ steps.context.outputs.branch_name }} - - Issue Number: ${{ steps.context.outputs.issue_number }} - - Changed Files: ${{ steps.context.outputs.changed_files }} - - Rules Count: ${{ steps.rules.outputs.rules_count }} - - ## Your Task - - 1. **Read the changed files list**: - - Read /tmp/changed_files.txt - - 2. **Discover and read all rules**: - - Read /tmp/rules_files.txt for list of rule files - - Read each rule file in .claude/rules/ - - Each rule has frontmatter with path patterns (globs) that define which files it applies to - - 3. **Group changed files by matching rules**: - - For each changed file, find which rules apply based on path patterns in frontmatter - - A file may match multiple rules - - Track files that don't match any rule - - 4. **For each rule-file group, evaluate compliance**: - - Read the rule's requirements - - Check if the changed files comply with those requirements - - Identify any violations - - 5. **Check for needed rule updates**: - - Read /tmp/issue.json for linked issue context - - Based on issue requirements and comments, identify if any rules need updating - - This is a BLOCKING ERROR if rule updates seem clearly needed - - 6. **Make your decision**: - - PASS: All files comply with their matching rules - - FAIL: Rule violations found, or rule updates needed - - ## IMPORTANT RULES - - - **DO NOT edit any files** - you are a reviewer only - - **Advise on rule fixes** - suggest what should be changed to comply - - **Advise on rule updates** - if rules seem outdated based on issue/comments - - **Blocking errors**: Rule updates needed = automatic FAIL - - ## Output Format - - Provide detailed rule-by-rule analysis, then output your final decision as a JSON code block. - - Your response MUST end with a JSON code block in this exact format: - - ```json - { - "passed": true/false, - "confidence": 0.0-1.0, - "rule_evaluations": [ - { - "rule_file": "path/to/rule.md", - "matched_files": ["file1.ts", "file2.ts"], - "compliant": true/false, - "violations": ["violation description"], - "suggestions": ["suggestion"] - } - ], - "files_without_rules": ["files not covered by rules"], - "rule_updates_needed": [ - { - "rule_file": "path/to/rule.md", - "reason": "why update needed", - "suggested_update": "what to change" - } - ], - "blocking_errors": ["critical errors"], - "summary": "Brief summary of your decision" - } - ``` - - - name: Extract review result - id: extract - run: | - EXEC_FILE="${{ steps.review.outputs.execution_file }}" - CONCLUSION="${{ steps.review.outputs.conclusion }}" - - if [ "$CONCLUSION" = "success" ]; then - DEFAULT_OUTPUT='{"passed":true,"confidence":0.8,"summary":"Review completed successfully"}' - else - DEFAULT_OUTPUT='{"passed":false,"confidence":0.5,"summary":"Review completed with issues"}' - fi - - if [ -f "$EXEC_FILE" ]; then - LAST_TEXT=$(jq -r '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text] | last // ""' "$EXEC_FILE" 2>/dev/null) - OUTPUT=$(echo "$LAST_TEXT" | sed -n '/```json/,/```/{/```json/d;/```/d;p;}' | tr -d '\r') - if echo "$OUTPUT" | jq . >/dev/null 2>&1 && [ -n "$OUTPUT" ]; then - echo "$OUTPUT" > /tmp/review_output.json - else - echo "$DEFAULT_OUTPUT" > /tmp/review_output.json - fi - else - echo "$DEFAULT_OUTPUT" > /tmp/review_output.json - fi - - PASSED=$(jq -r '.passed // false' /tmp/review_output.json) - SUMMARY=$(jq -r '.summary // "No summary"' /tmp/review_output.json | head -c 200) - echo "passed=$PASSED" >> $GITHUB_OUTPUT - echo "summary=$SUMMARY" >> $GITHUB_OUTPUT - - - name: Post review comment - if: github.event_name == 'pull_request' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - OUTPUT=$(cat /tmp/review_output.json) - PASSED=$(echo "$OUTPUT" | jq -r '.passed // false') - CONFIDENCE=$(echo "$OUTPUT" | jq -r '.confidence // 0') - SUMMARY=$(echo "$OUTPUT" | jq -r '.summary // "No summary available"') - - if [ "$PASSED" = "true" ]; then - STATUS_ICON=":white_check_mark:" - STATUS_TEXT="PASSED" - else - STATUS_ICON=":x:" - STATUS_TEXT="FAILED" - fi - - COMMENT_BODY="## Rules Review ${STATUS_ICON} ${STATUS_TEXT} - - **Confidence:** ${CONFIDENCE} - - ### Summary - ${SUMMARY} - " - - # Add rule evaluations - RULE_EVALS=$(echo "$OUTPUT" | jq -c '.rule_evaluations // []') - if [ "$RULE_EVALS" != "[]" ]; then - COMMENT_BODY="${COMMENT_BODY} - ### Rule Evaluations - " - echo "$OUTPUT" | jq -r '.rule_evaluations[]? | "#### \(.rule_file)\n- **Files:** \(.matched_files | join(\", \"))\n- **Compliant:** \(.compliant)\n\(.violations | if length > 0 then \"- **Violations:**\n\" + (map(\" - \" + .) | join(\"\n\")) else \"\" end)\n"' 2>/dev/null | while IFS= read -r line; do - COMMENT_BODY="${COMMENT_BODY}${line} - " - done - fi - - # Add blocking errors - BLOCKING=$(echo "$OUTPUT" | jq -r '.blocking_errors // [] | .[]' 2>/dev/null) - if [ -n "$BLOCKING" ]; then - COMMENT_BODY="${COMMENT_BODY} - ### :rotating_light: Blocking Errors - $(echo "$BLOCKING" | while read -r err; do echo "- $err"; done) - " - fi - - # Add rule updates needed - UPDATES=$(echo "$OUTPUT" | jq -c '.rule_updates_needed // []') - if [ "$UPDATES" != "[]" ]; then - COMMENT_BODY="${COMMENT_BODY} - ### Rule Updates Needed - $(echo "$OUTPUT" | jq -r '.rule_updates_needed[]? | "- **\(.rule_file)**: \(.reason)"' 2>/dev/null) - " - fi - - COMMENT_BODY="${COMMENT_BODY} - - --- - *Automated Rules Review by Claude Code*" - - gh pr comment ${{ github.event.pull_request.number }} --body "$COMMENT_BODY" - - - name: Set commit status - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PASSED="${{ steps.extract.outputs.passed }}" - SUMMARY="${{ steps.extract.outputs.summary }}" - - if [ "$PASSED" = "true" ]; then - STATE="success" - DESCRIPTION="Rules review passed" - else - STATE="failure" - DESCRIPTION="Rules review failed: $SUMMARY" - fi - - gh api repos/${{ github.repository }}/statuses/${{ github.sha }} \ - -f state="$STATE" \ - -f target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ - -f description="$DESCRIPTION" \ - -f context="Rules Review" - - - name: Fail if review failed - run: | - PASSED="${{ steps.extract.outputs.passed }}" - - if [ "$PASSED" != "true" ]; then - echo "::error::Rules review failed. See PR comment for details." - exit 1 - fi - - project-memory-review: - needs: check-prerequisites - if: needs.check-prerequisites.outputs.can_proceed == 'true' - runs-on: ubuntu-latest - outputs: - review_passed: ${{ steps.review.outputs.passed }} - structured_output: ${{ steps.review.outputs.structured_output }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Check if path filter applies - id: filter - uses: dorny/paths-filter@v3 - with: - filters: | - memory: - - 'CLAUDE.md' - - '**/CLAUDE.md' - - '.claude/**' - - - name: Extract context - if: steps.filter.outputs.memory == 'true' - id: context - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - BRANCH_NAME="${{ github.head_ref || github.ref_name }}" - echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT - - # Extract issue number - ISSUE_NUMBER="" - if [[ $BRANCH_NAME =~ ^(feature|fix|bugfix|hotfix)/([0-9]+) ]]; then - ISSUE_NUMBER="${BASH_REMATCH[2]}" - elif [[ $BRANCH_NAME =~ ^([0-9]+)- ]]; then - ISSUE_NUMBER="${BASH_REMATCH[1]}" - fi - echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT - - # Get all changed files - if [ "${{ github.event_name }}" = "pull_request" ]; then - gh pr view ${{ github.event.pull_request.number }} --json files --jq '.files[].path' > /tmp/all_changed_files.txt - else - git diff --name-only HEAD~1 HEAD > /tmp/all_changed_files.txt 2>/dev/null || echo "" > /tmp/all_changed_files.txt - fi - - # Find touched CLAUDE.md files - grep -E '(^|/)CLAUDE\.md$' /tmp/all_changed_files.txt > /tmp/changed_claude_files.txt || echo "" > /tmp/changed_claude_files.txt - - # Find folders touched by the commit (to check their CLAUDE.md files) - cat /tmp/all_changed_files.txt | while read -r file; do - dir=$(dirname "$file") - while [ "$dir" != "." ]; do - if [ -f "$dir/CLAUDE.md" ]; then - echo "$dir/CLAUDE.md" - fi - dir=$(dirname "$dir") - done - done | sort -u > /tmp/folder_claude_files.txt - - # Combine touched and folder CLAUDE.md files - cat /tmp/changed_claude_files.txt /tmp/folder_claude_files.txt | sort -u > /tmp/relevant_claude_files.txt - - # Always include root CLAUDE.md if it exists - if [ -f "CLAUDE.md" ]; then - echo "CLAUDE.md" >> /tmp/relevant_claude_files.txt - fi - - sort -u /tmp/relevant_claude_files.txt -o /tmp/relevant_claude_files.txt - - - name: Get issue context - if: steps.filter.outputs.memory == 'true' && steps.context.outputs.issue_number != '' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh issue view ${{ steps.context.outputs.issue_number }} --json title,body,comments > /tmp/issue.json 2>/dev/null || echo "{}" > /tmp/issue.json - - - name: Get PR comments - if: steps.filter.outputs.memory == 'true' && github.event_name == 'pull_request' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh pr view ${{ github.event.pull_request.number }} --json comments,reviews > /tmp/pr_comments.json 2>/dev/null || echo "{}" > /tmp/pr_comments.json - - - name: Project Memory Review with Claude - if: steps.filter.outputs.memory == 'true' - id: review - uses: anthropics/claude-code-base-action@beta - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - allowed_tools: "View,GlobTool,Grep,Bash(cat:*),Bash(find:*),Bash(git diff:*),Bash(git log:*)" - max_turns: 20 - prompt: | - # Project Memory Review Agent - - You are a project memory review agent. Your job is to verify that code changes comply with the rules and requirements documented in CLAUDE.md files. - - ## Context - - Branch: ${{ steps.context.outputs.branch_name }} - - Issue: ${{ steps.context.outputs.issue_number }} - - ## Your Task - - 1. **Identify relevant CLAUDE.md files**: - - Read /tmp/relevant_claude_files.txt for list of CLAUDE.md files to check - - Read /tmp/all_changed_files.txt for all files changed in this commit - - 2. **For each CLAUDE.md file**: - - Read the CLAUDE.md content - - Extract rules and requirements from it - - Identify which changed files should comply with these rules - - Check if the changes comply with the documented requirements - - 3. **Check for needed updates**: - - Read /tmp/issue.json for linked issue context - - Read /tmp/pr_comments.json for PR feedback - - Based on issue/comments, identify if CLAUDE.md files need updating - - 4. **Evaluate compliance**: - - For each rule in each CLAUDE.md: - - Is it met by the code changes? - - Are there violations? - - Should the rule be updated based on issue context? - - 5. **Make your decision**: - - PASS: All documented rules/requirements met - - FAIL: Rule violations found or critical updates needed - - ## CLAUDE.md Structure - - CLAUDE.md files typically contain: - - Frontmatter with metadata (folder structure, allowed files, etc.) - - Project description and architecture - - Development rules and guidelines - - Technology-specific requirements - - Best practices to follow - - ## IMPORTANT RULES - - - **DO NOT edit any files** - you are a reviewer only - - Check both root CLAUDE.md and folder-specific ones - - Folder CLAUDE.md rules apply to files in that folder - - Suggest documentation updates if issue context indicates needs - - Be specific about which rules are violated and where - - ## Output Format - - Provide file-by-file analysis, then output your final decision as a JSON code block. - - Your response MUST end with a JSON code block in this exact format: - - ```json - { - "passed": true/false, - "confidence": 0.0-1.0, - "claude_file_evaluations": [ - { - "claude_file": "path/to/CLAUDE.md", - "rules_extracted": ["rule descriptions"], - "rules_met": ["met rules"], - "rules_violated": [ - { - "rule": "rule name", - "violation": "description", - "affected_files": ["file1.ts"] - } - ], - "update_suggestions": ["suggestions"] - } - ], - "documentation_updates_needed": [ - { - "file": "path/to/file", - "reason": "why update needed", - "suggested_change": "what to change" - } - ], - "blocking_violations": ["critical violations"], - "summary": "Brief summary of your decision" - } - ``` - - - name: Extract review result - if: steps.filter.outputs.memory == 'true' - id: extract - run: | - EXEC_FILE="${{ steps.review.outputs.execution_file }}" - CONCLUSION="${{ steps.review.outputs.conclusion }}" - - if [ "$CONCLUSION" = "success" ]; then - DEFAULT_OUTPUT='{"passed":true,"confidence":0.8,"summary":"Review completed successfully"}' - else - DEFAULT_OUTPUT='{"passed":false,"confidence":0.5,"summary":"Review completed with issues"}' - fi - - if [ -f "$EXEC_FILE" ]; then - LAST_TEXT=$(jq -r '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text] | last // ""' "$EXEC_FILE" 2>/dev/null) - OUTPUT=$(echo "$LAST_TEXT" | sed -n '/```json/,/```/{/```json/d;/```/d;p;}' | tr -d '\r') - if echo "$OUTPUT" | jq . >/dev/null 2>&1 && [ -n "$OUTPUT" ]; then - echo "$OUTPUT" > /tmp/review_output.json - else - echo "$DEFAULT_OUTPUT" > /tmp/review_output.json - fi - else - echo "$DEFAULT_OUTPUT" > /tmp/review_output.json - fi - - PASSED=$(jq -r '.passed // false' /tmp/review_output.json) - SUMMARY=$(jq -r '.summary // "No summary"' /tmp/review_output.json | head -c 200) - echo "passed=$PASSED" >> $GITHUB_OUTPUT - echo "summary=$SUMMARY" >> $GITHUB_OUTPUT - - - name: Post review comment - if: steps.filter.outputs.memory == 'true' && github.event_name == 'pull_request' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - OUTPUT=$(cat /tmp/review_output.json) - PASSED=$(echo "$OUTPUT" | jq -r '.passed // false') - CONFIDENCE=$(echo "$OUTPUT" | jq -r '.confidence // 0') - SUMMARY=$(echo "$OUTPUT" | jq -r '.summary // "No summary available"') - - if [ "$PASSED" = "true" ]; then - STATUS_ICON=":white_check_mark:" - STATUS_TEXT="PASSED" - else - STATUS_ICON=":x:" - STATUS_TEXT="FAILED" - fi - - COMMENT_BODY="## Project Memory Review ${STATUS_ICON} ${STATUS_TEXT} - - **Confidence:** ${CONFIDENCE} - - ### Summary - ${SUMMARY} - " - - # Add file evaluations - echo "$OUTPUT" | jq -r '.claude_file_evaluations[]? | "### \(.claude_file)\n\n**Rules Extracted:** \(.rules_extracted | length)\n\n**Rules Met:**\n\(.rules_met | if length > 0 then map(\"- :white_check_mark: \" + .) | join(\"\n\") else \"None specified\" end)\n\n**Violations:**\n\(.rules_violated | if length > 0 then map(\"- :x: **\" + .rule + \"**: \" + .violation + \" (Files: \" + (.affected_files | join(\", \")) + \")\") | join(\"\n\") else \"None\" end)\n"' 2>/dev/null | while IFS= read -r line; do - COMMENT_BODY="${COMMENT_BODY}${line} - " - done - - # Add blocking violations - BLOCKING=$(echo "$OUTPUT" | jq -r '.blocking_violations // [] | .[]' 2>/dev/null) - if [ -n "$BLOCKING" ]; then - COMMENT_BODY="${COMMENT_BODY} - ### :rotating_light: Blocking Violations - $(echo "$BLOCKING" | while read -r v; do echo "- $v"; done) - " - fi - - # Add documentation update suggestions - DOC_UPDATES=$(echo "$OUTPUT" | jq -c '.documentation_updates_needed // []') - if [ "$DOC_UPDATES" != "[]" ]; then - COMMENT_BODY="${COMMENT_BODY} - ### Documentation Updates Suggested - $(echo "$OUTPUT" | jq -r '.documentation_updates_needed[]? | "- **\(.file)**: \(.reason)"' 2>/dev/null) - " - fi - - COMMENT_BODY="${COMMENT_BODY} - - --- - *Automated Project Memory Review by Claude Code*" - - gh pr comment ${{ github.event.pull_request.number }} --body "$COMMENT_BODY" - - - name: Set commit status - if: steps.filter.outputs.memory == 'true' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PASSED="${{ steps.extract.outputs.passed }}" - SUMMARY="${{ steps.extract.outputs.summary }}" - - if [ "$PASSED" = "true" ]; then - STATE="success" - DESCRIPTION="Project memory review passed" - else - STATE="failure" - DESCRIPTION="Project memory violations found" - fi - - gh api repos/${{ github.repository }}/statuses/${{ github.sha }} \ - -f state="$STATE" \ - -f target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ - -f description="$DESCRIPTION" \ - -f context="Project Memory Review" - - - name: Fail if review failed - if: steps.filter.outputs.memory == 'true' - run: | - PASSED="${{ steps.extract.outputs.passed }}" - - if [ "$PASSED" != "true" ]; then - echo "::error::Project memory review failed. See PR comment for details." - exit 1 - fi - - agents-review: - needs: check-prerequisites - if: needs.check-prerequisites.outputs.can_proceed == 'true' - runs-on: ubuntu-latest - outputs: - review_passed: ${{ steps.review.outputs.passed }} - structured_output: ${{ steps.review.outputs.structured_output }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Check if path filter applies - id: filter - uses: dorny/paths-filter@v3 - with: - filters: | - agents: - - '.claude/agents/**' - - 'agents/**' - - '**/agents/*.md' - - - name: Extract context - if: steps.filter.outputs.agents == 'true' - id: context - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - BRANCH_NAME="${{ github.head_ref || github.ref_name }}" - echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT - - # Extract issue number - ISSUE_NUMBER="" - if [[ $BRANCH_NAME =~ ^(feature|fix|bugfix|hotfix)/([0-9]+) ]]; then - ISSUE_NUMBER="${BASH_REMATCH[2]}" - elif [[ $BRANCH_NAME =~ ^([0-9]+)- ]]; then - ISSUE_NUMBER="${BASH_REMATCH[1]}" - fi - echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT - - # Get changed agent files - if [ "${{ github.event_name }}" = "pull_request" ]; then - CHANGED_AGENTS=$(gh pr view ${{ github.event.pull_request.number }} --json files --jq '.files[].path' | grep -E '(agents/.*\.md|AGENT\.md)' || echo "") - else - CHANGED_AGENTS=$(git diff --name-only HEAD~1 HEAD | grep -E '(agents/.*\.md|AGENT\.md)' || echo "") - fi - echo "$CHANGED_AGENTS" > /tmp/changed_agents.txt - echo "changed_agents=$CHANGED_AGENTS" >> $GITHUB_OUTPUT - - - name: Get issue context - if: steps.filter.outputs.agents == 'true' && steps.context.outputs.issue_number != '' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh issue view ${{ steps.context.outputs.issue_number }} --json title,body,comments > /tmp/issue.json 2>/dev/null || echo "{}" > /tmp/issue.json - - - name: Agents Review with Claude - if: steps.filter.outputs.agents == 'true' - id: review - uses: anthropics/claude-code-base-action@beta - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - allowed_tools: "View,GlobTool,Grep,Bash(cat:*),Bash(find:*)" - max_turns: 15 - prompt: | - # Agents Review Agent - - You are an agents review specialist focused on context optimization. Your job is to review .claude/agents/*.md files for quality, completeness, and context efficiency. - - ## Context - - Branch: ${{ steps.context.outputs.branch_name }} - - Issue: ${{ steps.context.outputs.issue_number }} - - Changed Agents: Read /tmp/changed_agents.txt - - ## Agent Quality Criteria - - 1. **Frontmatter Requirements**: - - `description`: Clear, concise purpose (1-2 sentences) - - `capabilities`: Array of specific capabilities - - Optional: `color`, `model`, `tools` - - 2. **Content Structure**: - - Clear heading with agent name - - Detailed description of role and expertise - - When to use (triggering conditions) - - What the agent does (step-by-step) - - What it doesn't do (boundaries) - - 3. **Context Optimization**: - - Is the description too verbose? (wastes context tokens) - - Is it too vague? (causes confusion) - - Are capabilities specific and actionable? - - Does it avoid duplicating system knowledge? - - Does it focus on domain-specific guidance? - - 4. **Integration Quality**: - - Does the when-to-use section help Claude decide correctly? - - Are there clear boundaries to prevent scope creep? - - Is the agent focused on ONE specialized domain? - - ## Your Task - - 1. Read each changed agent file - 2. Evaluate against the criteria above - 3. Check if issue context suggests any needed updates - 4. Identify context optimization opportunities - 5. Provide specific, actionable suggestions - - ## IMPORTANT RULES - - - **DO NOT edit any files** - you are a reviewer only - - Focus on context optimization (smaller = better if equally clear) - - Prefer specific examples over abstract descriptions - - Flag agents that try to do too much (should be split) - - ## Output Format - - Provide agent-by-agent analysis, then output your final decision as a JSON code block. - - Your response MUST end with a JSON code block in this exact format: - - ```json - { - "passed": true/false, - "confidence": 0.0-1.0, - "agent_evaluations": [ - { - "agent_file": "path/to/agent.md", - "frontmatter_valid": true/false, - "context_score": 1-10, - "clarity_score": 1-10, - "focus_score": 1-10, - "issues": ["issue descriptions"], - "suggestions": ["suggestions"] - } - ], - "optimization_opportunities": ["opportunities for context optimization"], - "summary": "Brief summary of your decision" - } - ``` - - - name: Extract review result - if: steps.filter.outputs.agents == 'true' - id: extract - run: | - EXEC_FILE="${{ steps.review.outputs.execution_file }}" - CONCLUSION="${{ steps.review.outputs.conclusion }}" - - if [ "$CONCLUSION" = "success" ]; then - DEFAULT_OUTPUT='{"passed":true,"confidence":0.8,"summary":"Review completed successfully"}' - else - DEFAULT_OUTPUT='{"passed":false,"confidence":0.5,"summary":"Review completed with issues"}' - fi - - if [ -f "$EXEC_FILE" ]; then - LAST_TEXT=$(jq -r '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text] | last // ""' "$EXEC_FILE" 2>/dev/null) - OUTPUT=$(echo "$LAST_TEXT" | sed -n '/```json/,/```/{/```json/d;/```/d;p;}' | tr -d '\r') - if echo "$OUTPUT" | jq . >/dev/null 2>&1 && [ -n "$OUTPUT" ]; then - echo "$OUTPUT" > /tmp/review_output.json - else - echo "$DEFAULT_OUTPUT" > /tmp/review_output.json - fi - else - echo "$DEFAULT_OUTPUT" > /tmp/review_output.json - fi - - PASSED=$(jq -r '.passed // false' /tmp/review_output.json) - SUMMARY=$(jq -r '.summary // "No summary"' /tmp/review_output.json | head -c 200) - echo "passed=$PASSED" >> $GITHUB_OUTPUT - echo "summary=$SUMMARY" >> $GITHUB_OUTPUT - - - name: Post review comment - if: steps.filter.outputs.agents == 'true' && github.event_name == 'pull_request' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - OUTPUT=$(cat /tmp/review_output.json) - PASSED=$(echo "$OUTPUT" | jq -r '.passed // false') - CONFIDENCE=$(echo "$OUTPUT" | jq -r '.confidence // 0') - SUMMARY=$(echo "$OUTPUT" | jq -r '.summary // "No summary available"') - - if [ "$PASSED" = "true" ]; then - STATUS_ICON=":white_check_mark:" - STATUS_TEXT="PASSED" - else - STATUS_ICON=":warning:" - STATUS_TEXT="NEEDS IMPROVEMENT" - fi - - COMMENT_BODY="## Agents Review ${STATUS_ICON} ${STATUS_TEXT} - - **Confidence:** ${CONFIDENCE} - - ### Summary - ${SUMMARY} - " - - # Add agent evaluations - echo "$OUTPUT" | jq -r '.agent_evaluations[]? | "### \(.agent_file)\n- **Context Score:** \(.context_score)/10\n- **Clarity Score:** \(.clarity_score)/10\n- **Focus Score:** \(.focus_score)/10\n- **Frontmatter Valid:** \(.frontmatter_valid)\n\n**Issues:**\n\(.issues | if length > 0 then map(\"- \" + .) | join(\"\n\") else \"None\" end)\n\n**Suggestions:**\n\(.suggestions | if length > 0 then map(\"- \" + .) | join(\"\n\") else \"None\" end)\n"' 2>/dev/null | while IFS= read -r line; do - COMMENT_BODY="${COMMENT_BODY}${line} - " - done - - # Add optimization opportunities - OPTS=$(echo "$OUTPUT" | jq -r '.optimization_opportunities // [] | .[]' 2>/dev/null) - if [ -n "$OPTS" ]; then - COMMENT_BODY="${COMMENT_BODY} - ### Context Optimization Opportunities - $(echo "$OPTS" | while read -r opt; do echo "- :zap: $opt"; done) - " - fi - - COMMENT_BODY="${COMMENT_BODY} - - --- - *Automated Agents Review by Claude Code*" - - gh pr comment ${{ github.event.pull_request.number }} --body "$COMMENT_BODY" - - - name: Set commit status - if: steps.filter.outputs.agents == 'true' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PASSED="${{ steps.extract.outputs.passed }}" - SUMMARY="${{ steps.extract.outputs.summary }}" - - if [ "$PASSED" = "true" ]; then - STATE="success" - DESCRIPTION="Agents review passed" - else - STATE="failure" - DESCRIPTION="Agents need improvement" - fi - - gh api repos/${{ github.repository }}/statuses/${{ github.sha }} \ - -f state="$STATE" \ - -f target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ - -f description="$DESCRIPTION" \ - -f context="Agents Review" - - - name: Fail if review failed - if: steps.filter.outputs.agents == 'true' - run: | - PASSED="${{ steps.extract.outputs.passed }}" - - if [ "$PASSED" != "true" ]; then - echo "::warning::Agents review identified improvements needed. See PR comment." - exit 1 - fi - - skills-review: - needs: check-prerequisites - if: needs.check-prerequisites.outputs.can_proceed == 'true' - runs-on: ubuntu-latest - outputs: - review_passed: ${{ steps.review.outputs.passed }} - structured_output: ${{ steps.review.outputs.structured_output }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Check if path filter applies - id: filter - uses: dorny/paths-filter@v3 - with: - filters: | - skills: - - '.claude/skills/**' - - 'skills/**' - - '**/SKILL.md' - - - name: Extract context - if: steps.filter.outputs.skills == 'true' - id: context - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - BRANCH_NAME="${{ github.head_ref || github.ref_name }}" - echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT - - # Extract issue number - ISSUE_NUMBER="" - if [[ $BRANCH_NAME =~ ^(feature|fix|bugfix|hotfix)/([0-9]+) ]]; then - ISSUE_NUMBER="${BASH_REMATCH[2]}" - elif [[ $BRANCH_NAME =~ ^([0-9]+)- ]]; then - ISSUE_NUMBER="${BASH_REMATCH[1]}" - fi - echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT - - # Get changed skill files - if [ "${{ github.event_name }}" = "pull_request" ]; then - CHANGED_SKILLS=$(gh pr view ${{ github.event.pull_request.number }} --json files --jq '.files[].path' | grep -E '(skills/.*\.md|SKILL\.md)' || echo "") - else - CHANGED_SKILLS=$(git diff --name-only HEAD~1 HEAD | grep -E '(skills/.*\.md|SKILL\.md)' || echo "") - fi - echo "$CHANGED_SKILLS" > /tmp/changed_skills.txt - echo "changed_skills=$CHANGED_SKILLS" >> $GITHUB_OUTPUT - - - name: Get issue context - if: steps.filter.outputs.skills == 'true' && steps.context.outputs.issue_number != '' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh issue view ${{ steps.context.outputs.issue_number }} --json title,body,comments > /tmp/issue.json 2>/dev/null || echo "{}" > /tmp/issue.json - - - name: Skills Review with Claude - if: steps.filter.outputs.skills == 'true' - id: review - uses: anthropics/claude-code-base-action@beta - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - allowed_tools: "View,GlobTool,Grep,Bash(cat:*),Bash(find:*)" - max_turns: 15 - prompt: | - # Skills Review Agent - - You are a skills review specialist focused on context optimization. Your job is to review .claude/skills/**/SKILL.md files for quality, structure, and context efficiency. - - ## Context - - Branch: ${{ steps.context.outputs.branch_name }} - - Issue: ${{ steps.context.outputs.issue_number }} - - Changed Skills: Read /tmp/changed_skills.txt - - ## Skill Quality Criteria - - 1. **Frontmatter Requirements**: - - `description`: Clear, concise skill purpose (triggers skill invocation) - - `capabilities`: Array of specific things the skill enables - - Optional: `required_skills`, `related_skills` - - 2. **Content Structure**: - - Clear title heading - - Overview section explaining the skill - - Detailed guidance organized with sub-headings - - Examples with code blocks where appropriate - - Links to official documentation (prefer .md URLs) - - 3. **Context Optimization**: - - Is the skill focused on ONE topic? - - Does it avoid duplicating general knowledge? - - Is content dense with actionable guidance? - - Are examples minimal but representative? - - Could any sections be split to separate skills? - - 4. **Progressive Disclosure**: - - Is the most important info at the top? - - Are details progressively revealed? - - Can Claude find key info quickly? - - 5. **Documentation Links**: - - Are there links to official docs? - - Are .md file URLs preferred over HTML? - - Are links current and accessible? - - ## Your Task - - 1. Read each changed skill file - 2. Evaluate against the criteria above - 3. Check if issue context suggests needed updates - 4. Identify context optimization opportunities - 5. Provide specific, actionable suggestions - - ## IMPORTANT RULES - - - **DO NOT edit any files** - you are a reviewer only - - Focus on context efficiency (every token should add value) - - Skills should be self-contained but not redundant - - Flag skills that are too broad or try to cover too much - - ## Output Format - - Provide skill-by-skill analysis, then output your final decision as a JSON code block. - - Your response MUST end with a JSON code block in this exact format: - - ```json - { - "passed": true/false, - "confidence": 0.0-1.0, - "skill_evaluations": [ - { - "skill_file": "path/to/SKILL.md", - "frontmatter_valid": true/false, - "context_efficiency": 1-10, - "structure_score": 1-10, - "documentation_links": true/false, - "progressive_disclosure": true/false, - "issues": ["issue descriptions"], - "suggestions": ["suggestions"] - } - ], - "optimization_opportunities": ["opportunities"], - "split_recommendations": [ - { - "skill_file": "path/to/SKILL.md", - "reason": "why split needed", - "suggested_split": ["new-skill-1", "new-skill-2"] - } - ], - "summary": "Brief summary of your decision" - } - ``` - - - name: Extract review result - if: steps.filter.outputs.skills == 'true' - id: extract - run: | - EXEC_FILE="${{ steps.review.outputs.execution_file }}" - CONCLUSION="${{ steps.review.outputs.conclusion }}" - - if [ "$CONCLUSION" = "success" ]; then - DEFAULT_OUTPUT='{"passed":true,"confidence":0.8,"summary":"Review completed successfully"}' - else - DEFAULT_OUTPUT='{"passed":false,"confidence":0.5,"summary":"Review completed with issues"}' - fi - - if [ -f "$EXEC_FILE" ]; then - LAST_TEXT=$(jq -r '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text] | last // ""' "$EXEC_FILE" 2>/dev/null) - OUTPUT=$(echo "$LAST_TEXT" | sed -n '/```json/,/```/{/```json/d;/```/d;p;}' | tr -d '\r') - if echo "$OUTPUT" | jq . >/dev/null 2>&1 && [ -n "$OUTPUT" ]; then - echo "$OUTPUT" > /tmp/review_output.json - else - echo "$DEFAULT_OUTPUT" > /tmp/review_output.json - fi - else - echo "$DEFAULT_OUTPUT" > /tmp/review_output.json - fi - - PASSED=$(jq -r '.passed // false' /tmp/review_output.json) - SUMMARY=$(jq -r '.summary // "No summary"' /tmp/review_output.json | head -c 200) - echo "passed=$PASSED" >> $GITHUB_OUTPUT - echo "summary=$SUMMARY" >> $GITHUB_OUTPUT - - - name: Post review comment - if: steps.filter.outputs.skills == 'true' && github.event_name == 'pull_request' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - OUTPUT=$(cat /tmp/review_output.json) - PASSED=$(echo "$OUTPUT" | jq -r '.passed // false') - CONFIDENCE=$(echo "$OUTPUT" | jq -r '.confidence // 0') - SUMMARY=$(echo "$OUTPUT" | jq -r '.summary // "No summary available"') - - if [ "$PASSED" = "true" ]; then - STATUS_ICON=":white_check_mark:" - STATUS_TEXT="PASSED" - else - STATUS_ICON=":warning:" - STATUS_TEXT="NEEDS IMPROVEMENT" - fi - - COMMENT_BODY="## Skills Review ${STATUS_ICON} ${STATUS_TEXT} - - **Confidence:** ${CONFIDENCE} - - ### Summary - ${SUMMARY} - " - - # Add skill evaluations - echo "$OUTPUT" | jq -r '.skill_evaluations[]? | "### \(.skill_file)\n- **Context Efficiency:** \(.context_efficiency)/10\n- **Structure Score:** \(.structure_score)/10\n- **Has Doc Links:** \(.documentation_links)\n- **Progressive Disclosure:** \(.progressive_disclosure)\n\n**Issues:**\n\(.issues | if length > 0 then map(\"- \" + .) | join(\"\n\") else \"None\" end)\n\n**Suggestions:**\n\(.suggestions | if length > 0 then map(\"- \" + .) | join(\"\n\") else \"None\" end)\n"' 2>/dev/null | while IFS= read -r line; do - COMMENT_BODY="${COMMENT_BODY}${line} - " - done - - # Add optimization opportunities - OPTS=$(echo "$OUTPUT" | jq -r '.optimization_opportunities // [] | .[]' 2>/dev/null) - if [ -n "$OPTS" ]; then - COMMENT_BODY="${COMMENT_BODY} - ### Context Optimization Opportunities - $(echo "$OPTS" | while read -r opt; do echo "- :zap: $opt"; done) - " - fi - - # Add split recommendations - SPLITS=$(echo "$OUTPUT" | jq -c '.split_recommendations // []') - if [ "$SPLITS" != "[]" ]; then - COMMENT_BODY="${COMMENT_BODY} - ### Skill Split Recommendations - $(echo "$OUTPUT" | jq -r '.split_recommendations[]? | "- **\(.skill_file)**: \(.reason)\n - Suggested: \(.suggested_split | join(\", \"))"' 2>/dev/null) - " - fi - - COMMENT_BODY="${COMMENT_BODY} - - --- - *Automated Skills Review by Claude Code*" - - gh pr comment ${{ github.event.pull_request.number }} --body "$COMMENT_BODY" - - - name: Set commit status - if: steps.filter.outputs.skills == 'true' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PASSED="${{ steps.extract.outputs.passed }}" - SUMMARY="${{ steps.extract.outputs.summary }}" - - if [ "$PASSED" = "true" ]; then - STATE="success" - DESCRIPTION="Skills review passed" - else - STATE="failure" - DESCRIPTION="Skills need improvement" - fi - - gh api repos/${{ github.repository }}/statuses/${{ github.sha }} \ - -f state="$STATE" \ - -f target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ - -f description="$DESCRIPTION" \ - -f context="Skills Review" - - - name: Fail if review failed - if: steps.filter.outputs.skills == 'true' - run: | - PASSED="${{ steps.extract.outputs.passed }}" - - if [ "$PASSED" != "true" ]; then - echo "::warning::Skills review identified improvements needed. See PR comment." - exit 1 - fi - - playwright-ui-review: - needs: check-prerequisites - if: needs.check-prerequisites.outputs.can_proceed == 'true' - runs-on: ubuntu-latest - outputs: - review_passed: ${{ steps.review.outputs.passed }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Check if path filter applies - id: filter - uses: dorny/paths-filter@v3 - with: - filters: | - ui: - - 'apps/**' - - 'packages/ui/**' - - '**/*.tsx' - - '**/*.css' - - '**/tailwind.config.*' - - - name: Download screenshots - if: steps.filter.outputs.ui == 'true' - uses: actions/download-artifact@v4 - with: - name: playwright-screenshots-${{ github.sha }} - path: .claude/screenshots/ - continue-on-error: true - - - name: Extract context - if: steps.filter.outputs.ui == 'true' - id: context - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - BRANCH_NAME="${{ github.head_ref || github.ref_name }}" - echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT - - # Get changed UI files - if [ "${{ github.event_name }}" = "pull_request" ]; then - CHANGED_FILES=$(gh pr view ${{ github.event.pull_request.number }} --json files --jq '.files[].path' | grep -E '\.(tsx|css|scss)$' || echo "") - else - CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD | grep -E '\.(tsx|css|scss)$' || echo "") - fi - echo "$CHANGED_FILES" > /tmp/changed_ui_files.txt - - # Check for screenshots - SCREENSHOT_COUNT=$(ls -1 .claude/screenshots/*.png 2>/dev/null | wc -l) - echo "screenshot_count=$SCREENSHOT_COUNT" >> $GITHUB_OUTPUT - - - name: Gather UI rules - if: steps.filter.outputs.ui == 'true' - id: rules - run: | - # Find UI-related rules - mkdir -p /tmp/ui-rules - - # Look for ui-prefixed skills - find .claude/skills -name "ui-*" -type d 2>/dev/null | while read -r dir; do - if [ -f "$dir/SKILL.md" ]; then - cp "$dir/SKILL.md" "/tmp/ui-rules/$(basename $dir).md" - fi - done - - # Look for UI rules in .claude/rules - find .claude/rules -name "ui-*" -o -name "*-ui-*" 2>/dev/null | while read -r file; do - cp "$file" "/tmp/ui-rules/" 2>/dev/null || true - done - - # Count rules found - RULES_COUNT=$(ls -1 /tmp/ui-rules/*.md 2>/dev/null | wc -l) - echo "rules_count=$RULES_COUNT" >> $GITHUB_OUTPUT - - - name: UI Review with Claude - if: steps.filter.outputs.ui == 'true' - id: review - uses: anthropics/claude-code-base-action@beta - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - allowed_tools: "View,GlobTool,Grep,Bash(cat:*),Bash(find:*),Bash(ls:*)" - max_turns: 25 - model: claude-sonnet-4-5-20250929 - prompt: | - # Playwright UI Review Agent - - You are a UI review specialist. Your job is to review UI screenshots from Playwright tests and evaluate them against UI rules and modern best practices. - - ## Context - - Branch: ${{ steps.context.outputs.branch_name }} - - Screenshot Count: ${{ steps.context.outputs.screenshot_count }} - - UI Rules Count: ${{ steps.rules.outputs.rules_count }} - - ## Your Task - - 1. **Review screenshots** (in .claude/screenshots/): - - Read the screenshot manifest: .claude/screenshot-manifest.txt - - View each screenshot file - - Note the file path for context on what UI element it shows - - 2. **Check against UI rules** (in /tmp/ui-rules/): - - Read each UI rule/skill file - - Evaluate screenshots against documented standards - - 3. **Evaluate against modern UI best practices**: - - **Visual Design:** - - Clear visual hierarchy - - Consistent spacing (8px grid) - - Accessible color contrast (WCAG 2.1 AA: 4.5:1 minimum) - - Readable typography - - Dark/light mode support if applicable - - **Layout & Responsiveness:** - - Mobile-first approach - - Proper responsive breakpoints - - No layout shifts or overflow issues - - **Accessibility:** - - Visible focus indicators - - Proper touch targets (44x44px minimum) - - Semantic structure visible in UI - - **User Experience:** - - Clear call-to-action buttons - - Logical flow and navigation - - Loading and error states handled - - Empty states designed - - **Modern Elegance:** - - Clean, uncluttered design - - Consistent component styling - - Appropriate use of whitespace - - Professional and polished appearance - - 4. **Categorize issues**: - - **Critical**: Broken layout, missing content, accessibility violations - - **Major**: Poor UX, inconsistent styling, confusing navigation - - **Minor**: Small visual tweaks, optimization opportunities - - 5. **Make your decision**: - - PASS: No critical issues, acceptable visual quality - - FAIL: Critical issues found - - ## IMPORTANT RULES - - - **DO NOT edit any files** - you are a reviewer only - - If no screenshots available, note this and skip visual review - - Be specific about which screenshot shows each issue - - Suggest concrete improvements - - ## Output Format - - Provide screenshot-by-screenshot analysis, then output your final decision as a JSON code block. - - Your response MUST end with a JSON code block in this exact format: - - ```json - { - "passed": true/false, - "confidence": 0.0-1.0, - "tests_passed": true/false, - "screenshot_reviews": [ - { - "screenshot_path": "path/to/screenshot.png", - "description": "what this screenshot shows", - "critical_issues": ["critical issues"], - "major_issues": ["major issues"], - "minor_issues": ["minor issues"], - "positive_aspects": ["good things"] - } - ], - "rule_compliance": [ - { - "rule": "rule name", - "compliant": true/false, - "violations": ["violations"] - } - ], - "best_practices_evaluation": { - "visual_design": 1-10, - "layout_responsiveness": 1-10, - "accessibility": 1-10, - "user_experience": 1-10, - "modern_elegance": 1-10 - }, - "total_critical": 0, - "total_major": 0, - "total_minor": 0, - "top_recommendations": ["recommendations"], - "summary": "Brief summary of your decision" - } - ``` - - - name: Extract review result - if: steps.filter.outputs.ui == 'true' - id: extract - run: | - EXEC_FILE="${{ steps.review.outputs.execution_file }}" - CONCLUSION="${{ steps.review.outputs.conclusion }}" - - if [ "$CONCLUSION" = "success" ]; then - DEFAULT_OUTPUT='{"passed":true,"confidence":0.8,"tests_passed":true,"summary":"Review completed successfully"}' - else - DEFAULT_OUTPUT='{"passed":false,"confidence":0.5,"tests_passed":false,"summary":"Review completed with issues"}' - fi - - if [ -f "$EXEC_FILE" ]; then - LAST_TEXT=$(jq -r '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text] | last // ""' "$EXEC_FILE" 2>/dev/null) - OUTPUT=$(echo "$LAST_TEXT" | sed -n '/```json/,/```/{/```json/d;/```/d;p;}' | tr -d '\r') - if echo "$OUTPUT" | jq . >/dev/null 2>&1 && [ -n "$OUTPUT" ]; then - echo "$OUTPUT" > /tmp/review_output.json - else - echo "$DEFAULT_OUTPUT" > /tmp/review_output.json - fi - else - echo "$DEFAULT_OUTPUT" > /tmp/review_output.json - fi - - PASSED=$(jq -r '.passed // false' /tmp/review_output.json) - SUMMARY=$(jq -r '.summary // "No summary"' /tmp/review_output.json | head -c 200) - TESTS_PASSED=$(jq -r '.tests_passed // false' /tmp/review_output.json) - echo "passed=$PASSED" >> $GITHUB_OUTPUT - echo "summary=$SUMMARY" >> $GITHUB_OUTPUT - echo "tests_passed=$TESTS_PASSED" >> $GITHUB_OUTPUT - - - name: Post review comment - if: steps.filter.outputs.ui == 'true' && github.event_name == 'pull_request' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - OUTPUT=$(cat /tmp/review_output.json) - PASSED=$(echo "$OUTPUT" | jq -r '.passed // false') - CONFIDENCE=$(echo "$OUTPUT" | jq -r '.confidence // 0') - SUMMARY=$(echo "$OUTPUT" | jq -r '.summary // "No summary available"') - TESTS_PASSED=$(echo "$OUTPUT" | jq -r '.tests_passed // false') - - CRITICAL=$(echo "$OUTPUT" | jq -r '.total_critical // 0') - MAJOR=$(echo "$OUTPUT" | jq -r '.total_major // 0') - MINOR=$(echo "$OUTPUT" | jq -r '.total_minor // 0') - - if [ "$PASSED" = "true" ]; then - STATUS_ICON=":white_check_mark:" - STATUS_TEXT="PASSED" - else - STATUS_ICON=":x:" - STATUS_TEXT="FAILED" - fi - - COMMENT_BODY="## Playwright UI Review ${STATUS_ICON} ${STATUS_TEXT} - - **Confidence:** ${CONFIDENCE} - **Tests Passed:** $TESTS_PASSED - - ### Issue Summary - | Level | Count | - |-------|-------| - | :rotating_light: Critical | ${CRITICAL} | - | :warning: Major | ${MAJOR} | - | :information_source: Minor | ${MINOR} | - - ### Summary - ${SUMMARY} - " - - # Add best practices scores - BP=$(echo "$OUTPUT" | jq '.best_practices_evaluation // {}') - if [ "$BP" != "{}" ]; then - VISUAL=$(echo "$BP" | jq -r '.visual_design // "N/A"') - LAYOUT=$(echo "$BP" | jq -r '.layout_responsiveness // "N/A"') - A11Y=$(echo "$BP" | jq -r '.accessibility // "N/A"') - UX=$(echo "$BP" | jq -r '.user_experience // "N/A"') - ELEGANCE=$(echo "$BP" | jq -r '.modern_elegance // "N/A"') - - COMMENT_BODY="${COMMENT_BODY} - ### Best Practices Scores - | Category | Score | - |----------|-------| - | Visual Design | ${VISUAL}/10 | - | Layout & Responsiveness | ${LAYOUT}/10 | - | Accessibility | ${A11Y}/10 | - | User Experience | ${UX}/10 | - | Modern Elegance | ${ELEGANCE}/10 | - " - fi - - # Add top recommendations - RECS=$(echo "$OUTPUT" | jq -r '.top_recommendations // [] | .[]' 2>/dev/null) - if [ -n "$RECS" ]; then - COMMENT_BODY="${COMMENT_BODY} - ### Top Recommendations - $(echo "$RECS" | while read -r rec; do echo "1. $rec"; done) - " - fi - - # Add critical issues if any - if [ "$CRITICAL" -gt 0 ]; then - COMMENT_BODY="${COMMENT_BODY} - ### :rotating_light: Critical Issues - $(echo "$OUTPUT" | jq -r '.screenshot_reviews[]? | select(.critical_issues | length > 0) | "**\(.screenshot_path)**:\n\(.critical_issues | map(\"- \" + .) | join(\"\n\"))"' 2>/dev/null) - " - fi - - COMMENT_BODY="${COMMENT_BODY} - - --- - *Automated Playwright UI Review by Claude Code* - - [View Screenshots Artifact](/${{ github.repository }}/actions/runs/${{ github.run_id }})" - - gh pr comment ${{ github.event.pull_request.number }} --body "$COMMENT_BODY" - - - name: Set commit status - if: steps.filter.outputs.ui == 'true' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - PASSED="${{ steps.extract.outputs.passed }}" - OUTPUT=$(cat /tmp/review_output.json) - CRITICAL=$(echo "$OUTPUT" | jq -r '.total_critical // 0') - - if [ "$PASSED" = "true" ]; then - STATE="success" - DESCRIPTION="UI review passed" - else - STATE="failure" - DESCRIPTION="UI review failed: ${CRITICAL} critical issues" - fi - - gh api repos/${{ github.repository }}/statuses/${{ github.sha }} \ - -f state="$STATE" \ - -f target_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ - -f description="$DESCRIPTION" \ - -f context="Playwright UI Review" - - - name: Fail if review failed - if: steps.filter.outputs.ui == 'true' - run: | - PASSED="${{ steps.extract.outputs.passed }}" - - if [ "$PASSED" != "true" ]; then - echo "::error::UI review failed. See PR comment for details." - exit 1 - fi diff --git a/PLAN.md b/PLAN.md new file mode 120000 index 0000000..71cfe53 --- /dev/null +++ b/PLAN.md @@ -0,0 +1 @@ +/home/ben/.claude/plans/compiled-puzzling-crane.md \ No newline at end of file diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..a6f0c74 --- /dev/null +++ b/bun.lock @@ -0,0 +1,399 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "claude-code-actions", + "devDependencies": { + "@types/node": "^22.10.0", + "eslint": "^9.17.0", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.0", + "vitest": "^2.1.8", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], + + "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.54.0", "", { "os": "android", "cpu": "arm" }, "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.54.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.54.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.54.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.54.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.54.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.54.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.54.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.54.0", "", { "os": "none", "cpu": "arm64" }, "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.54.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.54.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.51.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/type-utils": "8.51.0", "@typescript-eslint/utils": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.2.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.51.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.51.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.51.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.51.0", "@typescript-eslint/types": "^8.51.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.51.0", "", { "dependencies": { "@typescript-eslint/types": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0" } }, "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.51.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.51.0", "", { "dependencies": { "@typescript-eslint/types": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0", "@typescript-eslint/utils": "8.51.0", "debug": "^4.3.4", "ts-api-utils": "^2.2.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.51.0", "", {}, "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.51.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.51.0", "@typescript-eslint/tsconfig-utils": "8.51.0", "@typescript-eslint/types": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.2.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.51.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.51.0", "", { "dependencies": { "@typescript-eslint/types": "8.51.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg=="], + + "@vitest/expect": ["@vitest/expect@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw=="], + + "@vitest/mocker": ["@vitest/mocker@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.12" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@2.1.9", "", { "dependencies": { "tinyrainbow": "^1.2.0" } }, "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ=="], + + "@vitest/runner": ["@vitest/runner@2.1.9", "", { "dependencies": { "@vitest/utils": "2.1.9", "pathe": "^1.1.2" } }, "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g=="], + + "@vitest/snapshot": ["@vitest/snapshot@2.1.9", "", { "dependencies": { "@vitest/pretty-format": "2.1.9", "magic-string": "^0.30.12", "pathe": "^1.1.2" } }, "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ=="], + + "@vitest/spy": ["@vitest/spy@2.1.9", "", { "dependencies": { "tinyspy": "^3.0.2" } }, "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ=="], + + "@vitest/utils": ["@vitest/utils@2.1.9", "", { "dependencies": { "@vitest/pretty-format": "2.1.9", "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="], + + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "tinyrainbow": ["tinyrainbow@1.2.0", "", {}, "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ=="], + + "tinyspy": ["tinyspy@3.0.2", "", {}, "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q=="], + + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "typescript-eslint": ["typescript-eslint@8.51.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.51.0", "@typescript-eslint/parser": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0", "@typescript-eslint/utils": "8.51.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + + "vite-node": ["vite-node@2.1.9", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.3.7", "es-module-lexer": "^1.5.4", "pathe": "^1.1.2", "vite": "^5.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA=="], + + "vitest": ["vitest@2.1.9", "", { "dependencies": { "@vitest/expect": "2.1.9", "@vitest/mocker": "2.1.9", "@vitest/pretty-format": "^2.1.9", "@vitest/runner": "2.1.9", "@vitest/snapshot": "2.1.9", "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "debug": "^4.3.7", "expect-type": "^1.1.0", "magic-string": "^0.30.12", "pathe": "^1.1.2", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.1", "tinypool": "^1.0.1", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", "vite-node": "2.1.9", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", "@vitest/browser": "2.1.9", "@vitest/ui": "2.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + } +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..39cd42b --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,16 @@ +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { + ignores: ["node_modules", "dist"], + }, + ...tseslint.configs.recommended, + { + files: ["**/*.ts"], + languageOptions: { + parserOptions: { + project: true, + }, + }, + } +); diff --git a/package.json b/package.json new file mode 100644 index 0000000..88c0cd4 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "claude-code-actions", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "eslint": "^9.17.0", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.0", + "vitest": "^2.1.8" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..fe025eb --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2 @@ +// Placeholder for CI template repo +export const version = "0.0.0"; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..40bfcc0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src/**/*", "*.ts"], + "exclude": ["node_modules"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..ae847ff --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + }, +});