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,526 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Capture CodeRabbit CLI review output as Code Mower calibration evidence."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
import urllib.error
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Mapping, Sequence
|
|
16
|
+
|
|
17
|
+
if __package__ in {None, ""}:
|
|
18
|
+
module_dir = Path(__file__).resolve().parent
|
|
19
|
+
sys.path.insert(0, str(module_dir.parent))
|
|
20
|
+
if module_dir.name == "code_mower": # pragma: no cover - extracted direct CLI.
|
|
21
|
+
from code_mower.provider_runners import (
|
|
22
|
+
build_allowlisted_child_env,
|
|
23
|
+
fetch_pull_request as _fetch_pull_request,
|
|
24
|
+
local_head_sha as _local_head_sha,
|
|
25
|
+
ProviderWorkspaceError,
|
|
26
|
+
resolve_github_token_from_env_or_gh,
|
|
27
|
+
verify_checkout_at_head as _verify_checkout_at_head,
|
|
28
|
+
)
|
|
29
|
+
else:
|
|
30
|
+
from tools.provider_runners import (
|
|
31
|
+
build_allowlisted_child_env,
|
|
32
|
+
fetch_pull_request as _fetch_pull_request,
|
|
33
|
+
local_head_sha as _local_head_sha,
|
|
34
|
+
ProviderWorkspaceError,
|
|
35
|
+
resolve_github_token_from_env_or_gh,
|
|
36
|
+
verify_checkout_at_head as _verify_checkout_at_head,
|
|
37
|
+
)
|
|
38
|
+
elif __package__ == "tools":
|
|
39
|
+
from tools.provider_runners import (
|
|
40
|
+
build_allowlisted_child_env,
|
|
41
|
+
fetch_pull_request as _fetch_pull_request,
|
|
42
|
+
local_head_sha as _local_head_sha,
|
|
43
|
+
ProviderWorkspaceError,
|
|
44
|
+
resolve_github_token_from_env_or_gh,
|
|
45
|
+
verify_checkout_at_head as _verify_checkout_at_head,
|
|
46
|
+
)
|
|
47
|
+
else: # pragma: no cover - exercised after package extraction.
|
|
48
|
+
from .provider_runners import (
|
|
49
|
+
build_allowlisted_child_env,
|
|
50
|
+
fetch_pull_request as _fetch_pull_request,
|
|
51
|
+
local_head_sha as _local_head_sha,
|
|
52
|
+
ProviderWorkspaceError,
|
|
53
|
+
resolve_github_token_from_env_or_gh,
|
|
54
|
+
verify_checkout_at_head as _verify_checkout_at_head,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
DEFAULT_CODERABBIT_COMMAND = "coderabbit"
|
|
59
|
+
DEFAULT_BASE_REF = "origin/main"
|
|
60
|
+
DEFAULT_REVIEW_TYPE = "committed"
|
|
61
|
+
DEFAULT_TIMEOUT_SECONDS = 900
|
|
62
|
+
RESPONSE_JSON_RE = re.compile(r"```(?:json)?\s*(.*?)\s*```", re.DOTALL | re.IGNORECASE)
|
|
63
|
+
CODERABBIT_ENV_ALLOWLIST = (
|
|
64
|
+
"CODERABBIT_API_KEY",
|
|
65
|
+
"CODERABBIT_API_BASE",
|
|
66
|
+
"PATH",
|
|
67
|
+
"HOME",
|
|
68
|
+
"USER",
|
|
69
|
+
"LOGNAME",
|
|
70
|
+
"TMPDIR",
|
|
71
|
+
"TEMP",
|
|
72
|
+
"TMP",
|
|
73
|
+
"XDG_CONFIG_HOME",
|
|
74
|
+
"XDG_CACHE_HOME",
|
|
75
|
+
"XDG_STATE_HOME",
|
|
76
|
+
"GIT_CONFIG_GLOBAL",
|
|
77
|
+
"GIT_CONFIG_NOSYSTEM",
|
|
78
|
+
"GIT_TERMINAL_PROMPT",
|
|
79
|
+
"LANG",
|
|
80
|
+
"LC_ALL",
|
|
81
|
+
"LC_CTYPE",
|
|
82
|
+
"SSL_CERT_FILE",
|
|
83
|
+
"REQUESTS_CA_BUNDLE",
|
|
84
|
+
"NODE_EXTRA_CA_CERTS",
|
|
85
|
+
"HTTPS_PROXY",
|
|
86
|
+
"HTTP_PROXY",
|
|
87
|
+
"NO_PROXY",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class CoderabbitCliHeadChangedError(RuntimeError):
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class CoderabbitCliWorkspaceError(ProviderWorkspaceError):
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def fetch_pull_request(repo: str, pr_number: int, *, token: str) -> Mapping[str, Any]:
|
|
100
|
+
payload = _fetch_pull_request(repo, pr_number, token=token)
|
|
101
|
+
if not isinstance(payload, Mapping):
|
|
102
|
+
raise ValueError("GitHub pull request response was not an object")
|
|
103
|
+
return payload
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def resolve_github_token() -> str:
|
|
107
|
+
return resolve_github_token_from_env_or_gh()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def build_coderabbit_child_env() -> dict[str, str]:
|
|
111
|
+
return build_allowlisted_child_env(CODERABBIT_ENV_ALLOWLIST)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def verify_checkout(
|
|
115
|
+
repo_path: Path,
|
|
116
|
+
*,
|
|
117
|
+
expected_head_sha: str,
|
|
118
|
+
allow_dirty: bool = False,
|
|
119
|
+
) -> dict[str, Any]:
|
|
120
|
+
try:
|
|
121
|
+
return _verify_checkout_at_head(
|
|
122
|
+
repo_path,
|
|
123
|
+
expected_head_sha=expected_head_sha,
|
|
124
|
+
allow_dirty=allow_dirty,
|
|
125
|
+
purpose="CodeRabbit CLI calibration",
|
|
126
|
+
dirty_remediation=(
|
|
127
|
+
"commit/stash them or pass --allow-dirty for an explicitly exploratory run"
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
except ProviderWorkspaceError as exc:
|
|
131
|
+
raise CoderabbitCliWorkspaceError(str(exc)) from exc
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _unwrap_fenced_json(text: str) -> str:
|
|
135
|
+
stripped = text.strip()
|
|
136
|
+
match = RESPONSE_JSON_RE.fullmatch(stripped)
|
|
137
|
+
if match:
|
|
138
|
+
return match.group(1).strip()
|
|
139
|
+
return stripped
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _json_fragment(text: str) -> str | None:
|
|
143
|
+
candidates = [
|
|
144
|
+
(text.find("{"), "{", "}"),
|
|
145
|
+
(text.find("["), "[", "]"),
|
|
146
|
+
]
|
|
147
|
+
candidates = [candidate for candidate in candidates if candidate[0] >= 0]
|
|
148
|
+
if not candidates:
|
|
149
|
+
return None
|
|
150
|
+
start, _, close_char = min(candidates, key=lambda item: item[0])
|
|
151
|
+
end = text.rfind(close_char)
|
|
152
|
+
if end <= start:
|
|
153
|
+
return None
|
|
154
|
+
return text[start : end + 1]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def parse_agent_json(text: str) -> Any | None:
|
|
158
|
+
stripped = _unwrap_fenced_json(text)
|
|
159
|
+
try:
|
|
160
|
+
payload = json.loads(stripped)
|
|
161
|
+
except json.JSONDecodeError:
|
|
162
|
+
fragment = _json_fragment(stripped)
|
|
163
|
+
if fragment is None:
|
|
164
|
+
return None
|
|
165
|
+
try:
|
|
166
|
+
payload = json.loads(fragment)
|
|
167
|
+
except json.JSONDecodeError:
|
|
168
|
+
return None
|
|
169
|
+
if isinstance(payload, Mapping):
|
|
170
|
+
return dict(payload)
|
|
171
|
+
if isinstance(payload, list):
|
|
172
|
+
return payload
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def parse_agent_events(text: str) -> list[dict[str, Any]]:
|
|
177
|
+
events: list[dict[str, Any]] = []
|
|
178
|
+
for line in text.splitlines():
|
|
179
|
+
stripped = line.strip()
|
|
180
|
+
if not stripped:
|
|
181
|
+
continue
|
|
182
|
+
try:
|
|
183
|
+
payload = json.loads(stripped)
|
|
184
|
+
except json.JSONDecodeError:
|
|
185
|
+
continue
|
|
186
|
+
if isinstance(payload, Mapping):
|
|
187
|
+
events.append(dict(payload))
|
|
188
|
+
return events
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _iter_findings(value: Any) -> list[Mapping[str, Any]]:
|
|
192
|
+
if isinstance(value, list):
|
|
193
|
+
return [item for item in value if isinstance(item, Mapping)]
|
|
194
|
+
if not isinstance(value, Mapping):
|
|
195
|
+
return []
|
|
196
|
+
if str(value.get("type") or "") == "finding":
|
|
197
|
+
return [value]
|
|
198
|
+
for key in ("findings", "issues", "comments"):
|
|
199
|
+
nested = value.get(key)
|
|
200
|
+
if isinstance(nested, list):
|
|
201
|
+
return [item for item in nested if isinstance(item, Mapping)]
|
|
202
|
+
for key in ("result", "review", "data", "response"):
|
|
203
|
+
nested = value.get(key)
|
|
204
|
+
nested_findings = _iter_findings(nested)
|
|
205
|
+
if nested_findings:
|
|
206
|
+
return nested_findings
|
|
207
|
+
return []
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _complete_event(events: Sequence[Mapping[str, Any]]) -> Mapping[str, Any] | None:
|
|
211
|
+
for event in reversed(events):
|
|
212
|
+
if str(event.get("type") or "") == "complete":
|
|
213
|
+
return event
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _normalize_findings(payload: Any | None) -> list[dict[str, Any]]:
|
|
218
|
+
findings = _iter_findings(payload)
|
|
219
|
+
normalized: list[dict[str, Any]] = []
|
|
220
|
+
for finding in findings:
|
|
221
|
+
try:
|
|
222
|
+
line = int(
|
|
223
|
+
finding.get("line")
|
|
224
|
+
or finding.get("start_line")
|
|
225
|
+
or finding.get("line_number")
|
|
226
|
+
or 0
|
|
227
|
+
)
|
|
228
|
+
except (TypeError, ValueError):
|
|
229
|
+
line = 0
|
|
230
|
+
normalized.append(
|
|
231
|
+
{
|
|
232
|
+
"severity": str(
|
|
233
|
+
finding.get("severity") or finding.get("level") or ""
|
|
234
|
+
).strip(),
|
|
235
|
+
"title": str(
|
|
236
|
+
finding.get("title")
|
|
237
|
+
or finding.get("rule")
|
|
238
|
+
or finding.get("message")
|
|
239
|
+
or finding.get("fileName")
|
|
240
|
+
or ""
|
|
241
|
+
).strip(),
|
|
242
|
+
"file": str(
|
|
243
|
+
finding.get("file")
|
|
244
|
+
or finding.get("path")
|
|
245
|
+
or finding.get("filename")
|
|
246
|
+
or finding.get("fileName")
|
|
247
|
+
or ""
|
|
248
|
+
).strip(),
|
|
249
|
+
"line": line,
|
|
250
|
+
"detail": str(
|
|
251
|
+
finding.get("detail")
|
|
252
|
+
or finding.get("body")
|
|
253
|
+
or finding.get("description")
|
|
254
|
+
or finding.get("message")
|
|
255
|
+
or finding.get("codegenInstructions")
|
|
256
|
+
or ""
|
|
257
|
+
).strip(),
|
|
258
|
+
}
|
|
259
|
+
)
|
|
260
|
+
return normalized
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def run_coderabbit_cli_audit(
|
|
264
|
+
*,
|
|
265
|
+
repo: str,
|
|
266
|
+
pr_number: int,
|
|
267
|
+
github_token: str,
|
|
268
|
+
repo_path: Path,
|
|
269
|
+
command: str = DEFAULT_CODERABBIT_COMMAND,
|
|
270
|
+
expected_head_sha: str | None = None,
|
|
271
|
+
base_ref: str = DEFAULT_BASE_REF,
|
|
272
|
+
review_type: str = DEFAULT_REVIEW_TYPE,
|
|
273
|
+
config_files: Sequence[Path] = (),
|
|
274
|
+
timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS,
|
|
275
|
+
output_dir: Path | None = None,
|
|
276
|
+
allow_dirty: bool = False,
|
|
277
|
+
allow_historical_head: bool = False,
|
|
278
|
+
) -> dict[str, Any]:
|
|
279
|
+
repo_path = repo_path.expanduser().resolve()
|
|
280
|
+
pr_meta = fetch_pull_request(repo, pr_number, token=github_token)
|
|
281
|
+
pr_head_sha = str(pr_meta.get("head", {}).get("sha") or "")
|
|
282
|
+
if not pr_head_sha:
|
|
283
|
+
raise ValueError("GitHub pull request response did not include head.sha")
|
|
284
|
+
head_sha = _local_head_sha(repo_path) if allow_historical_head else pr_head_sha
|
|
285
|
+
normalized_expected = str(expected_head_sha or "").strip().lower()
|
|
286
|
+
if normalized_expected and normalized_expected != head_sha.lower():
|
|
287
|
+
raise CoderabbitCliHeadChangedError(
|
|
288
|
+
"review head does not match calibration corpus; "
|
|
289
|
+
f"expected {expected_head_sha}, current={head_sha}."
|
|
290
|
+
)
|
|
291
|
+
checkout = verify_checkout(
|
|
292
|
+
repo_path,
|
|
293
|
+
expected_head_sha=head_sha,
|
|
294
|
+
allow_dirty=allow_dirty,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
cli_args = [
|
|
298
|
+
command,
|
|
299
|
+
"review",
|
|
300
|
+
"--agent",
|
|
301
|
+
"--type",
|
|
302
|
+
review_type,
|
|
303
|
+
"--base",
|
|
304
|
+
base_ref,
|
|
305
|
+
"--dir",
|
|
306
|
+
str(repo_path),
|
|
307
|
+
]
|
|
308
|
+
for config_file in config_files:
|
|
309
|
+
cli_args.extend(["--config", str(config_file)])
|
|
310
|
+
|
|
311
|
+
started = time.monotonic()
|
|
312
|
+
completed = subprocess.run(
|
|
313
|
+
cli_args,
|
|
314
|
+
capture_output=True,
|
|
315
|
+
cwd=repo_path,
|
|
316
|
+
env=build_coderabbit_child_env(),
|
|
317
|
+
text=True,
|
|
318
|
+
check=False,
|
|
319
|
+
timeout=timeout_seconds,
|
|
320
|
+
)
|
|
321
|
+
duration_seconds = time.monotonic() - started
|
|
322
|
+
|
|
323
|
+
parsed_output = parse_agent_json(completed.stdout)
|
|
324
|
+
parsed_events = [] if parsed_output is not None else parse_agent_events(completed.stdout)
|
|
325
|
+
if parsed_output is not None:
|
|
326
|
+
parse_status = "json"
|
|
327
|
+
normalized_findings = _normalize_findings(parsed_output)
|
|
328
|
+
elif parsed_events:
|
|
329
|
+
parse_status = "ndjson"
|
|
330
|
+
normalized_findings = [
|
|
331
|
+
finding
|
|
332
|
+
for event in parsed_events
|
|
333
|
+
for finding in _normalize_findings(event)
|
|
334
|
+
]
|
|
335
|
+
else:
|
|
336
|
+
parse_status = "raw"
|
|
337
|
+
normalized_findings = []
|
|
338
|
+
final_event = _complete_event(parsed_events)
|
|
339
|
+
finding_count = len(normalized_findings)
|
|
340
|
+
finding_count_source = "parsed_findings"
|
|
341
|
+
if final_event is not None:
|
|
342
|
+
try:
|
|
343
|
+
final_finding_count = int(final_event.get("findings") or 0)
|
|
344
|
+
if final_finding_count > finding_count or not normalized_findings:
|
|
345
|
+
finding_count = final_finding_count
|
|
346
|
+
finding_count_source = "final_event"
|
|
347
|
+
except (TypeError, ValueError):
|
|
348
|
+
if not finding_count:
|
|
349
|
+
finding_count = 0
|
|
350
|
+
head_after = ""
|
|
351
|
+
if allow_historical_head:
|
|
352
|
+
head_after = _local_head_sha(repo_path)
|
|
353
|
+
if head_after == head_sha:
|
|
354
|
+
head_check = {"status": "pass", "source": "local_checkout"}
|
|
355
|
+
else:
|
|
356
|
+
head_check = {
|
|
357
|
+
"status": "changed",
|
|
358
|
+
"source": "local_checkout",
|
|
359
|
+
"message": (
|
|
360
|
+
"local checkout head changed during CodeRabbit CLI audit; "
|
|
361
|
+
"discard this run and rerun."
|
|
362
|
+
),
|
|
363
|
+
}
|
|
364
|
+
else:
|
|
365
|
+
try:
|
|
366
|
+
head_after_meta = fetch_pull_request(repo, pr_number, token=github_token)
|
|
367
|
+
head_after = str(head_after_meta.get("head", {}).get("sha") or "")
|
|
368
|
+
if not head_after:
|
|
369
|
+
head_check = {
|
|
370
|
+
"status": "error",
|
|
371
|
+
"message": "post-run PR response did not include head.sha",
|
|
372
|
+
}
|
|
373
|
+
elif head_after == head_sha:
|
|
374
|
+
head_check = {"status": "pass"}
|
|
375
|
+
else:
|
|
376
|
+
head_check = {
|
|
377
|
+
"status": "changed",
|
|
378
|
+
"message": "PR head changed during CodeRabbit CLI audit; discard this run and rerun.",
|
|
379
|
+
}
|
|
380
|
+
except (OSError, ValueError, urllib.error.URLError) as exc:
|
|
381
|
+
head_check = {
|
|
382
|
+
"status": "error",
|
|
383
|
+
"message": f"post-run PR head check failed: {exc}",
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
payload: dict[str, Any] = {
|
|
387
|
+
"mode": "coderabbit-cli-audit",
|
|
388
|
+
"repo": repo,
|
|
389
|
+
"pr_number": pr_number,
|
|
390
|
+
"head_sha": head_sha,
|
|
391
|
+
"head_sha_end": head_after,
|
|
392
|
+
"pr_head_sha": pr_head_sha,
|
|
393
|
+
"command": command,
|
|
394
|
+
"base_ref": base_ref,
|
|
395
|
+
"review_type": review_type,
|
|
396
|
+
"returncode": completed.returncode,
|
|
397
|
+
"duration_seconds": round(duration_seconds, 3),
|
|
398
|
+
"checkout": checkout,
|
|
399
|
+
"parse_status": parse_status,
|
|
400
|
+
"parsed_output": parsed_output,
|
|
401
|
+
"parsed_events": parsed_events,
|
|
402
|
+
"final_event": dict(final_event) if final_event is not None else None,
|
|
403
|
+
"findings": normalized_findings,
|
|
404
|
+
"finding_count": finding_count,
|
|
405
|
+
"finding_count_source": finding_count_source,
|
|
406
|
+
"head_check": head_check,
|
|
407
|
+
"stdout": completed.stdout,
|
|
408
|
+
"stderr": completed.stderr,
|
|
409
|
+
}
|
|
410
|
+
if output_dir is not None:
|
|
411
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
412
|
+
paths = {
|
|
413
|
+
"command": output_dir / "coderabbit-cli.command.json",
|
|
414
|
+
"stdout": output_dir / "coderabbit-cli.stdout.txt",
|
|
415
|
+
"stderr": output_dir / "coderabbit-cli.stderr.txt",
|
|
416
|
+
"summary": output_dir / "coderabbit-cli.summary.json",
|
|
417
|
+
}
|
|
418
|
+
paths["command"].write_text(
|
|
419
|
+
json.dumps({"args": cli_args}, indent=2, sort_keys=True) + "\n",
|
|
420
|
+
encoding="utf-8",
|
|
421
|
+
)
|
|
422
|
+
paths["stdout"].write_text(completed.stdout, encoding="utf-8")
|
|
423
|
+
paths["stderr"].write_text(completed.stderr, encoding="utf-8")
|
|
424
|
+
payload["output_paths"] = {name: str(path) for name, path in paths.items()}
|
|
425
|
+
paths["summary"].write_text(
|
|
426
|
+
json.dumps(payload, indent=2, sort_keys=True) + "\n",
|
|
427
|
+
encoding="utf-8",
|
|
428
|
+
)
|
|
429
|
+
return payload
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def render_text(payload: Mapping[str, Any]) -> str:
|
|
433
|
+
lines = [
|
|
434
|
+
f"CodeRabbit CLI audit for {payload.get('repo')}#{payload.get('pr_number')}",
|
|
435
|
+
f"head: {payload.get('head_sha')}",
|
|
436
|
+
f"head_check: {payload.get('head_check', {}).get('status') if isinstance(payload.get('head_check'), Mapping) else 'unknown'}",
|
|
437
|
+
f"parse_status: {payload.get('parse_status')}",
|
|
438
|
+
f"findings: {payload.get('finding_count')}",
|
|
439
|
+
f"runtime: {payload.get('duration_seconds')}s",
|
|
440
|
+
]
|
|
441
|
+
if payload.get("output_paths"):
|
|
442
|
+
lines.extend(["", "Artifacts:"])
|
|
443
|
+
output_paths = payload.get("output_paths", {})
|
|
444
|
+
if isinstance(output_paths, Mapping):
|
|
445
|
+
for name, path in sorted(output_paths.items()):
|
|
446
|
+
lines.append(f"- {name}: {path}")
|
|
447
|
+
return "\n".join(lines) + "\n"
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def main(argv: list[str] | None = None) -> int:
|
|
451
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
452
|
+
parser.add_argument("--repo", required=True, help="owner/repo")
|
|
453
|
+
parser.add_argument("--pr", type=int, required=True, help="PR number")
|
|
454
|
+
parser.add_argument("--repo-path", type=Path, default=Path.cwd())
|
|
455
|
+
parser.add_argument("--expected-head-sha", default=None)
|
|
456
|
+
parser.add_argument(
|
|
457
|
+
"--command",
|
|
458
|
+
default=os.environ.get("CODERABBIT_CLI_COMMAND", DEFAULT_CODERABBIT_COMMAND),
|
|
459
|
+
)
|
|
460
|
+
parser.add_argument("--base-ref", default=DEFAULT_BASE_REF)
|
|
461
|
+
parser.add_argument(
|
|
462
|
+
"--review-type",
|
|
463
|
+
default=DEFAULT_REVIEW_TYPE,
|
|
464
|
+
choices=("all", "committed", "uncommitted"),
|
|
465
|
+
)
|
|
466
|
+
parser.add_argument("--config", dest="config_files", type=Path, action="append", default=[])
|
|
467
|
+
parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT_SECONDS)
|
|
468
|
+
parser.add_argument("--output-dir", type=Path, default=None)
|
|
469
|
+
parser.add_argument("--allow-dirty", action="store_true")
|
|
470
|
+
parser.add_argument(
|
|
471
|
+
"--allow-historical-head",
|
|
472
|
+
action="store_true",
|
|
473
|
+
help="allow --repo-path HEAD to differ from the current GitHub PR head",
|
|
474
|
+
)
|
|
475
|
+
parser.add_argument("--json", action="store_true")
|
|
476
|
+
args = parser.parse_args(argv)
|
|
477
|
+
|
|
478
|
+
token = resolve_github_token()
|
|
479
|
+
if not token:
|
|
480
|
+
print(
|
|
481
|
+
"error: set GITHUB_TOKEN or authenticate gh so `gh auth token` works",
|
|
482
|
+
file=sys.stderr,
|
|
483
|
+
)
|
|
484
|
+
return 1
|
|
485
|
+
|
|
486
|
+
try:
|
|
487
|
+
payload = run_coderabbit_cli_audit(
|
|
488
|
+
repo=args.repo,
|
|
489
|
+
pr_number=args.pr,
|
|
490
|
+
github_token=token,
|
|
491
|
+
repo_path=args.repo_path,
|
|
492
|
+
command=args.command,
|
|
493
|
+
expected_head_sha=args.expected_head_sha,
|
|
494
|
+
base_ref=args.base_ref,
|
|
495
|
+
review_type=args.review_type,
|
|
496
|
+
config_files=tuple(args.config_files),
|
|
497
|
+
timeout_seconds=args.timeout,
|
|
498
|
+
output_dir=args.output_dir,
|
|
499
|
+
allow_dirty=args.allow_dirty,
|
|
500
|
+
allow_historical_head=args.allow_historical_head,
|
|
501
|
+
)
|
|
502
|
+
except CoderabbitCliHeadChangedError as exc:
|
|
503
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
504
|
+
return 2
|
|
505
|
+
except (
|
|
506
|
+
CoderabbitCliWorkspaceError,
|
|
507
|
+
OSError,
|
|
508
|
+
ValueError,
|
|
509
|
+
subprocess.TimeoutExpired,
|
|
510
|
+
subprocess.CalledProcessError,
|
|
511
|
+
urllib.error.URLError,
|
|
512
|
+
) as exc:
|
|
513
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
514
|
+
return 1
|
|
515
|
+
|
|
516
|
+
if args.json:
|
|
517
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
518
|
+
else:
|
|
519
|
+
print(render_text(payload), end="")
|
|
520
|
+
head_check = payload.get("head_check")
|
|
521
|
+
head_check_status = head_check.get("status") if isinstance(head_check, Mapping) else None
|
|
522
|
+
return 0 if payload.get("returncode") == 0 and head_check_status == "pass" else 1
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
if __name__ == "__main__":
|
|
526
|
+
raise SystemExit(main())
|