code-mower 0.5.0b5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_mower/__init__.py +3 -0
- code_mower/adapters/__init__.py +39 -0
- code_mower/adapters/_base.py +148 -0
- code_mower/adapters/cursor_bugbot.py +97 -0
- code_mower/adapters/gitar.py +111 -0
- code_mower/adapters/greptile.py +191 -0
- code_mower/adapters/qodo.py +140 -0
- code_mower/antigravity_cli_audit_pr.py +241 -0
- code_mower/audit_handoff_log.py +345 -0
- code_mower/audit_labeler_lib.py +311 -0
- code_mower/audit_progress.py +203 -0
- code_mower/blind_review_artifacts.py +562 -0
- code_mower/blind_review_coordinator.py +276 -0
- code_mower/bootstrap.py +524 -0
- code_mower/builder_experiment.py +539 -0
- code_mower/calibration/__init__.py +181 -0
- code_mower/calibration/arms.py +267 -0
- code_mower/calibration/auto_discovery.py +348 -0
- code_mower/calibration/commands.py +192 -0
- code_mower/calibration/context_inputs.py +199 -0
- code_mower/calibration/corpus.py +91 -0
- code_mower/calibration/evidence.py +20 -0
- code_mower/calibration/evidence_report.py +360 -0
- code_mower/calibration/identity.py +27 -0
- code_mower/calibration/metrics.py +18 -0
- code_mower/calibration/overlap.py +103 -0
- code_mower/calibration/planning.py +309 -0
- code_mower/calibration/policy.py +207 -0
- code_mower/calibration/results.py +315 -0
- code_mower/calibration/run_results.py +147 -0
- code_mower/calibration/run_status.py +64 -0
- code_mower/calibration/runner.py +360 -0
- code_mower/calibration/truth.py +188 -0
- code_mower/calibration/value_report.py +142 -0
- code_mower/checks.py +402 -0
- code_mower/claude_audit_pr.py +1126 -0
- code_mower/claude_cli_bounce.py +303 -0
- code_mower/claude_cli_environment.py +73 -0
- code_mower/clear_stale.py +374 -0
- code_mower/cli.py +537 -0
- code_mower/cloud.py +674 -0
- code_mower/cloud_client/__init__.py +173 -0
- code_mower/cloud_client/bundle.py +155 -0
- code_mower/cloud_client/doctor.py +206 -0
- code_mower/cloud_client/dogfood.py +78 -0
- code_mower/cloud_client/endpoints.py +114 -0
- code_mower/cloud_client/errors.py +7 -0
- code_mower/cloud_client/events.py +278 -0
- code_mower/cloud_client/export.py +272 -0
- code_mower/cloud_client/git_metadata.py +46 -0
- code_mower/cloud_client/manifest.py +39 -0
- code_mower/cloud_client/operations.py +448 -0
- code_mower/cloud_client/reports.py +45 -0
- code_mower/cloud_client/setup.py +205 -0
- code_mower/cloud_client/upload.py +97 -0
- code_mower/code_mower_calibration.py +598 -0
- code_mower/code_mower_context_packs.py +591 -0
- code_mower/code_mower_merge.py +227 -0
- code_mower/code_mower_telemetry.py +561 -0
- code_mower/coderabbit_cli_audit_pr.py +526 -0
- code_mower/codex_audit_env_preflight.py +220 -0
- code_mower/codex_audit_pr.py +1738 -0
- code_mower/codex_audit_schema_smoke.py +160 -0
- code_mower/codex_audit_verdict.schema.json +44 -0
- code_mower/config.py +655 -0
- code_mower/doctor.py +161 -0
- code_mower/doctor_checks/__init__.py +104 -0
- code_mower/doctor_checks/cloud.py +129 -0
- code_mower/doctor_checks/common.py +215 -0
- code_mower/doctor_checks/github.py +128 -0
- code_mower/doctor_checks/github_actions.py +11 -0
- code_mower/doctor_checks/github_actions_cost.py +99 -0
- code_mower/doctor_checks/github_actions_cost_summary.py +111 -0
- code_mower/doctor_checks/github_actions_failure_annotations.py +27 -0
- code_mower/doctor_checks/github_actions_failure_models.py +47 -0
- code_mower/doctor_checks/github_actions_failure_scan.py +200 -0
- code_mower/doctor_checks/github_actions_failure_selection.py +63 -0
- code_mower/doctor_checks/github_actions_failures.py +103 -0
- code_mower/doctor_checks/github_actions_permissions.py +55 -0
- code_mower/doctor_checks/github_api.py +79 -0
- code_mower/doctor_checks/github_branch.py +56 -0
- code_mower/doctor_checks/github_config.py +25 -0
- code_mower/doctor_checks/github_provider.py +61 -0
- code_mower/doctor_checks/github_repo.py +120 -0
- code_mower/doctor_checks/groups.py +36 -0
- code_mower/doctor_checks/models.py +96 -0
- code_mower/doctor_checks/output.py +86 -0
- code_mower/doctor_checks/presets.py +64 -0
- code_mower/doctor_checks/privacy.py +20 -0
- code_mower/doctor_checks/provider_api_model.py +138 -0
- code_mower/doctor_checks/provider_api_model_openai.py +29 -0
- code_mower/doctor_checks/provider_api_model_profiles.py +137 -0
- code_mower/doctor_checks/provider_env.py +113 -0
- code_mower/doctor_checks/provider_env_required.py +56 -0
- code_mower/doctor_checks/provider_env_tokens.py +100 -0
- code_mower/doctor_checks/provider_local_cli.py +162 -0
- code_mower/doctor_checks/provider_local_cli_commands.py +47 -0
- code_mower/doctor_checks/provider_local_cli_probe_config.py +70 -0
- code_mower/doctor_checks/provider_probe.py +20 -0
- code_mower/doctor_checks/provider_probe_auth.py +52 -0
- code_mower/doctor_checks/provider_probe_evaluation.py +109 -0
- code_mower/doctor_checks/provider_probe_json.py +45 -0
- code_mower/doctor_checks/provider_probe_remediation.py +39 -0
- code_mower/doctor_checks/providers.py +159 -0
- code_mower/doctor_checks/registry.py +69 -0
- code_mower/doctor_checks/runner.py +188 -0
- code_mower/doctor_checks/runtime.py +89 -0
- code_mower/doctor_checks/runtime_github_auth.py +148 -0
- code_mower/gemini_cli_audit_pr.py +897 -0
- code_mower/hermes_cli_audit_pr.py +436 -0
- code_mower/init.py +888 -0
- code_mower/lane_configs/__init__.py +37 -0
- code_mower/lane_configs/aider.py +32 -0
- code_mower/lane_configs/antigravity_cli.py +35 -0
- code_mower/lane_configs/claude.py +35 -0
- code_mower/lane_configs/codex.py +32 -0
- code_mower/lane_configs/devin.py +33 -0
- code_mower/lane_configs/gemini_cli.py +35 -0
- code_mower/lane_configs/hermes_cli.py +35 -0
- code_mower/lane_configs/local_llm.py +31 -0
- code_mower/local_llm_audit_pr.py +1364 -0
- code_mower/local_llm_bakeoff.py +458 -0
- code_mower/local_llm_calibration.py +441 -0
- code_mower/local_llm_profiles.py +66 -0
- code_mower/migration.py +508 -0
- code_mower/migration_install.py +292 -0
- code_mower/migration_mirror.py +392 -0
- code_mower/migration_readiness.py +237 -0
- code_mower/migration_rehearsal.py +718 -0
- code_mower/next_steps.py +441 -0
- code_mower/package.py +673 -0
- code_mower/package_content.py +444 -0
- code_mower/package_manifest.py +452 -0
- code_mower/package_paths.py +53 -0
- code_mower/package_rendering.py +90 -0
- code_mower/package_static.py +585 -0
- code_mower/prompts.py +267 -0
- code_mower/provider_registry.py +469 -0
- code_mower/provider_runners/__init__.py +60 -0
- code_mower/provider_runners/comments.py +31 -0
- code_mower/provider_runners/git.py +46 -0
- code_mower/provider_runners/github_auth.py +61 -0
- code_mower/provider_runners/github_pr.py +120 -0
- code_mower/provider_runners/process.py +58 -0
- code_mower/provider_runners/repo_paths.py +23 -0
- code_mower/provider_runners/text_schema.py +41 -0
- code_mower/provider_runners/verdict_artifacts.py +103 -0
- code_mower/provider_runners/workspace.py +57 -0
- code_mower/release_readiness.py +549 -0
- code_mower/reviewer_metrics.py +389 -0
- code_mower/saas_reviewer_labeler.py +809 -0
- code_mower/secrets.py +89 -0
- code_mower/templates/builder-experiment.example.json +55 -0
- code_mower/templates/calibration-corpus.example.json +129 -0
- code_mower/templates/calibration-corpus.json +129 -0
- code_mower/templates/code-mower.example.yml +423 -0
- code_mower/templates/context-packs.example.json +150 -0
- code_mower/templates/lane_prompts/base-audit.md +22 -0
- code_mower/templates/lane_prompts/calibration-policy.md +21 -0
- code_mower/templates/lane_prompts/context-driven-quality.md +21 -0
- code_mower/templates/lane_prompts/docs-design.md +12 -0
- code_mower/templates/lane_prompts/generic-programming.md +21 -0
- code_mower/templates/lane_prompts/operability.md +22 -0
- code_mower/templates/lane_prompts/package-runtime.md +12 -0
- code_mower/templates/lane_prompts/security-threat-model.md +22 -0
- code_mower/templates/product-support/code_mower +216 -0
- code_mower/templates/product-support/code_mower_standalone_pin.env +7 -0
- code_mower/templates/product-support/code_mower_standalone_shadow.sh +151 -0
- code_mower/templates/product-support/run_claude_audit_pr.sh +32 -0
- code_mower/templates/product-support/run_codex_audit_pr.sh +32 -0
- code_mower/templates/product-support/safe_gh_comment.py +96 -0
- code_mower/templates/providers.yml +454 -0
- code_mower/templates/reviewer-spend.example.json +28 -0
- code_mower/templates/reviewer-value-report.example.md +20 -0
- code_mower/templates/workflows/private-standalone-shadow.yml.j2 +106 -0
- code_mower/templates/workflows/review-clear-stale.yml.j2 +83 -0
- code_mower/trailer_comment_labeler.py +207 -0
- code_mower/versioning.py +32 -0
- code_mower-0.5.0b5.dist-info/METADATA +302 -0
- code_mower-0.5.0b5.dist-info/RECORD +185 -0
- code_mower-0.5.0b5.dist-info/WHEEL +5 -0
- code_mower-0.5.0b5.dist-info/entry_points.txt +2 -0
- code_mower-0.5.0b5.dist-info/licenses/LICENSE +202 -0
- code_mower-0.5.0b5.dist-info/licenses/NOTICE +10 -0
- code_mower-0.5.0b5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run a local/private LLM bakeoff against one PR.
|
|
3
|
+
|
|
4
|
+
The bakeoff is informational only: it never posts GitHub comments and never
|
|
5
|
+
changes audit labels. It exists to compare local model behavior under the same
|
|
6
|
+
PR context and reviewer prompt.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import concurrent.futures
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
import urllib.error
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
if __package__ in {None, ""}:
|
|
23
|
+
module_dir = Path(__file__).resolve().parent
|
|
24
|
+
sys.path.insert(0, str(module_dir.parent))
|
|
25
|
+
if module_dir.name == "code_mower": # pragma: no cover - extracted direct CLI.
|
|
26
|
+
from code_mower import local_llm_audit_pr, local_llm_profiles
|
|
27
|
+
else:
|
|
28
|
+
from tools import local_llm_audit_pr, local_llm_profiles
|
|
29
|
+
elif __package__ == "tools":
|
|
30
|
+
from tools import local_llm_audit_pr, local_llm_profiles
|
|
31
|
+
else: # pragma: no cover - exercised after package extraction.
|
|
32
|
+
from . import local_llm_audit_pr, local_llm_profiles
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
DEFAULT_BAKEOFF_PROFILES = (
|
|
36
|
+
"qwen3-coder-next-lmstudio",
|
|
37
|
+
"gemma4-ollama",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class BakeoffHeadChangedError(RuntimeError):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _split_profiles(value: str) -> list[str]:
|
|
46
|
+
return [item.strip() for item in value.split(",") if item.strip()]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _artifact_name(profile_id: str, suffix: str) -> str:
|
|
50
|
+
safe = "".join(ch if ch.isalnum() or ch in {"-", "_"} else "-" for ch in profile_id)
|
|
51
|
+
return f"{safe}.{suffix}"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resolve_github_token() -> str:
|
|
55
|
+
token = os.environ.get("GITHUB_TOKEN")
|
|
56
|
+
if token:
|
|
57
|
+
return token
|
|
58
|
+
try:
|
|
59
|
+
completed = subprocess.run(
|
|
60
|
+
["gh", "auth", "token"],
|
|
61
|
+
check=True,
|
|
62
|
+
capture_output=True,
|
|
63
|
+
text=True,
|
|
64
|
+
timeout=10,
|
|
65
|
+
)
|
|
66
|
+
except (
|
|
67
|
+
FileNotFoundError,
|
|
68
|
+
subprocess.CalledProcessError,
|
|
69
|
+
subprocess.TimeoutExpired,
|
|
70
|
+
):
|
|
71
|
+
return ""
|
|
72
|
+
return completed.stdout.strip()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def summarize_audit_result(
|
|
76
|
+
profile: local_llm_profiles.LocalLlmProfile,
|
|
77
|
+
result: local_llm_audit_pr.AuditResult,
|
|
78
|
+
*,
|
|
79
|
+
duration_seconds: float,
|
|
80
|
+
max_files: int | None = None,
|
|
81
|
+
max_file_bytes: int | None = None,
|
|
82
|
+
http_timeout: int | None = None,
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
blocker_findings = [
|
|
85
|
+
{"path": finding.path, "blockers": finding.blockers}
|
|
86
|
+
for finding in result.file_findings
|
|
87
|
+
if finding.blockers
|
|
88
|
+
]
|
|
89
|
+
concern_findings = [
|
|
90
|
+
{"path": finding.path, "concerns": finding.concerns}
|
|
91
|
+
for finding in result.file_findings
|
|
92
|
+
if finding.concerns and not finding.blockers
|
|
93
|
+
]
|
|
94
|
+
parse_failure_count = sum(1 for finding in result.file_findings if finding.parse_failed)
|
|
95
|
+
return {
|
|
96
|
+
"profile_id": profile.profile_id,
|
|
97
|
+
"description": profile.description,
|
|
98
|
+
"api_base": profile.api_base,
|
|
99
|
+
"model": profile.model,
|
|
100
|
+
"context_window": profile.context_window,
|
|
101
|
+
"max_files": max_files if max_files is not None else profile.max_files,
|
|
102
|
+
"max_file_bytes": (
|
|
103
|
+
max_file_bytes if max_file_bytes is not None else profile.max_file_bytes
|
|
104
|
+
),
|
|
105
|
+
"http_timeout": http_timeout if http_timeout is not None else profile.http_timeout,
|
|
106
|
+
"duration_seconds": round(duration_seconds, 3),
|
|
107
|
+
"repo": result.repo,
|
|
108
|
+
"pr_number": result.pr_number,
|
|
109
|
+
"head_sha_start": result.head_sha_start,
|
|
110
|
+
"head_sha_end": result.head_sha_end,
|
|
111
|
+
"verdict": result.verdict,
|
|
112
|
+
"files_reviewed": len(result.file_findings),
|
|
113
|
+
"blocker_file_count": len(blocker_findings),
|
|
114
|
+
"concern_file_count": len(concern_findings),
|
|
115
|
+
"parse_failure_count": parse_failure_count,
|
|
116
|
+
"json_repair_used_count": sum(1 for finding in result.file_findings if finding.json_repair_used),
|
|
117
|
+
"parse_attempts_total": sum(finding.parse_attempts for finding in result.file_findings),
|
|
118
|
+
"pr_level_blockers": list(result.pr_level_blockers),
|
|
119
|
+
"blocker_findings": blocker_findings,
|
|
120
|
+
"concern_findings": concern_findings,
|
|
121
|
+
"trailer": result.trailer,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _run_profile_bakeoff(
|
|
126
|
+
*,
|
|
127
|
+
profile: local_llm_profiles.LocalLlmProfile,
|
|
128
|
+
repo: str,
|
|
129
|
+
pr_number: int,
|
|
130
|
+
github_token: str,
|
|
131
|
+
expected_head_sha: str,
|
|
132
|
+
max_files: int | None,
|
|
133
|
+
max_file_bytes: int | None,
|
|
134
|
+
http_timeout: int | None,
|
|
135
|
+
api_key: str | None,
|
|
136
|
+
json_repair_retries: int | None,
|
|
137
|
+
repo_path: Path | None,
|
|
138
|
+
base_ref: str,
|
|
139
|
+
allow_historical_head: bool,
|
|
140
|
+
) -> tuple[str, local_llm_audit_pr.AuditResult, dict[str, Any]]:
|
|
141
|
+
started = time.monotonic()
|
|
142
|
+
config = local_llm_audit_pr.AuditConfig(
|
|
143
|
+
github_token=github_token,
|
|
144
|
+
api_base=profile.api_base,
|
|
145
|
+
model=profile.model,
|
|
146
|
+
api_key=api_key or os.environ.get("LOCAL_LLM_API_KEY") or profile.api_key,
|
|
147
|
+
http_timeout=http_timeout if http_timeout is not None else profile.http_timeout,
|
|
148
|
+
max_file_bytes=max_file_bytes if max_file_bytes is not None else profile.max_file_bytes,
|
|
149
|
+
max_files=max_files if max_files is not None else profile.max_files,
|
|
150
|
+
profile_id=profile.profile_id,
|
|
151
|
+
context_window=profile.context_window,
|
|
152
|
+
json_repair_retries=(
|
|
153
|
+
json_repair_retries
|
|
154
|
+
if json_repair_retries is not None
|
|
155
|
+
else 1
|
|
156
|
+
),
|
|
157
|
+
repo_path=repo_path,
|
|
158
|
+
base_ref=base_ref,
|
|
159
|
+
allow_historical_head=allow_historical_head,
|
|
160
|
+
dry_run=True,
|
|
161
|
+
)
|
|
162
|
+
result = local_llm_audit_pr.audit_pr(config, repo, pr_number)
|
|
163
|
+
duration = time.monotonic() - started
|
|
164
|
+
if (
|
|
165
|
+
result.head_sha_start != expected_head_sha
|
|
166
|
+
or result.head_sha_end != expected_head_sha
|
|
167
|
+
):
|
|
168
|
+
raise BakeoffHeadChangedError(
|
|
169
|
+
"PR head changed during bakeoff; "
|
|
170
|
+
f"expected {expected_head_sha}, "
|
|
171
|
+
f"got start={result.head_sha_start} end={result.head_sha_end}. "
|
|
172
|
+
"Discard this comparison and rerun on the current head."
|
|
173
|
+
)
|
|
174
|
+
summary = summarize_audit_result(
|
|
175
|
+
profile,
|
|
176
|
+
result,
|
|
177
|
+
duration_seconds=duration,
|
|
178
|
+
max_files=max_files,
|
|
179
|
+
max_file_bytes=max_file_bytes,
|
|
180
|
+
http_timeout=http_timeout,
|
|
181
|
+
)
|
|
182
|
+
return profile.profile_id, result, summary
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def run_bakeoff(
|
|
186
|
+
*,
|
|
187
|
+
repo: str,
|
|
188
|
+
pr_number: int,
|
|
189
|
+
github_token: str,
|
|
190
|
+
profile_ids: list[str],
|
|
191
|
+
expected_head_sha: str | None = None,
|
|
192
|
+
max_files: int | None = None,
|
|
193
|
+
max_file_bytes: int | None = None,
|
|
194
|
+
http_timeout: int | None = None,
|
|
195
|
+
api_key: str | None = None,
|
|
196
|
+
json_repair_retries: int | None = None,
|
|
197
|
+
output_dir: Path | None = None,
|
|
198
|
+
jobs: int = 1,
|
|
199
|
+
repo_path: Path | None = None,
|
|
200
|
+
base_ref: str = local_llm_audit_pr.DEFAULT_BASE_REF,
|
|
201
|
+
allow_historical_head: bool = False,
|
|
202
|
+
) -> dict[str, Any]:
|
|
203
|
+
if not profile_ids:
|
|
204
|
+
raise ValueError("no local LLM profiles selected; pass --profiles")
|
|
205
|
+
seen_profile_ids: set[str] = set()
|
|
206
|
+
for profile_id in profile_ids:
|
|
207
|
+
if profile_id in seen_profile_ids:
|
|
208
|
+
raise ValueError(f"duplicate local LLM profile id: {profile_id}")
|
|
209
|
+
seen_profile_ids.add(profile_id)
|
|
210
|
+
profiles = [local_llm_profiles.get_profile(profile_id) for profile_id in profile_ids]
|
|
211
|
+
|
|
212
|
+
runs: list[dict[str, Any]] = []
|
|
213
|
+
output_paths: dict[str, str] = {}
|
|
214
|
+
if output_dir is not None:
|
|
215
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
216
|
+
|
|
217
|
+
pr_meta = local_llm_audit_pr.fetch_pull_request(repo, pr_number, token=github_token)
|
|
218
|
+
pr_head_sha = str(pr_meta["head"]["sha"])
|
|
219
|
+
current_head_sha = (
|
|
220
|
+
local_llm_audit_pr._local_head_sha(repo_path.expanduser().resolve())
|
|
221
|
+
if repo_path is not None
|
|
222
|
+
else pr_head_sha
|
|
223
|
+
)
|
|
224
|
+
if (
|
|
225
|
+
repo_path is not None
|
|
226
|
+
and not allow_historical_head
|
|
227
|
+
and current_head_sha.lower() != pr_head_sha.lower()
|
|
228
|
+
):
|
|
229
|
+
raise BakeoffHeadChangedError(
|
|
230
|
+
"local checkout is not at the current PR head; pass "
|
|
231
|
+
"--allow-historical-head for archived calibration runs. "
|
|
232
|
+
f"local={current_head_sha} current_pr={pr_head_sha}."
|
|
233
|
+
)
|
|
234
|
+
normalized_expected_head_sha = expected_head_sha.strip().lower() if expected_head_sha else ""
|
|
235
|
+
normalized_current_head_sha = str(current_head_sha).strip().lower()
|
|
236
|
+
if normalized_expected_head_sha and normalized_expected_head_sha != normalized_current_head_sha:
|
|
237
|
+
raise BakeoffHeadChangedError(
|
|
238
|
+
"PR head does not match calibration corpus; "
|
|
239
|
+
f"expected {expected_head_sha}, current={current_head_sha}. "
|
|
240
|
+
"Refresh the corpus or rerun against the pinned head before comparing results."
|
|
241
|
+
)
|
|
242
|
+
expected_head_sha = current_head_sha
|
|
243
|
+
jobs = max(1, min(jobs, len(profiles)))
|
|
244
|
+
|
|
245
|
+
completed: dict[str, tuple[local_llm_audit_pr.AuditResult, dict[str, Any]]] = {}
|
|
246
|
+
if jobs == 1:
|
|
247
|
+
for profile in profiles:
|
|
248
|
+
profile_id, result, summary = _run_profile_bakeoff(
|
|
249
|
+
profile=profile,
|
|
250
|
+
repo=repo,
|
|
251
|
+
pr_number=pr_number,
|
|
252
|
+
github_token=github_token,
|
|
253
|
+
expected_head_sha=expected_head_sha,
|
|
254
|
+
max_files=max_files,
|
|
255
|
+
max_file_bytes=max_file_bytes,
|
|
256
|
+
http_timeout=http_timeout,
|
|
257
|
+
api_key=api_key,
|
|
258
|
+
json_repair_retries=json_repair_retries,
|
|
259
|
+
repo_path=repo_path,
|
|
260
|
+
base_ref=base_ref,
|
|
261
|
+
allow_historical_head=allow_historical_head,
|
|
262
|
+
)
|
|
263
|
+
completed[profile_id] = (result, summary)
|
|
264
|
+
else:
|
|
265
|
+
executor = concurrent.futures.ThreadPoolExecutor(max_workers=jobs)
|
|
266
|
+
failure: Exception | None = None
|
|
267
|
+
try:
|
|
268
|
+
future_by_profile = {
|
|
269
|
+
executor.submit(
|
|
270
|
+
_run_profile_bakeoff,
|
|
271
|
+
profile=profile,
|
|
272
|
+
repo=repo,
|
|
273
|
+
pr_number=pr_number,
|
|
274
|
+
github_token=github_token,
|
|
275
|
+
expected_head_sha=expected_head_sha,
|
|
276
|
+
max_files=max_files,
|
|
277
|
+
max_file_bytes=max_file_bytes,
|
|
278
|
+
http_timeout=http_timeout,
|
|
279
|
+
api_key=api_key,
|
|
280
|
+
json_repair_retries=json_repair_retries,
|
|
281
|
+
repo_path=repo_path,
|
|
282
|
+
base_ref=base_ref,
|
|
283
|
+
allow_historical_head=allow_historical_head,
|
|
284
|
+
): profile.profile_id
|
|
285
|
+
for profile in profiles
|
|
286
|
+
}
|
|
287
|
+
for future in concurrent.futures.as_completed(future_by_profile):
|
|
288
|
+
try:
|
|
289
|
+
profile_id, result, summary = future.result()
|
|
290
|
+
except Exception as exc:
|
|
291
|
+
for pending in future_by_profile:
|
|
292
|
+
pending.cancel()
|
|
293
|
+
failure = exc
|
|
294
|
+
break
|
|
295
|
+
completed[profile_id] = (result, summary)
|
|
296
|
+
finally:
|
|
297
|
+
executor.shutdown(wait=True, cancel_futures=failure is not None)
|
|
298
|
+
if failure is not None:
|
|
299
|
+
raise failure
|
|
300
|
+
|
|
301
|
+
for profile in profiles:
|
|
302
|
+
result, summary = completed[profile.profile_id]
|
|
303
|
+
runs.append(summary)
|
|
304
|
+
if output_dir is not None:
|
|
305
|
+
comment_path = output_dir / _artifact_name(profile.profile_id, "comment.md")
|
|
306
|
+
comment_path.write_text(result.comment_body, encoding="utf-8")
|
|
307
|
+
output_paths[f"{profile.profile_id}:comment"] = str(comment_path)
|
|
308
|
+
|
|
309
|
+
payload = {
|
|
310
|
+
"mode": "local-llm-bakeoff",
|
|
311
|
+
"repo": repo,
|
|
312
|
+
"pr_number": pr_number,
|
|
313
|
+
"head_sha": expected_head_sha,
|
|
314
|
+
"pr_head_sha": pr_head_sha,
|
|
315
|
+
"diff_source": "local_checkout" if repo_path is not None else "github_pr",
|
|
316
|
+
"base_ref": base_ref if repo_path is not None else None,
|
|
317
|
+
"profiles": profile_ids,
|
|
318
|
+
"jobs": jobs,
|
|
319
|
+
"runs": runs,
|
|
320
|
+
}
|
|
321
|
+
if output_dir is not None:
|
|
322
|
+
summary_path = output_dir / "summary.json"
|
|
323
|
+
payload["output_paths"] = output_paths
|
|
324
|
+
output_paths["summary"] = str(summary_path)
|
|
325
|
+
summary_path.write_text(
|
|
326
|
+
json.dumps(payload, indent=2, sort_keys=True) + "\n",
|
|
327
|
+
encoding="utf-8",
|
|
328
|
+
)
|
|
329
|
+
return payload
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def render_bakeoff_text(payload: dict[str, Any]) -> str:
|
|
333
|
+
lines = [
|
|
334
|
+
f"Local LLM bakeoff for {payload['repo']}#{payload['pr_number']}",
|
|
335
|
+
f"jobs: {payload.get('jobs', 1)}",
|
|
336
|
+
"",
|
|
337
|
+
]
|
|
338
|
+
for run in payload["runs"]:
|
|
339
|
+
lines.extend(
|
|
340
|
+
[
|
|
341
|
+
f"- {run['profile_id']} ({run['model']})",
|
|
342
|
+
f" verdict: {run['verdict']}",
|
|
343
|
+
f" files: {run['files_reviewed']}, blockers: {run['blocker_file_count']}, "
|
|
344
|
+
f"concerns: {run['concern_file_count']}",
|
|
345
|
+
f" parse failures: {run['parse_failure_count']}, "
|
|
346
|
+
f"json retry used: {run['json_repair_used_count']}",
|
|
347
|
+
f" runtime: {run['duration_seconds']}s",
|
|
348
|
+
]
|
|
349
|
+
)
|
|
350
|
+
if payload.get("output_paths"):
|
|
351
|
+
lines.extend(["", "Artifacts:"])
|
|
352
|
+
for name, path in sorted(payload["output_paths"].items()):
|
|
353
|
+
lines.append(f"- {name}: {path}")
|
|
354
|
+
return "\n".join(lines) + "\n"
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def main(argv: list[str] | None = None) -> int:
|
|
358
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
359
|
+
parser.add_argument("--repo", required=True, help="owner/repo")
|
|
360
|
+
parser.add_argument("--pr", type=int, required=True, help="PR number")
|
|
361
|
+
parser.add_argument(
|
|
362
|
+
"--profiles",
|
|
363
|
+
default=",".join(DEFAULT_BAKEOFF_PROFILES),
|
|
364
|
+
help="Comma-separated local LLM profile ids.",
|
|
365
|
+
)
|
|
366
|
+
parser.add_argument("--max-files", type=int, default=None)
|
|
367
|
+
parser.add_argument("--max-file-bytes", type=int, default=None)
|
|
368
|
+
parser.add_argument("--http-timeout", type=int, default=None)
|
|
369
|
+
parser.add_argument("--api-key", default=None)
|
|
370
|
+
parser.add_argument(
|
|
371
|
+
"--expected-head-sha",
|
|
372
|
+
default=None,
|
|
373
|
+
help="Fail if the PR current head does not match this corpus-pinned SHA.",
|
|
374
|
+
)
|
|
375
|
+
parser.add_argument(
|
|
376
|
+
"--repo-path",
|
|
377
|
+
type=Path,
|
|
378
|
+
default=None,
|
|
379
|
+
help="optional local checkout to review for archived calibration heads",
|
|
380
|
+
)
|
|
381
|
+
parser.add_argument("--base-ref", default=local_llm_audit_pr.DEFAULT_BASE_REF)
|
|
382
|
+
parser.add_argument(
|
|
383
|
+
"--allow-historical-head",
|
|
384
|
+
action="store_true",
|
|
385
|
+
help="allow --repo-path HEAD to differ from the current GitHub PR head",
|
|
386
|
+
)
|
|
387
|
+
parser.add_argument("--json-repair-retries", type=int, default=None)
|
|
388
|
+
parser.add_argument("--output-dir", type=Path, default=None)
|
|
389
|
+
parser.add_argument(
|
|
390
|
+
"--jobs",
|
|
391
|
+
type=int,
|
|
392
|
+
default=None,
|
|
393
|
+
help="Number of profiles to review concurrently.",
|
|
394
|
+
)
|
|
395
|
+
parser.add_argument("--json", action="store_true")
|
|
396
|
+
args = parser.parse_args(argv)
|
|
397
|
+
|
|
398
|
+
token = resolve_github_token()
|
|
399
|
+
if not token:
|
|
400
|
+
print(
|
|
401
|
+
"error: set GITHUB_TOKEN or authenticate gh so `gh auth token` works",
|
|
402
|
+
file=sys.stderr,
|
|
403
|
+
)
|
|
404
|
+
return 1
|
|
405
|
+
|
|
406
|
+
try:
|
|
407
|
+
jobs = (
|
|
408
|
+
args.jobs
|
|
409
|
+
if args.jobs is not None
|
|
410
|
+
else int(os.environ.get("LOCAL_LLM_BAKEOFF_JOBS", "1"))
|
|
411
|
+
)
|
|
412
|
+
payload = run_bakeoff(
|
|
413
|
+
repo=args.repo,
|
|
414
|
+
pr_number=args.pr,
|
|
415
|
+
github_token=token,
|
|
416
|
+
profile_ids=_split_profiles(args.profiles),
|
|
417
|
+
expected_head_sha=args.expected_head_sha,
|
|
418
|
+
max_files=args.max_files,
|
|
419
|
+
max_file_bytes=args.max_file_bytes,
|
|
420
|
+
http_timeout=args.http_timeout,
|
|
421
|
+
api_key=args.api_key,
|
|
422
|
+
json_repair_retries=args.json_repair_retries,
|
|
423
|
+
output_dir=args.output_dir,
|
|
424
|
+
jobs=jobs,
|
|
425
|
+
repo_path=args.repo_path,
|
|
426
|
+
base_ref=args.base_ref,
|
|
427
|
+
allow_historical_head=args.allow_historical_head,
|
|
428
|
+
)
|
|
429
|
+
except (KeyError, ValueError) as exc:
|
|
430
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
431
|
+
return 1
|
|
432
|
+
except BakeoffHeadChangedError as exc:
|
|
433
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
434
|
+
return 2
|
|
435
|
+
except urllib.error.HTTPError as exc:
|
|
436
|
+
print(f"error: GitHub/API HTTP {exc.code} - {exc.reason}", file=sys.stderr)
|
|
437
|
+
return 1
|
|
438
|
+
except urllib.error.URLError as exc:
|
|
439
|
+
print(f"error: network - {exc}", file=sys.stderr)
|
|
440
|
+
return 1
|
|
441
|
+
except (
|
|
442
|
+
OSError,
|
|
443
|
+
RuntimeError,
|
|
444
|
+
subprocess.CalledProcessError,
|
|
445
|
+
subprocess.TimeoutExpired,
|
|
446
|
+
) as exc:
|
|
447
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
448
|
+
return 1
|
|
449
|
+
|
|
450
|
+
if args.json:
|
|
451
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
452
|
+
else:
|
|
453
|
+
print(render_bakeoff_text(payload), end="")
|
|
454
|
+
return 0
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
if __name__ == "__main__":
|
|
458
|
+
raise SystemExit(main())
|