-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli.py
More file actions
169 lines (139 loc) · 5.75 KB
/
Copy pathcli.py
File metadata and controls
169 lines (139 loc) · 5.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
#!/usr/bin/env python3
"""
CodeQuorum CLI — detects design flaws, proposes tests, and refactors.
Used by the GitHub Actions workflow to post results as a PR comment.
Usage:
python cli.py --path . --format markdown
python cli.py --path ./src --format json
"""
import argparse
import json
import os
import sys
from pathlib import Path
CODE_EXTENSIONS = {
".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".go", ".rb", ".rs",
".cpp", ".c", ".cs", ".php", ".swift", ".kt", ".scala", ".sh",
}
SKIP_DIRS = {"node_modules", "dist", "build", ".git", "venv", "__pycache__", ".next", "vendor"}
MAX_FILES = 10
MAX_BYTES = 30_000
def get_pr_changed_files() -> list[str]:
"""Return files changed in the current PR using git diff against the base branch."""
import subprocess
base = os.getenv("GITHUB_BASE_REF", "main")
try:
result = subprocess.run(
["git", "diff", "--name-only", f"origin/{base}...HEAD"],
capture_output=True, text=True, check=True,
)
return [f.strip() for f in result.stdout.splitlines() if f.strip()]
except Exception:
return []
def read_local_files(path: str, pr_files: list[str] | None = None) -> tuple[str, str]:
root = Path(path).resolve()
sections = []
if pr_files:
candidates = [
root / f for f in pr_files
if Path(f).suffix in CODE_EXTENSIONS
and not any(part in SKIP_DIRS for part in Path(f).parts)
]
if len(candidates) > MAX_FILES:
skipped = len(candidates) - MAX_FILES
print(f"Warning: {len(candidates)} changed files found, reviewing first {MAX_FILES} (skipping {skipped})", file=sys.stderr)
label_suffix = f"{min(len(candidates), MAX_FILES)} of {len(candidates)} changed files"
else:
candidates = sorted(
[f for f in root.rglob("*")
if f.is_file()
and f.suffix in CODE_EXTENSIONS
and not any(part in SKIP_DIRS for part in f.parts)],
key=lambda f: f.stat().st_size,
reverse=True,
)
label_suffix = f"{min(len(candidates), MAX_FILES)} files"
for f in candidates[:MAX_FILES]:
try:
content = Path(f).read_text(errors="ignore")[:MAX_BYTES]
rel = Path(f).relative_to(root)
sections.append(f"# --- {rel} ---\n{content}")
except Exception:
pass
if not sections:
print("No code files found.", file=sys.stderr)
sys.exit(1)
label = f"{root.name} ({label_suffix})"
return "\n\n".join(sections), label
def findings_to_markdown(source_label: str, synthesis: dict) -> str:
findings = synthesis.get("findings", [])
verdict = synthesis.get("verdict", "")
fix_items = [f for f in findings if f.get("call") == "FIX_IT"]
call_items = [f for f in findings if f.get("call") == "YOUR_CALL"]
AGENT_ICONS = {"pragmatist": "🚢", "purist": "🎯", "operator": "🔧"}
lines = [
"## ⚖️ CodeQuorum Review",
"",
f"**Reviewed:** `{source_label}`",
f"**Design flaws:** {len(findings)} total · 🔴 {len(fix_items)} Fix It · 🟡 {len(call_items)} Your Call",
"",
]
if verdict:
lines += [f"> {verdict}", ""]
if fix_items:
lines += ["### 🔴 Fix It — Refactored Code + Test (2+ agents agreed)", ""]
for f in fix_items:
agents = f.get("agents", [])
icons = " ".join(AGENT_ICONS.get(a, "") for a in agents)
conf = f.get("confidence", "")
lines += [f"**{f.get('issue', '')}** — `{conf}` {icons}", ""]
if f.get("fix"):
lines += [f"*{f['fix']}*", ""]
if f.get("refactored_code"):
lines += ["**Refactored:**", f"```python\n{f['refactored_code']}\n```", ""]
if f.get("test"):
lines += ["**Proposed test:**", f"```python\n{f['test']}\n```", ""]
lines.append("---")
if call_items:
lines += ["", "### 🟡 Your Call — Design Tradeoffs (1 agent flagged)", ""]
for f in call_items:
agents = f.get("agents", [])
icons = " ".join(AGENT_ICONS.get(a, "") for a in agents)
lines += [f"**{f.get('issue', '')}** {icons}", ""]
if f.get("fix"):
lines += [f"*{f['fix']}*", ""]
lines.append("---")
lines += [
"",
"*Reviewed by [CodeQuorum](https://github.com/suboss87/CodeQuorum) — "
"3 specialist agents, parallel execution, confidence from consensus*",
]
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="CodeQuorum — detects design flaws, proposes tests, refactors")
parser.add_argument("--path", default=".", help="Path to local repo or directory")
parser.add_argument("--format", default="markdown", choices=["markdown", "json"],
help="Output format")
args = parser.parse_args()
from dotenv import load_dotenv
load_dotenv()
from graph import graph
pr_files = get_pr_changed_files() if os.getenv("GITHUB_BASE_REF") else None
if pr_files:
print(f"PR mode: reviewing {len(pr_files)} changed files", file=sys.stderr)
code, source_label = read_local_files(args.path, pr_files)
initial = {
"code": code,
"pragmatist": [], "purist": [], "operator": [], "synthesis": {},
}
synthesis = {}
for event in graph.stream(initial):
for node_name, state_update in event.items():
if node_name == "synthesis":
synthesis = state_update.get("synthesis", {})
if args.format == "json":
print(json.dumps(synthesis, indent=2))
else:
print(findings_to_markdown(source_label, synthesis))
if __name__ == "__main__":
main()