diff --git a/.github/workflows/pr-behavioral-analysis.yml b/.github/workflows/pr-behavioral-analysis.yml
new file mode 100644
index 0000000000..df60f1f525
--- /dev/null
+++ b/.github/workflows/pr-behavioral-analysis.yml
@@ -0,0 +1,935 @@
+# PR Behavioral Analysis — altimate-code
+#
+# Generated from agent/templates/pr-behavioral-analysis.yml
+# with config/repos/altimate-code/profile.json values.
+#
+# Install: copy to AltimateAI/altimate-code at .github/workflows/pr-behavioral-analysis.yml
+#
+# Jobs:
+# gate → analyze → dispatch-fix (semantic bug detection + relay to QA Autopilot)
+# generate-tests → run-unit-tests (auto-generate + validate tests)
+# report-unfixable (PR comment + Slack for findings needing human review)
+# report (comprehensive quality report after all jobs)
+#
+# No Docker/smoke tests (has_smoke_tests: false)
+# No Sentry dispatch (sentry_project: null)
+
+name: PR Behavioral Analysis
+
+on:
+ pull_request:
+ types: [opened, ready_for_review]
+ branches: [main]
+ paths:
+ - 'packages/*/src/**'
+ - 'packages/*/test/**'
+ - 'packages/*/tests/**'
+
+ workflow_dispatch:
+ inputs:
+ pr_number:
+ description: 'PR number to analyze'
+ required: true
+ type: string
+ integration_only:
+ description: 'Only run integration tests (skip behavioral analysis)'
+ type: boolean
+ default: false
+
+concurrency:
+ group: behavioral-${{ github.event.pull_request.number || inputs.pr_number }}
+ cancel-in-progress: true
+
+env:
+ REPO_TOKEN_SECRET: ${{ secrets.BACKEND_REPO_TOKEN }}
+ QA_REPO: AltimateAI/altimate-qa
+
+jobs:
+ # ═══════════════════════════════════════════════════════════════
+ # Gate: Skip for bot-triggered PRs
+ # ═══════════════════════════════════════════════════════════════
+ gate:
+ name: Gate
+ runs-on: ubuntu-latest
+ if: >-
+ github.event_name == 'workflow_dispatch' ||
+ (github.event.pull_request.draft == false &&
+ !contains(fromJSON('["claude", "github-actions[bot]", "dependabot[bot]"]'), github.actor))
+ outputs:
+ should_run: ${{ steps.decide.outputs.run }}
+ steps:
+ - id: decide
+ run: echo "run=true" >> "$GITHUB_OUTPUT"
+
+ # ═══════════════════════════════════════════════════════════════
+ # Phase 1: Semantic analysis with Claude
+ # ═══════════════════════════════════════════════════════════════
+ analyze:
+ name: Behavioral Analysis
+ needs: [gate]
+ if: needs.gate.outputs.should_run == 'true' && inputs.integration_only != true
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ contents: read
+ pull-requests: read
+
+ outputs:
+ pr_number: ${{ steps.pr-info.outputs.pr_number }}
+ pr_author: ${{ steps.pr-info.outputs.pr_author }}
+ pr_url: ${{ steps.pr-info.outputs.pr_url }}
+ feature_branch: ${{ steps.pr-info.outputs.feature_branch }}
+ has_findings: ${{ steps.process.outputs.has_findings }}
+ fixable_count: ${{ steps.process.outputs.fixable_count }}
+ unfixable_count: ${{ steps.process.outputs.unfixable_count }}
+ skip: ${{ steps.process.outputs.skip }}
+ analysis_ran: ${{ steps.process.outputs.analysis_ran }}
+ has_dangerous_changes: ${{ steps.check-dangerous.outputs.has_dangerous }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Get PR info
+ id: pr-info
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
+ PR_NUM="${{ inputs.pr_number }}"
+ else
+ PR_NUM="${{ github.event.pull_request.number }}"
+ fi
+ PR_DATA=$(gh pr view "$PR_NUM" --json number,author,url,headRefName)
+ echo "pr_number=$(echo "$PR_DATA" | jq -r .number)" >> "$GITHUB_OUTPUT"
+ echo "pr_author=$(echo "$PR_DATA" | jq -r .author.login)" >> "$GITHUB_OUTPUT"
+ echo "pr_url=$(echo "$PR_DATA" | jq -r .url)" >> "$GITHUB_OUTPUT"
+ echo "feature_branch=$(echo "$PR_DATA" | jq -r .headRefName)" >> "$GITHUB_OUTPUT"
+
+ - name: Check for dangerous changes
+ id: check-dangerous
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ DIFF=$(gh pr diff "${{ steps.pr-info.outputs.pr_number }}" --name-only 2>/dev/null || echo "")
+ HAS_DANGEROUS=false
+
+ # Forbidden paths from altimate-code profile
+ for pattern in "packages/altimate-code/migration/" "bun.lock" "packages/altimate-engine/pyproject.toml" "node_modules/" "packages/*/dist/"; do
+ if echo "$DIFF" | grep -q "$pattern"; then
+ HAS_DANGEROUS=true
+ break
+ fi
+ done
+
+ echo "has_dangerous=$HAS_DANGEROUS" >> "$GITHUB_OUTPUT"
+
+ - name: Get PR diff
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ gh pr diff "${{ steps.pr-info.outputs.pr_number }}" > pr-diff.txt 2>/dev/null || echo "" > pr-diff.txt
+ DIFF_SIZE=$(wc -c < pr-diff.txt | tr -d ' ')
+ echo "Diff size: $DIFF_SIZE bytes"
+
+ # Truncate very large diffs
+ if [ "$DIFF_SIZE" -gt 100000 ]; then
+ head -c 100000 pr-diff.txt > pr-diff-truncated.txt
+ mv pr-diff-truncated.txt pr-diff.txt
+ echo "Truncated to 100KB"
+ fi
+
+ - name: Analyze with Claude
+ id: analyze
+ uses: anthropics/claude-code-action@v1
+ with:
+ model: claude-opus-4-6
+ max_turns: 3
+ prompt: |
+ You are a senior code reviewer performing behavioral analysis on a PR.
+
+ # IMPORTANT: Output format
+ You MUST output ONLY a JSON object. No markdown, no explanation, no preamble.
+
+ # Repository context
+ Language: TypeScript (primary), Python (secondary — altimate-engine)
+ Framework: CLI agent (Bun + bun:test)
+
+ # What to look for
+ Analyze the PR diff for semantic bugs — issues that are syntactically valid
+ but behaviorally wrong. Focus on:
+
+ 1. **null_access** — accessing properties/fields that could be null/undefined
+ 2. **type_mismatch** — wrong types passed, implicit conversions that lose data
+ 3. **missing_error_handling** — unhandled exceptions, missing error cases
+ 4. **behavioral_regression** — logic changes that break existing behavior
+ 5. **race_condition** — concurrent access, async/await issues
+ 6. **security** — injection, auth bypass, data exposure
+ 7. **missing_test** — complex new logic with no test coverage
+ 8. **api_contract_break** — changes to public API signatures/responses
+ 9. **anti_pattern** — code that works but will cause maintenance issues
+ 10. **backward_compat** — changes that break existing callers
+
+ # TypeScript-specific checks
+ - Type assertions (as/!) that bypass safety
+ - Optional chaining (?.) missing where needed
+ - Promise handling (missing await, unhandled rejections)
+ - Bun API misuse
+ - MCP protocol compliance issues
+
+ # Python-specific checks (altimate-engine)
+ - SQL injection in warehouse queries
+ - Missing error handling for external API calls
+ - Type annotation mismatches
+
+ # Diff to analyze
+
+ $(cat pr-diff.txt)
+
+
+ # Output format
+ Return a JSON object with this exact schema:
+ {
+ "findings": [
+ {
+ "category": "null_access|type_mismatch|...",
+ "severity": "critical|warning|suggestion",
+ "file": "path/to/file",
+ "line": 42,
+ "description": "What's wrong and why it matters",
+ "suggestion": "How to fix it",
+ "fixable": true,
+ "fix_code": "corrected code snippet (only if fixable=true)",
+ "reason_not_fixable": "why auto-fix isn't safe (only if fixable=false)",
+ "test_suggestion": "test case that would catch this (if missing_test)"
+ }
+ ],
+ "summary": "1-2 sentence overview",
+ "risk_level": "low|medium|high|critical"
+ }
+
+ Rules:
+ - Only report REAL issues you're confident about. No speculative findings.
+ - For each finding, explain WHY it's a problem with a concrete scenario.
+ - Mark fixable=true ONLY if the fix is unambiguous and safe.
+ - If the diff is trivial (docs, comments, formatting), return {"findings": [], "summary": "No issues", "risk_level": "low"}
+ allowed_tools: |
+ Read
+ Bash(cat pr-diff.txt)
+ Bash(wc*)
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
+
+ - name: Process findings
+ id: process
+ env:
+ CLAUDE_RESPONSE: ${{ steps.analyze.outputs.result }}
+ run: |
+ # Pass response via env var to avoid shell quoting issues
+ python3 -c "
+ import json, sys, re, os
+ text = os.environ.get('CLAUDE_RESPONSE', '')
+ try:
+ data = json.loads(text)
+ except:
+ match = re.search(r'\{[\s\S]*\}', text)
+ if match:
+ data = json.loads(match.group())
+ else:
+ data = {'findings': [], 'summary': 'Analysis failed to produce valid JSON', 'risk_level': 'low'}
+
+ with open('behavioral-findings.json', 'w') as f:
+ json.dump(data, f, indent=2)
+
+ findings = data.get('findings', [])
+ fixable = [f for f in findings if f.get('fixable')]
+ unfixable = [f for f in findings if not f.get('fixable')]
+
+ print(f'Total: {len(findings)}, Fixable: {len(fixable)}, Unfixable: {len(unfixable)}')
+
+ with open(os.environ.get('GITHUB_OUTPUT', '/dev/null'), 'a') as out:
+ out.write(f'has_findings={\"true\" if findings else \"false\"}\n')
+ out.write(f'fixable_count={len(fixable)}\n')
+ out.write(f'unfixable_count={len(unfixable)}\n')
+ out.write(f'skip={\"false\"}\n')
+ out.write(f'analysis_ran=true\n')
+ " || {
+ echo "has_findings=false" >> "$GITHUB_OUTPUT"
+ echo "fixable_count=0" >> "$GITHUB_OUTPUT"
+ echo "unfixable_count=0" >> "$GITHUB_OUTPUT"
+ echo "skip=true" >> "$GITHUB_OUTPUT"
+ echo "analysis_ran=false" >> "$GITHUB_OUTPUT"
+ }
+
+ - name: Upload findings
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: behavioral-findings
+ path: behavioral-findings.json
+ retention-days: 7
+ if-no-files-found: ignore
+
+ # ═══════════════════════════════════════════════════════════════
+ # Phase 2: Dispatch critical findings to QA Autopilot
+ # ═══════════════════════════════════════════════════════════════
+ dispatch-fix:
+ name: Dispatch to Autopilot
+ needs: [gate, analyze]
+ if: >-
+ always() &&
+ needs.gate.result == 'success' &&
+ needs.analyze.result == 'success' &&
+ needs.analyze.outputs.has_findings == 'true' &&
+ needs.analyze.outputs.fixable_count != '0'
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ permissions:
+ contents: read
+
+ steps:
+ - name: Download findings
+ uses: actions/download-artifact@v4
+ with:
+ name: behavioral-findings
+
+ - name: Build and dispatch mission packet
+ env:
+ GH_TOKEN: ${{ secrets.BACKEND_REPO_TOKEN }}
+ QA_TOKEN: ${{ secrets.BACKEND_REPO_TOKEN }}
+ PR_NUM: ${{ needs.analyze.outputs.pr_number }}
+ PR_AUTHOR: ${{ needs.analyze.outputs.pr_author }}
+ run: |
+ python3 << 'PYEOF'
+ import json, os, subprocess, datetime
+
+ with open("behavioral-findings.json") as f:
+ data = json.load(f)
+
+ findings = data.get("findings", [])
+ critical = [f for f in findings if f.get("severity") == "critical" and f.get("fixable")]
+
+ if not critical:
+ print("No critical fixable findings — skipping dispatch")
+ exit(0)
+
+ # Build mission packet for QA Autopilot
+ mission = {
+ "mission_type": "behavioral-fix",
+ "source": "pr-behavioral-analysis",
+ "repo": "AltimateAI/altimate-code",
+ "slug": "altimate-code",
+ "target_repo": "altimate-code",
+ "pr_number": os.environ.get("PR_NUM", ""),
+ "pr_author": os.environ.get("PR_AUTHOR", ""),
+ "created_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
+ "triage": {
+ "critical_count": len(critical),
+ "total_count": len(findings),
+ "risk_level": data.get("risk_level", "medium"),
+ },
+ "findings": critical[:5],
+ "summary": data.get("summary", ""),
+ }
+
+ # Save mission for artifact
+ with open("behavioral-mission.json", "w") as f:
+ json.dump(mission, f, indent=2)
+
+ # Push to autopilot-state branch on altimate-qa
+ qa_token = os.environ.get("QA_TOKEN", "")
+ if qa_token:
+ mission_file = f"missions/behavioral-altimate-code-pr{os.environ.get('PR_NUM', 'unknown')}.json"
+ import base64 as b64
+ content_b64 = b64.b64encode(json.dumps(mission).encode()).decode()
+ result = subprocess.run(
+ ["gh", "api", f"repos/AltimateAI/altimate-qa/contents/{mission_file}",
+ "--method", "PUT",
+ "--field", f"message=behavioral mission: altimate-code PR#{os.environ.get('PR_NUM', '')}",
+ "--field", "branch=autopilot-state",
+ "--field", f"content={content_b64}"],
+ capture_output=True, text=True,
+ env={**os.environ, "GH_TOKEN": qa_token})
+ if result.returncode == 0:
+ print(f"Mission pushed to autopilot-state: {mission_file}")
+ else:
+ print(f"Mission push failed: {result.stderr}")
+
+ # Dispatch qa-autopilot workflow with target_repo=altimate-code
+ if qa_token:
+ result = subprocess.run(
+ ["gh", "workflow", "run", "qa-autopilot.yml",
+ "--repo", "AltimateAI/altimate-qa",
+ "--ref", "main",
+ "-f", f"mission_ref={mission_file}",
+ "-f", "target_repo=altimate-code"],
+ capture_output=True, text=True,
+ env={**os.environ, "GH_TOKEN": qa_token})
+ if result.returncode == 0:
+ print("Dispatched qa-autopilot workflow with target_repo=altimate-code")
+ else:
+ print(f"Dispatch failed: {result.stderr}")
+ PYEOF
+
+ - name: Upload mission
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: behavioral-mission
+ path: behavioral-mission.json
+ retention-days: 7
+ if-no-files-found: ignore
+
+ # ═══════════════════════════════════════════════════════════════
+ # Phase 3: Generate tests for changed code
+ # ═══════════════════════════════════════════════════════════════
+ generate-tests:
+ name: Generate Tests
+ needs: [gate, analyze]
+ if: >-
+ always() &&
+ needs.gate.result == 'success' &&
+ needs.analyze.outputs.skip != 'true'
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ permissions:
+ contents: write
+ pull-requests: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ needs.analyze.outputs.feature_branch }}
+ fetch-depth: 0
+ token: ${{ secrets.BACKEND_REPO_TOKEN }}
+
+ - name: Download findings
+ uses: actions/download-artifact@v4
+ with:
+ name: behavioral-findings
+ continue-on-error: true
+
+ - name: Get changed files
+ id: changed
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ PR_NUM="${{ needs.analyze.outputs.pr_number }}"
+ # Get TypeScript and Python source files changed in the PR
+ gh pr diff "$PR_NUM" --name-only | grep -E '\.(ts|tsx|js|py)$' > changed-files.txt || true
+ echo "Changed files:"
+ cat changed-files.txt
+ FILE_COUNT=$(wc -l < changed-files.txt | tr -d ' ')
+ echo "count=$FILE_COUNT" >> "$GITHUB_OUTPUT"
+
+ - name: Set up Bun
+ if: steps.changed.outputs.count != '0'
+ uses: oven-sh/setup-bun@v2
+
+ - name: Install dependencies
+ if: steps.changed.outputs.count != '0'
+ run: bun install
+
+ - name: Generate tests with Claude
+ if: steps.changed.outputs.count != '0'
+ uses: anthropics/claude-code-action@v1
+ with:
+ model: claude-sonnet-4-20250514
+ max_turns: 15
+ prompt: |
+ You are a test generation agent. Your job is to write tests for changed code.
+
+ # Repository context
+ Language: TypeScript (primary), Python (secondary — altimate-engine)
+ Framework: CLI agent (Bun, bun:test)
+
+ # Test patterns
+ TypeScript tests:
+ - Framework: bun:test
+ - Imports: { describe, test, expect, beforeEach, mock } from 'bun:test'
+ - Fixtures: tmpdir() for temporary directories, Instance.provide() for app context
+ - Mocking: mock.module() for external dependencies, extensive SDK mocking
+ - Assertions: Jest-style expect().toBe(), expect().toContain(), etc.
+ - Cleanup: await using for automatic resource cleanup
+
+ Python tests (altimate-engine):
+ - Framework: pytest
+ - Structure: class-based test organization (TestClassName)
+ - Standard assert statements
+
+ # Changed files
+ $(cat changed-files.txt)
+
+ # Behavioral findings (if any)
+ $(cat behavioral-findings.json 2>/dev/null || echo '{"findings": []}')
+
+ # Instructions
+ 1. Read each changed source file
+ 2. Read existing tests for those files (if any)
+ 3. Generate new tests or update existing tests to cover:
+ - New/changed functionality
+ - Edge cases from behavioral findings
+ - Error handling paths
+ 4. Follow the repo's existing test patterns exactly
+ 5. TypeScript tests go in packages/altimate-code/test/
+ 6. Python tests go in packages/altimate-engine/tests/
+
+ # Test runners
+ - TypeScript: `bun test` in packages/altimate-code
+ - Python: `pytest` in packages/altimate-engine
+
+ # Formatters (run after writing tests)
+ - TypeScript: `bunx prettier --write `
+ - Python: `ruff check --fix `
+
+ # Safety rules
+ - NEVER modify database migrations in packages/altimate-code/migration/
+ - NEVER modify package.json or pyproject.toml dependencies
+ - NEVER modify auth/ or security-related modules
+
+ # Output
+ After writing all tests, create a summary JSON:
+ {
+ "tests_added": ,
+ "tests_modified": ,
+ "files_created": ["path/to/new_test.ext", ...],
+ "files_modified": ["path/to/existing_test.ext", ...]
+ }
+ Write this to tests-generated.json.
+ allowed_tools: |
+ Read
+ Write
+ Edit
+ Glob
+ Grep
+ Bash(cat*)
+ Bash(ls*)
+ Bash(find*)
+ Bash(wc*)
+ Bash(bun test*)
+ Bash(bunx prettier*)
+ Bash(cd packages/altimate-engine && pytest*)
+ Bash(ruff*)
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
+
+ - name: Commit generated tests
+ if: steps.changed.outputs.count != '0'
+ env:
+ GH_TOKEN: ${{ secrets.BACKEND_REPO_TOKEN }}
+ run: |
+ git config user.name "qa-autopilot[bot]"
+ git config user.email "qa-autopilot[bot]@users.noreply.github.com"
+
+ git add -A
+ if git diff --cached --quiet; then
+ echo "No test changes to commit"
+ else
+ git commit -m "test: add behavioral tests for PR #${{ needs.analyze.outputs.pr_number }}
+
+ Generated by PR Behavioral Analysis workflow.
+ Co-authored-by: Claude "
+ git push origin HEAD
+ echo "Tests committed and pushed"
+ fi
+
+ - name: Upload test generation summary
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: tests-generated
+ path: tests-generated.json
+ retention-days: 7
+ if-no-files-found: ignore
+
+ # ═══════════════════════════════════════════════════════════════
+ # Phase 4: Run unit tests
+ # ═══════════════════════════════════════════════════════════════
+ run-unit-tests:
+ name: Run Unit Tests
+ needs: [gate, analyze, generate-tests]
+ if: >-
+ always() &&
+ needs.gate.result == 'success' &&
+ needs.analyze.outputs.skip != 'true' &&
+ needs.generate-tests.result != 'cancelled'
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ needs.analyze.outputs.feature_branch }}
+ fetch-depth: 1
+
+ - name: Set up Bun
+ uses: oven-sh/setup-bun@v2
+
+ - name: Install dependencies
+ run: bun install
+
+ - name: Set up Python (for altimate-engine)
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Run TypeScript tests
+ id: ts-test
+ continue-on-error: true
+ run: |
+ cd packages/altimate-code
+ bun test --timeout 30000 2>&1 | tee /tmp/ts-test-output.txt
+ echo "exit_code=$?" >> "$GITHUB_OUTPUT"
+
+ - name: Run Python tests (altimate-engine)
+ id: py-test
+ continue-on-error: true
+ run: |
+ if [ -d packages/altimate-engine/tests ]; then
+ cd packages/altimate-engine
+ pip install -e '.[dev]' 2>/dev/null || true
+ pytest tests/ -v --tb=short 2>&1 | tee /tmp/py-test-output.txt || true
+ else
+ echo "No Python tests found"
+ fi
+
+ - name: Summarize results
+ run: |
+ python3 -c "
+ import json
+ summary = {'summary': {'passed': 0, 'failed': 0, 'total': 0}}
+ # Basic summary — actual counts come from test output parsing
+ json.dump(summary, open('unit-test-results.json', 'w'), indent=2)
+ "
+
+ - name: Upload results
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: unit-test-results
+ path: unit-test-results.json
+ retention-days: 7
+ if-no-files-found: ignore
+
+ # ═══════════════════════════════════════════════════════════════
+ # Phase 6: Report unfixable findings (PR comment + Slack)
+ # ═══════════════════════════════════════════════════════════════
+ report-unfixable:
+ name: Report Findings
+ needs: [gate, analyze]
+ if: >-
+ always() &&
+ needs.gate.result == 'success' &&
+ needs.analyze.result == 'success' &&
+ needs.analyze.outputs.has_findings == 'true'
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ permissions:
+ contents: read
+ pull-requests: write
+
+ env:
+ PR_NUM: ${{ needs.analyze.outputs.pr_number }}
+ PR_AUTHOR: ${{ needs.analyze.outputs.pr_author }}
+ PR_URL: ${{ needs.analyze.outputs.pr_url }}
+
+ steps:
+ - name: Download findings
+ uses: actions/download-artifact@v4
+ with:
+ name: behavioral-findings
+
+ - name: Post PR comment and Slack notification
+ env:
+ GH_TOKEN: ${{ github.token }}
+ SLACK_BOT_TOKEN: ${{ secrets.QA_SLACK_BOT_TOKEN }}
+ run: |
+ python3 << 'PYEOF'
+ import json, os, subprocess, urllib.request, urllib.parse
+
+ PR_NUM = os.environ["PR_NUM"]
+ PR_AUTHOR = os.environ["PR_AUTHOR"]
+ PR_URL = os.environ["PR_URL"]
+ SLACK_TOKEN = os.environ.get("SLACK_BOT_TOKEN", "")
+ RUN_URL = f"{os.environ.get('GITHUB_SERVER_URL', '')}/{os.environ.get('GITHUB_REPOSITORY', '')}/actions/runs/{os.environ.get('GITHUB_RUN_ID', '')}"
+
+ GITHUB_SLACK_MAP = {}
+
+ def resolve_slack_tag(github_user):
+ if github_user in GITHUB_SLACK_MAP:
+ return f"<@{GITHUB_SLACK_MAP[github_user]}>"
+ if SLACK_TOKEN:
+ try:
+ gh_result = subprocess.run(
+ ["gh", "api", f"users/{github_user}", "--jq", ".email // empty"],
+ capture_output=True, text=True)
+ email = gh_result.stdout.strip()
+ if email:
+ req = urllib.request.Request(
+ f"https://slack.com/api/users.lookupByEmail?email={urllib.parse.quote(email)}",
+ headers={"Authorization": f"Bearer {SLACK_TOKEN}"})
+ resp = urllib.request.urlopen(req, timeout=10)
+ sid = json.loads(resp.read()).get("user", {}).get("id", "")
+ if sid:
+ return f"<@{sid}>"
+ except Exception:
+ pass
+ return f"@{github_user}"
+
+ with open("behavioral-findings.json") as f:
+ data = json.load(f)
+
+ all_findings = data.get("findings", [])
+ if not all_findings:
+ exit(0)
+
+ severity_icons = {"critical": ":red_circle:", "warning": ":yellow_circle:", "suggestion": ":white_circle:"}
+
+ # PR comment for unfixable findings
+ unfixable = [f for f in all_findings if not f.get("fixable") or f.get("category") == "missing_test"]
+ if unfixable:
+ rows = []
+ for i, f in enumerate(unfixable, 1):
+ icon = severity_icons.get(f["severity"], ":white_circle:")
+ sev = f["severity"].capitalize()
+ file_line = f"{f['file']}:{f['line']}"
+ desc = f["description"]
+ reason = f.get("reason_not_fixable", f.get("test_suggestion", "Needs developer review"))
+ rows.append(f"| {i} | {icon} {sev} | `{file_line}` | {desc} | {reason} |")
+
+ comment = f"## :warning: Behavioral Analysis Findings\n\n"
+ comment += f"{len(unfixable)} issue(s) need your attention:\n\n"
+ comment += "| # | Severity | File | Issue | Why not auto-fixed |\n"
+ comment += "|---|----------|------|-------|-------------------|\n"
+ comment += "\n".join(rows) + "\n\n"
+ comment += f"---\n_Behavioral analysis by Claude — [workflow run]({RUN_URL})_"
+
+ subprocess.run(["gh", "pr", "comment", PR_NUM, "--body", comment], capture_output=True, text=True)
+
+ # Slack notification to #test-execution-alerts
+ SLACK_CHANNEL = "C0ABJ9CP50Q"
+
+ if not SLACK_TOKEN:
+ exit(0)
+
+ dev_tag = resolve_slack_tag(PR_AUTHOR)
+ critical = [f for f in all_findings if f["severity"] == "critical"]
+ warnings = [f for f in all_findings if f["severity"] == "warning"]
+
+ lines = []
+ for f in (critical + warnings)[:8]:
+ icon = severity_icons.get(f["severity"], ":white_circle:")
+ file_line = f"{f['file']}:{f['line']}"
+ desc = f["description"]
+ if len(desc) > 200:
+ desc = desc[:197] + "..."
+ lines.append(f"{icon} `{file_line}` — {desc}")
+
+ remaining = len(all_findings) - len(lines)
+ if remaining > 0:
+ lines.append(f"_...and {remaining} more finding(s) — see PR comment_")
+
+ findings_text = "\n".join(lines)
+ slack_msg = (
+ f":mag: *Behavioral Analysis — altimate-code <{PR_URL}|PR #{PR_NUM}>* by {dev_tag}\n\n"
+ f"{len(all_findings)} finding(s) ({len(critical)} critical, {len(warnings)} warning):\n\n"
+ f"{findings_text}\n\n"
+ f"<{PR_URL}|View PR> · <{RUN_URL}|View analysis>"
+ )
+
+ payload = json.dumps({"channel": SLACK_CHANNEL, "text": slack_msg, "unfurl_links": False}).encode()
+ req = urllib.request.Request(
+ "https://slack.com/api/chat.postMessage",
+ data=payload,
+ headers={"Authorization": f"Bearer {SLACK_TOKEN}", "Content-Type": "application/json"})
+ resp = urllib.request.urlopen(req)
+ resp_data = json.loads(resp.read())
+ if resp_data.get("ok"):
+ ts = resp_data.get("ts", "")
+ print(f"Posted to Slack — ts={ts}")
+ with open("slack-thread-ts.txt", "w") as tsf:
+ tsf.write(ts)
+ else:
+ print(f"Slack error: {resp_data.get('error')}")
+ PYEOF
+
+ - name: Upload Slack thread ID
+ if: always()
+ uses: actions/upload-artifact@v4
+ continue-on-error: true
+ with:
+ name: slack-thread-ts
+ path: slack-thread-ts.txt
+ retention-days: 1
+ if-no-files-found: ignore
+
+ # ═══════════════════════════════════════════════════════════════
+ # Phase 7: Comprehensive quality report
+ # ═══════════════════════════════════════════════════════════════
+ report:
+ name: Quality Report
+ needs: [gate, analyze, dispatch-fix, generate-tests, run-unit-tests]
+ if: always() && needs.gate.result == 'success' && needs.analyze.outputs.skip != 'true'
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ permissions:
+ contents: read
+ pull-requests: write
+ continue-on-error: true
+
+ env:
+ PR_NUM: ${{ needs.analyze.outputs.pr_number }}
+ PR_AUTHOR: ${{ needs.analyze.outputs.pr_author }}
+ PR_URL: ${{ needs.analyze.outputs.pr_url }}
+ FEATURE_BRANCH: ${{ needs.analyze.outputs.feature_branch }}
+ FIXABLE_COUNT: ${{ needs.analyze.outputs.fixable_count || '0' }}
+ UNFIXABLE_COUNT: ${{ needs.analyze.outputs.unfixable_count || '0' }}
+ AUTOFIX_RESULT: ${{ needs.dispatch-fix.result }}
+ GENTESTS_RESULT: ${{ needs.generate-tests.result }}
+ UNIT_RESULT: ${{ needs.run-unit-tests.result }}
+ HAS_DANGEROUS: ${{ needs.analyze.outputs.has_dangerous_changes || 'false' }}
+
+ steps:
+ - name: Download artifacts
+ continue-on-error: true
+ run: echo "Downloading artifacts..."
+
+ - name: Download behavioral findings
+ if: needs.analyze.outputs.analysis_ran == 'true'
+ uses: actions/download-artifact@v4
+ with:
+ name: behavioral-findings
+ path: artifacts/
+ continue-on-error: true
+
+ - name: Download behavioral mission
+ if: needs.dispatch-fix.result == 'success'
+ uses: actions/download-artifact@v4
+ with:
+ name: behavioral-mission
+ path: artifacts/
+ continue-on-error: true
+
+ - name: Download tests generated
+ if: needs.generate-tests.result == 'success'
+ uses: actions/download-artifact@v4
+ with:
+ name: tests-generated
+ path: artifacts/
+ continue-on-error: true
+
+ - name: Download unit test results
+ if: needs.run-unit-tests.result == 'success'
+ uses: actions/download-artifact@v4
+ with:
+ name: unit-test-results
+ path: artifacts/
+ continue-on-error: true
+
+ - name: Download Slack thread ID
+ uses: actions/download-artifact@v4
+ with:
+ name: slack-thread-ts
+ path: artifacts/
+ continue-on-error: true
+
+ - name: Build and post report
+ env:
+ GH_TOKEN: ${{ github.token }}
+ SLACK_BOT_TOKEN: ${{ secrets.QA_SLACK_BOT_TOKEN }}
+ run: |
+ python3 << 'PYEOF'
+ import json, os, subprocess, urllib.request
+
+ PR_NUM = os.environ["PR_NUM"]
+ PR_AUTHOR = os.environ["PR_AUTHOR"]
+ PR_URL = os.environ["PR_URL"]
+ BRANCH = os.environ["FEATURE_BRANCH"]
+ SLACK_TOKEN = os.environ.get("SLACK_BOT_TOKEN", "")
+ RUN_URL = f"{os.environ.get('GITHUB_SERVER_URL', '')}/{os.environ.get('GITHUB_REPOSITORY', '')}/actions/runs/{os.environ.get('GITHUB_RUN_ID', '')}"
+
+ FIXABLE = int(os.environ.get("FIXABLE_COUNT", "0"))
+ UNFIXABLE = int(os.environ.get("UNFIXABLE_COUNT", "0"))
+ AUTOFIX_OK = os.environ.get("AUTOFIX_RESULT") == "success"
+ UNIT_OK = os.environ.get("UNIT_RESULT") == "success"
+
+ def load_json(path):
+ try:
+ with open(path) as f:
+ return json.load(f)
+ except Exception:
+ return None
+
+ findings = load_json("artifacts/behavioral-findings.json")
+ autofix = load_json("artifacts/behavioral-mission.json")
+ tests_gen = load_json("artifacts/tests-generated.json")
+ unit_results = load_json("artifacts/unit-test-results.json")
+
+ # Build PR Comment
+ lines = [f"## PR #{PR_NUM} — Test Report\n"]
+
+ total_findings = FIXABLE + UNFIXABLE
+ if total_findings > 0:
+ lines.append("### Behavioral Analysis")
+ lines.append(f"Found **{total_findings}** issue(s): {FIXABLE} fixable, {UNFIXABLE} need review\n")
+ if autofix and autofix.get("mission_type") == "behavioral-fix":
+ critical_count = autofix.get("triage", {}).get("critical_count", 0)
+ lines.append(f"**Dispatched to QA Autopilot:** {critical_count} critical finding(s) relayed for auto-fix")
+ elif FIXABLE > 0 and AUTOFIX_OK:
+ lines.append("**Fix dispatched** to QA Autopilot for processing")
+ elif FIXABLE > 0:
+ lines.append("**Non-critical findings** — posted for developer review")
+ lines.append("")
+ else:
+ lines.append("### Behavioral Analysis")
+ lines.append("No semantic issues found.\n")
+
+ HAS_DANGEROUS = os.environ.get("HAS_DANGEROUS", "false") == "true"
+ if HAS_DANGEROUS:
+ lines.append("### :warning: Safety Guard Active")
+ lines.append("This PR has **migration/schema changes**. Additional tests were skipped to protect production.\n")
+
+ lines.append("### Tests Generated")
+ if tests_gen:
+ added = tests_gen.get("tests_added", 0)
+ modified = tests_gen.get("tests_modified", 0)
+ if added + modified > 0:
+ lines.append(f"Pushed to `{BRANCH}`: **{added}** new test(s), **{modified}** modified\n")
+ for f in tests_gen.get("files_created", []):
+ lines.append(f"- `{f}` (new)")
+ for f in tests_gen.get("files_modified", []):
+ lines.append(f"- `{f}` (modified)")
+ else:
+ lines.append("No test changes needed.")
+ else:
+ lines.append("Test generation skipped or failed.")
+ lines.append("")
+
+ lines.append("### Test Results\n")
+ lines.append("| Suite | Passed | Failed | Total | Status |")
+ lines.append("|-------|--------|--------|-------|--------|")
+
+ if unit_results and unit_results.get("summary"):
+ us = unit_results["summary"]
+ u_status = "passed" if us.get("failed", 0) == 0 else "failed"
+ u_icon = ":white_check_mark:" if u_status == "passed" else ":x:"
+ lines.append(f"| Unit Tests | {us.get('passed', 0)} | {us.get('failed', 0)} | {us.get('total', 0)} | {u_icon} |")
+ else:
+ u_icon = ":warning:"
+ lines.append(f"| Unit Tests | - | - | - | {u_icon} Skipped/Error |")
+
+ lines.append("")
+ lines.append("---")
+ lines.append(f"_Generated by [PR Behavioral Analysis]({RUN_URL})_")
+
+ comment = "\n".join(lines)
+ result = subprocess.run(["gh", "pr", "comment", PR_NUM, "--body", comment], capture_output=True, text=True)
+ if result.returncode == 0:
+ print("PR comment posted")
+ else:
+ print(f"PR comment failed: {result.stderr}")
+ PYEOF