Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/claude-review-phase1.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ jobs:
with:
claude_args: |
code-review --comment \
--model claude-opus-4-5-20251101 \
--model claude-opus-4-6 \
--max-turns 20 \
--allowedTools "Bash(git diff *),Bash(git log *),Bash(git blame *),Read"

Expand Down
35 changes: 19 additions & 16 deletions .github/workflows/claude-review-phase2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ jobs:
with:
claude_args: |
code-review \
--model claude-opus-4-5-20251101 \
--model claude-opus-4-6 \
--max-turns 20 \
--allowedTools "Bash(git diff *),Bash(git log *),Bash(git blame *),Read"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand Down Expand Up @@ -221,21 +221,24 @@ jobs:
CONSENSUS_REASON: ${{ needs.detect-high-stakes.outputs.reason }}
run: |
CLAUDE_REVIEW=$(cat /tmp/review-artifacts/claude-review.json | jq -R -s .)
DIFF_CONTENT=$(cat /tmp/pr-diff.patch | jq -R -s .)

cat > /tmp/consensus-request.json <<EOF
{
"prompt": "Review this PR for security, correctness, and architectural concerns. Claude Code Review has already analyzed it. Provide a multi-model consensus on whether this change is safe to merge.\n\nPR Description: ${PR_TITLE}\n\nChanged Files:\n$(cat /tmp/changed-files.txt)\n\nClaude Review Summary:\n${CLAUDE_REVIEW}\n\nReason for consensus: ${CONSENSUS_REASON}",
"models": [
{"model": "gpt-5-mini", "stance": "for", "stance_prompt": "Evaluate the benefits and improvements this change brings"},
{"model": "claude-haiku-4.5", "stance": "against", "stance_prompt": "Identify security risks, bugs, and potential issues"},
{"model": "gemini-2-flash", "stance": "neutral", "stance_prompt": "Provide a balanced assessment of merge-readiness"}
],
"focus_areas": ["security", "correctness", "maintainability", "testing"],
"temperature": 0.2,
"thinking_mode": "high"
}
EOF
CHANGED_FILES=$(cat /tmp/changed-files.txt)

jq -n \
--arg pr_title "$PR_TITLE" \
--arg changed_files "$CHANGED_FILES" \
--arg claude_review "$CLAUDE_REVIEW" \
--arg consensus_reason "$CONSENSUS_REASON" \
'{
prompt: ("Review this PR for security, correctness, and architectural concerns. Claude Code Review has already analyzed it. Provide a multi-model consensus on whether this change is safe to merge.\n\nPR Description: " + $pr_title + "\n\nChanged Files:\n" + $changed_files + "\n\nClaude Review Summary:\n" + $claude_review + "\n\nReason for consensus: " + $consensus_reason),
models: [
{model: "gpt-5-mini", stance: "for", stance_prompt: "Evaluate the benefits and improvements this change brings"},
{model: "claude-haiku-4.5", stance: "against", stance_prompt: "Identify security risks, bugs, and potential issues"},
{model: "gemini-2-flash", stance: "neutral", stance_prompt: "Provide a balanced assessment of merge-readiness"}
],
focus_areas: ["security", "correctness", "maintainability", "testing"],
temperature: 0.2,
thinking_mode: "high"
}' > /tmp/consensus-request.json

RESPONSE=$(curl -s -X POST "$PAL_MCP_ENDPOINT" \
-H "Authorization: Bearer $PAL_MCP_API_KEY" \
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/claude-review-phase3.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ jobs:
uses: actions/checkout@v6

- name: Download patch
uses: actions/download-artifact@v8
uses: actions/download-artifact@v4
with:
name: ai-suggestions-patch
path: /tmp/
Expand Down Expand Up @@ -250,7 +250,7 @@ jobs:
token: ${{ secrets.PAT_TOKEN || secrets.GITHUB_TOKEN }}

- name: Download patch
uses: actions/download-artifact@v8
uses: actions/download-artifact@v4
with:
name: ai-suggestions-patch
path: /tmp/
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/nightly-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,8 @@ jobs:
id: check_providers
if: steps.scope_selector.outputs.skip != 'true'
run: |
BEDROCK_CONFIGURED="${{ secrets.AWS_BEARER_TOKEN_BEDROCK != '' }}"
ANTHROPIC_CONFIGURED="${{ secrets.ANTHROPIC_API_KEY != '' }}"
BEDROCK_CONFIGURED="${{ secrets.AWS_BEARER_TOKEN_BEDROCK != '' && 'true' || 'false' }}"
ANTHROPIC_CONFIGURED="${{ secrets.ANTHROPIC_API_KEY != '' && 'true' || 'false' }}"

echo "bedrock_available=$BEDROCK_CONFIGURED" >> $GITHUB_OUTPUT
echo "anthropic_available=$ANTHROPIC_CONFIGURED" >> $GITHUB_OUTPUT
Expand Down Expand Up @@ -634,7 +634,7 @@ jobs:
run: |
# Read-only validation -- no --fix to ensure we validate the exact state
# produced by apply_autofix.py (using --fix here would mask idempotency failures)
ruff check SuperClaude/
ruff check .
RUFF_EXIT=$?

if [[ $RUFF_EXIT -eq 0 ]]; then
Expand Down
65 changes: 30 additions & 35 deletions scripts/create_prs.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,17 @@
}


def run_command(cmd: List[str], capture_output: bool = True) -> Optional[str]:
"""Run shell command and return output."""
try:
result = subprocess.run(cmd, capture_output=capture_output, text=True, check=True)
return result.stdout.strip() if capture_output else None
except subprocess.CalledProcessError as e:
print(f"Command failed: {' '.join(cmd)}", file=sys.stderr)
print(f"Error: {e.stderr if e.stderr else str(e)}", file=sys.stderr)
def run_command(cmd: List[str], capture_output: bool = True, check: bool = True) -> Optional[str]:
"""Run shell command and return output.

Let It Crash: CalledProcessError propagates by default (check=True).
Callers that need to inspect failure should pass check=False;
returns None when check=False and the command fails.
"""
result = subprocess.run(cmd, capture_output=capture_output, text=True, check=check)
if not check and result.returncode != 0:
return None
return result.stdout.strip() if capture_output else None


def get_existing_pr_for_category(category: str) -> Optional[Dict[str, Any]]:
Expand All @@ -59,7 +61,8 @@ def get_existing_pr_for_category(category: str) -> Optional[Dict[str, Any]]:
"number,title,headRefName,labels",
"--limit",
"10",
]
],
check=False,
)

if not output:
Expand All @@ -83,15 +86,17 @@ def create_or_update_branch(category: str, pr_content_file: Path) -> str:
branch_name = f"nightly-review/{category}/{date_str}"

# Check if branch exists locally
branch_exists = run_command(["git", "rev-parse", "--verify", branch_name]) is not None
branch_exists = (
run_command(["git", "rev-parse", "--verify", branch_name], check=False) is not None
)

if branch_exists:
print(f"Branch {branch_name} exists - checking out")
run_command(["git", "checkout", branch_name])
else:
print(f"Creating new branch: {branch_name}")
# Ensure we're on main/master before creating branch
main_branch = run_command(["git", "symbolic-ref", "refs/remotes/origin/HEAD"])
main_branch = run_command(["git", "symbolic-ref", "refs/remotes/origin/HEAD"], check=False)
if main_branch:
main_branch = main_branch.split("/")[-1]
else:
Expand Down Expand Up @@ -155,15 +160,10 @@ def update_existing_pr(pr_number: int, pr_content_file: Path) -> bool:
print(f"Error reading PR content: {e}", file=sys.stderr)
return False

# Update PR body
result = run_command(["gh", "pr", "edit", str(pr_number), "--body", pr_body])

if result is not None: # Command succeeded (even if no output)
print(f"Updated PR #{pr_number}")
return True
else:
print(f"Failed to update PR #{pr_number}", file=sys.stderr)
return False
# Update PR body — Let It Crash on failure (check=True default)
run_command(["gh", "pr", "edit", str(pr_number), "--body", pr_body])
print(f"Updated PR #{pr_number}")
return True


def get_existing_autofix_pr_for_category(category: str) -> Optional[Dict[str, Any]]:
Expand All @@ -184,7 +184,8 @@ def get_existing_autofix_pr_for_category(category: str) -> Optional[Dict[str, An
"number,title,headRefName,labels",
"--limit",
"10",
]
],
check=False,
)

if not output:
Expand All @@ -207,15 +208,17 @@ def create_autofix_branch(category: str) -> str:
branch_name = f"nightly-review-autofix/{category}/{date_str}"

# Check if branch exists locally
branch_exists = run_command(["git", "rev-parse", "--verify", branch_name]) is not None
branch_exists = (
run_command(["git", "rev-parse", "--verify", branch_name], check=False) is not None
)

if branch_exists:
print(f"Autofix branch {branch_name} exists - checking out")
run_command(["git", "checkout", branch_name])
else:
print(f"Creating new autofix branch: {branch_name}")
# Ensure we're on main/master before creating branch
main_branch = run_command(["git", "symbolic-ref", "refs/remotes/origin/HEAD"])
main_branch = run_command(["git", "symbolic-ref", "refs/remotes/origin/HEAD"], check=False)
if main_branch:
main_branch = main_branch.split("/")[-1]
else:
Expand Down Expand Up @@ -313,12 +316,8 @@ def process_autofix_category(category: str, pr_content_dir: Path) -> bool:
]
)

# Push branch (fail-fast on error, no force-push)
push_result = run_command(["git", "push", "-u", "origin", branch_name])
if push_result is None:
print(f"ERROR: Failed to push branch {branch_name}", file=sys.stderr)
print("Let It Crash: Push failed - investigate the error above", file=sys.stderr)
return False
# Push branch — Let It Crash on failure (no force-push)
run_command(["git", "push", "-u", "origin", branch_name])

# Create PR
return create_autofix_pr_with_gh(category, branch_name, pr_content_file)
Expand Down Expand Up @@ -363,12 +362,8 @@ def process_category(category: str, pr_content_dir: Path) -> bool:
]
)

# Push branch (fail-fast on error, no force-push)
push_result = run_command(["git", "push", "-u", "origin", branch_name])
if push_result is None:
print(f"ERROR: Failed to push branch {branch_name}", file=sys.stderr)
print("Let It Crash: Push failed - investigate the error above", file=sys.stderr)
return False
# Push branch — Let It Crash on failure (no force-push)
run_command(["git", "push", "-u", "origin", branch_name])

# Create PR
return create_pr_with_gh(category, branch_name, pr_content_file)
Expand Down
2 changes: 1 addition & 1 deletion scripts/generate_autofix_pr_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def generate_autofix_pr_description(
| Files Attempted | {category_results.get("files_attempted", 0) if category_results else 0} |
| Files Succeeded | {files_succeeded} |
| Files Failed | {files_failed} |
| Success Rate | {files_succeeded / max(category_results.get("files_attempted", 1), 1) * 100:.0f}% |
| Success Rate | {files_succeeded / max(category_results.get("files_attempted", 1) if category_results else 1, 1) * 100:.0f}% |

"""

Expand Down
2 changes: 1 addition & 1 deletion scripts/generate_suggestions.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def generate_pr_description(category: str, fix_plan: Dict[str, Any]) -> str:

**Generated by**: SuperClaude Nightly Code Review
**Review Method**: PAL MCP Multi-Model Consensus
**Review Date**: {{fix_plan.get('timestamp', 'N/A')}}
**Review Date**: {fix_plan.get("timestamp", "N/A")}
**Phase**: Phase 2 (Suggestion-only + Limited Autofix)

**Note**: This is a proactive, advisory review. All changes require human approval.
Expand Down
6 changes: 4 additions & 2 deletions scripts/normalize_findings.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def is_finding_autofix_eligible(finding: Dict[str, Any]) -> bool:

# LOC limit (estimate from line range)
line_start = finding.get("line_start", 0)
line_end = finding.get("line_end", 0)
line_end = finding.get("line_end", finding.get("line_start", 0))
loc_affected = line_end - line_start + 1

if loc_affected > AUTOFIX_MAX_LOC_PER_FINDING:
Expand Down Expand Up @@ -213,7 +213,9 @@ def generate_fix_plan(category: str, findings: List[Dict[str, Any]]) -> Dict[str
"summary": {
"top_severity": ranked[0].get("severity") if ranked else "none",
"files_affected": len(set(f.get("file") for f in ranked)),
"avg_confidence": sum(f.get("confidence", 0) for f in ranked) / len(ranked),
"avg_confidence": sum(f.get("confidence", 0) for f in ranked) / len(ranked)
if ranked
else 0,
"autofix_eligible": autofix_eligible_count, # Phase 2
"autofix_files": len(autofix_eligible_files), # Phase 2
},
Expand Down
Loading
Loading