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,436 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run Hermes Agent as an informational Code Mower calibration reviewer."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
import time
|
|
13
|
+
import urllib.error
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Mapping
|
|
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 import gemini_cli_audit_pr
|
|
22
|
+
from code_mower import prompts as code_mower_prompts
|
|
23
|
+
from code_mower.provider_runners import build_allowlisted_child_env
|
|
24
|
+
else:
|
|
25
|
+
from tools import gemini_cli_audit_pr, code_mower_prompts
|
|
26
|
+
from tools.provider_runners import build_allowlisted_child_env
|
|
27
|
+
elif __package__ == "tools":
|
|
28
|
+
from tools import gemini_cli_audit_pr, code_mower_prompts
|
|
29
|
+
from tools.provider_runners import build_allowlisted_child_env
|
|
30
|
+
else: # pragma: no cover - exercised after package extraction.
|
|
31
|
+
from . import gemini_cli_audit_pr
|
|
32
|
+
from . import prompts as code_mower_prompts
|
|
33
|
+
from .provider_runners import build_allowlisted_child_env
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
DEFAULT_HERMES_COMMAND = "hermes"
|
|
37
|
+
DEFAULT_HERMES_MODE = "hermes-cli-audit"
|
|
38
|
+
DEFAULT_HERMES_OUTPUT_STEM = "hermes-cli"
|
|
39
|
+
DEFAULT_HERMES_DISPLAY_NAME = "Hermes CLI"
|
|
40
|
+
DEFAULT_HERMES_MODEL_ENV = "HERMES_INFERENCE_MODEL"
|
|
41
|
+
DEFAULT_HERMES_PROVIDER_ENV = "HERMES_PROVIDER"
|
|
42
|
+
HERMES_AMBIENT_HOME_ENV = "HERMES_CLI_USE_AMBIENT_HOME"
|
|
43
|
+
HERMES_HELP_SENTINELS = (
|
|
44
|
+
"--oneshot",
|
|
45
|
+
"--ignore-user-config",
|
|
46
|
+
"--ignore-rules",
|
|
47
|
+
"--toolsets",
|
|
48
|
+
)
|
|
49
|
+
HERMES_ENV_ALLOWLIST = (
|
|
50
|
+
"PATH",
|
|
51
|
+
"LANG",
|
|
52
|
+
"LC_ALL",
|
|
53
|
+
"LC_CTYPE",
|
|
54
|
+
"SSL_CERT_FILE",
|
|
55
|
+
"REQUESTS_CA_BUNDLE",
|
|
56
|
+
"NODE_EXTRA_CA_CERTS",
|
|
57
|
+
"HTTPS_PROXY",
|
|
58
|
+
"HTTP_PROXY",
|
|
59
|
+
"NO_PROXY",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
HermesCliHeadChangedError = gemini_cli_audit_pr.GeminiCliHeadChangedError
|
|
63
|
+
HermesCliUnsupportedError = gemini_cli_audit_pr.GeminiCliUnsupportedError
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _env_flag_enabled(name: str) -> bool:
|
|
67
|
+
return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def build_hermes_child_env(home_dir: Path, *, preserve_ambient_home: bool) -> dict[str, str]:
|
|
71
|
+
return build_allowlisted_child_env(
|
|
72
|
+
HERMES_ENV_ALLOWLIST,
|
|
73
|
+
extra_env={
|
|
74
|
+
# Keep oneshot review deterministic and quiet. The CLI still needs
|
|
75
|
+
# ambient auth in current releases, but Code Mower does not need
|
|
76
|
+
# project rules, tool progress, or default toolsets for a diff-only
|
|
77
|
+
# calibration review.
|
|
78
|
+
"HERMES_IGNORE_USER_CONFIG": "1",
|
|
79
|
+
"HERMES_IGNORE_RULES": "1",
|
|
80
|
+
"HERMES_CORE_TOOLS": "",
|
|
81
|
+
"HERMES_TOOL_PROGRESS": "0",
|
|
82
|
+
"HERMES_QUIET": "1",
|
|
83
|
+
},
|
|
84
|
+
home_env={
|
|
85
|
+
"HOME": home_dir,
|
|
86
|
+
"HERMES_HOME": home_dir / ".hermes",
|
|
87
|
+
"XDG_CONFIG_HOME": home_dir / ".config",
|
|
88
|
+
"XDG_CACHE_HOME": home_dir / ".cache",
|
|
89
|
+
"XDG_STATE_HOME": home_dir / ".local" / "state",
|
|
90
|
+
},
|
|
91
|
+
preserve_ambient_home=preserve_ambient_home,
|
|
92
|
+
ambient_home_keys=(
|
|
93
|
+
"HOME",
|
|
94
|
+
"HERMES_HOME",
|
|
95
|
+
"XDG_CONFIG_HOME",
|
|
96
|
+
"XDG_CACHE_HOME",
|
|
97
|
+
"XDG_STATE_HOME",
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def verify_hermes_oneshot_contract(
|
|
103
|
+
command: str,
|
|
104
|
+
*,
|
|
105
|
+
cwd: Path,
|
|
106
|
+
env: Mapping[str, str],
|
|
107
|
+
) -> None:
|
|
108
|
+
completed = subprocess.run(
|
|
109
|
+
[command, "--help"],
|
|
110
|
+
capture_output=True,
|
|
111
|
+
cwd=cwd,
|
|
112
|
+
env=dict(env),
|
|
113
|
+
text=True,
|
|
114
|
+
check=False,
|
|
115
|
+
timeout=10,
|
|
116
|
+
)
|
|
117
|
+
help_text = f"{completed.stdout}\n{completed.stderr}"
|
|
118
|
+
missing = [sentinel for sentinel in HERMES_HELP_SENTINELS if sentinel not in help_text]
|
|
119
|
+
if completed.returncode != 0 or missing:
|
|
120
|
+
raise HermesCliUnsupportedError(
|
|
121
|
+
"Hermes CLI must support oneshot mode plus config/rules/toolset "
|
|
122
|
+
f"controls for Code Mower calibration. Missing help sentinel(s): {missing!r}."
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def hermes_prompt_file_arg(prompt_path: Path) -> str:
|
|
127
|
+
"""Return Hermes' single-query context-reference argument for a prompt file."""
|
|
128
|
+
# Hermes expands @ context references such as @file.txt in single-query and
|
|
129
|
+
# oneshot flows before sending the message to the model. Keep the prompt
|
|
130
|
+
# file in the subprocess cwd so argv contains only this short reference,
|
|
131
|
+
# not the full PR diff and audit prompt.
|
|
132
|
+
return f"@{prompt_path.name}"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def run_hermes_cli_audit(
|
|
136
|
+
*,
|
|
137
|
+
repo: str,
|
|
138
|
+
pr_number: int,
|
|
139
|
+
github_token: str,
|
|
140
|
+
command: str = DEFAULT_HERMES_COMMAND,
|
|
141
|
+
expected_head_sha: str | None = None,
|
|
142
|
+
prompt_lenses: tuple[str, ...] = code_mower_prompts.DEFAULT_REVIEW_LENSES,
|
|
143
|
+
prompt_dir: Path | None = None,
|
|
144
|
+
max_diff_bytes: int = gemini_cli_audit_pr.DEFAULT_MAX_DIFF_BYTES,
|
|
145
|
+
timeout_seconds: int = gemini_cli_audit_pr.DEFAULT_TIMEOUT_SECONDS,
|
|
146
|
+
output_dir: Path | None = None,
|
|
147
|
+
repo_path: Path | None = None,
|
|
148
|
+
base_ref: str = gemini_cli_audit_pr.DEFAULT_BASE_REF,
|
|
149
|
+
allow_historical_head: bool = False,
|
|
150
|
+
historical_calibration: bool = False,
|
|
151
|
+
allow_ambient_home: bool = False,
|
|
152
|
+
model: str | None = None,
|
|
153
|
+
provider: str | None = None,
|
|
154
|
+
context_pack_text: str = "",
|
|
155
|
+
) -> dict[str, Any]:
|
|
156
|
+
if not allow_ambient_home:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
"Hermes CLI oneshot currently uses local Hermes auth/session state. "
|
|
159
|
+
f"Set {HERMES_AMBIENT_HOME_ENV}=1 only in a trusted local "
|
|
160
|
+
"environment to opt into inheriting that ambient state."
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
pr_meta = gemini_cli_audit_pr.fetch_pull_request(repo, pr_number, token=github_token)
|
|
164
|
+
pr_head_sha = str(pr_meta.get("head", {}).get("sha") or "")
|
|
165
|
+
if not pr_head_sha:
|
|
166
|
+
raise ValueError("GitHub pull request response did not include head.sha")
|
|
167
|
+
normalized_expected = str(expected_head_sha or "").strip().lower()
|
|
168
|
+
diff_source = "github_pr"
|
|
169
|
+
if repo_path is None:
|
|
170
|
+
head_sha = pr_head_sha
|
|
171
|
+
if normalized_expected and normalized_expected != head_sha.lower():
|
|
172
|
+
raise HermesCliHeadChangedError(
|
|
173
|
+
"PR head does not match calibration corpus; "
|
|
174
|
+
f"expected {expected_head_sha}, current={head_sha}."
|
|
175
|
+
)
|
|
176
|
+
diff = gemini_cli_audit_pr.fetch_pull_request_diff(repo, pr_number, token=github_token)
|
|
177
|
+
else:
|
|
178
|
+
head_sha, diff = gemini_cli_audit_pr.fetch_local_checkout_diff(
|
|
179
|
+
repo_path,
|
|
180
|
+
base_ref=base_ref,
|
|
181
|
+
)
|
|
182
|
+
diff_source = "local_checkout"
|
|
183
|
+
if normalized_expected and normalized_expected != head_sha.lower():
|
|
184
|
+
raise HermesCliHeadChangedError(
|
|
185
|
+
"local checkout does not match calibration corpus; "
|
|
186
|
+
f"expected {expected_head_sha}, current={head_sha}."
|
|
187
|
+
)
|
|
188
|
+
if (
|
|
189
|
+
not allow_historical_head
|
|
190
|
+
and not historical_calibration
|
|
191
|
+
and head_sha.lower() != pr_head_sha.lower()
|
|
192
|
+
):
|
|
193
|
+
raise HermesCliHeadChangedError(
|
|
194
|
+
"local checkout is not at the current PR head; pass "
|
|
195
|
+
"--historical-calibration for archived calibration runs. "
|
|
196
|
+
f"local={head_sha} current_pr={pr_head_sha}."
|
|
197
|
+
)
|
|
198
|
+
if not diff.strip():
|
|
199
|
+
raise ValueError(
|
|
200
|
+
"Hermes CLI calibration diff is empty; check --repo-path and --base-ref"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
prompt, diagnostics = gemini_cli_audit_pr.build_prompt(
|
|
204
|
+
repo=repo,
|
|
205
|
+
pr_number=pr_number,
|
|
206
|
+
pr_meta=pr_meta,
|
|
207
|
+
head_sha=head_sha,
|
|
208
|
+
diff=diff,
|
|
209
|
+
prompt_lenses=prompt_lenses,
|
|
210
|
+
prompt_dir=prompt_dir,
|
|
211
|
+
max_diff_bytes=max_diff_bytes,
|
|
212
|
+
historical_calibration=historical_calibration,
|
|
213
|
+
display_name=DEFAULT_HERMES_DISPLAY_NAME,
|
|
214
|
+
context_pack_text=context_pack_text,
|
|
215
|
+
)
|
|
216
|
+
diagnostics["diff_source"] = diff_source
|
|
217
|
+
diagnostics["base_ref"] = base_ref if repo_path is not None else None
|
|
218
|
+
diagnostics["cli_transport"] = "oneshot_at_context_file_arg"
|
|
219
|
+
diagnostics["preserve_ambient_home"] = True
|
|
220
|
+
diagnostics["ignore_user_config"] = True
|
|
221
|
+
diagnostics["ignore_rules"] = True
|
|
222
|
+
diagnostics["toolsets"] = ""
|
|
223
|
+
|
|
224
|
+
hermes_model = (model or os.environ.get("HERMES_CLI_MODEL") or os.environ.get(DEFAULT_HERMES_MODEL_ENV) or "").strip()
|
|
225
|
+
hermes_provider = (provider or os.environ.get("HERMES_CLI_PROVIDER") or os.environ.get(DEFAULT_HERMES_PROVIDER_ENV) or "").strip()
|
|
226
|
+
|
|
227
|
+
started = time.monotonic()
|
|
228
|
+
with tempfile.TemporaryDirectory(prefix="code-mower-hermes-") as temp_dir_name:
|
|
229
|
+
temp_dir = Path(temp_dir_name)
|
|
230
|
+
home_dir = temp_dir / "home"
|
|
231
|
+
workspace_dir = temp_dir / "workspace"
|
|
232
|
+
home_dir.mkdir()
|
|
233
|
+
workspace_dir.mkdir()
|
|
234
|
+
child_env = build_hermes_child_env(
|
|
235
|
+
home_dir,
|
|
236
|
+
preserve_ambient_home=True,
|
|
237
|
+
)
|
|
238
|
+
verify_hermes_oneshot_contract(command, cwd=workspace_dir, env=child_env)
|
|
239
|
+
prompt_path = workspace_dir / "code-mower-hermes-prompt.txt"
|
|
240
|
+
prompt_path.write_text(prompt, encoding="utf-8")
|
|
241
|
+
hermes_args = [
|
|
242
|
+
command,
|
|
243
|
+
"--ignore-user-config",
|
|
244
|
+
"--ignore-rules",
|
|
245
|
+
"--toolsets",
|
|
246
|
+
"",
|
|
247
|
+
]
|
|
248
|
+
if hermes_model:
|
|
249
|
+
hermes_args.extend(["--model", hermes_model])
|
|
250
|
+
if hermes_provider:
|
|
251
|
+
hermes_args.extend(["--provider", hermes_provider])
|
|
252
|
+
hermes_args.extend(["--oneshot", hermes_prompt_file_arg(prompt_path)])
|
|
253
|
+
completed = subprocess.run(
|
|
254
|
+
hermes_args,
|
|
255
|
+
capture_output=True,
|
|
256
|
+
cwd=workspace_dir,
|
|
257
|
+
env=child_env,
|
|
258
|
+
text=True,
|
|
259
|
+
check=False,
|
|
260
|
+
timeout=timeout_seconds,
|
|
261
|
+
)
|
|
262
|
+
duration_seconds = time.monotonic() - started
|
|
263
|
+
|
|
264
|
+
response_text = completed.stdout
|
|
265
|
+
parsed_response = gemini_cli_audit_pr.parse_response_json(response_text)
|
|
266
|
+
verdict = gemini_cli_audit_pr._validate_verdict(parsed_response)
|
|
267
|
+
if repo_path is None:
|
|
268
|
+
head_after_meta = gemini_cli_audit_pr.fetch_pull_request(repo, pr_number, token=github_token)
|
|
269
|
+
head_after = str(head_after_meta.get("head", {}).get("sha") or "")
|
|
270
|
+
if head_after != head_sha:
|
|
271
|
+
raise HermesCliHeadChangedError(
|
|
272
|
+
"PR head changed during Hermes CLI audit; "
|
|
273
|
+
f"start={head_sha} end={head_after}. Discard this run and rerun."
|
|
274
|
+
)
|
|
275
|
+
else:
|
|
276
|
+
head_after = gemini_cli_audit_pr._local_head_sha(repo_path.expanduser().resolve())
|
|
277
|
+
if head_after != head_sha:
|
|
278
|
+
raise HermesCliHeadChangedError(
|
|
279
|
+
"local checkout head changed during Hermes CLI audit; "
|
|
280
|
+
f"start={head_sha} end={head_after}. Discard this run and rerun."
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
payload: dict[str, Any] = {
|
|
284
|
+
"mode": DEFAULT_HERMES_MODE,
|
|
285
|
+
"repo": repo,
|
|
286
|
+
"pr_number": pr_number,
|
|
287
|
+
"head_sha": head_sha,
|
|
288
|
+
"head_sha_end": head_after,
|
|
289
|
+
"pr_head_sha": pr_head_sha,
|
|
290
|
+
"command": command,
|
|
291
|
+
"model": hermes_model or None,
|
|
292
|
+
"provider": hermes_provider or None,
|
|
293
|
+
"returncode": completed.returncode,
|
|
294
|
+
"duration_seconds": round(duration_seconds, 3),
|
|
295
|
+
"diagnostics": diagnostics,
|
|
296
|
+
"response_text": response_text,
|
|
297
|
+
"parsed_response": parsed_response,
|
|
298
|
+
"verdict": verdict,
|
|
299
|
+
"stderr": completed.stderr,
|
|
300
|
+
"historical_calibration": historical_calibration,
|
|
301
|
+
}
|
|
302
|
+
if output_dir is not None:
|
|
303
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
304
|
+
paths = {
|
|
305
|
+
"prompt": output_dir / f"{DEFAULT_HERMES_OUTPUT_STEM}.prompt.txt",
|
|
306
|
+
"response": output_dir / f"{DEFAULT_HERMES_OUTPUT_STEM}.response.md",
|
|
307
|
+
"summary": output_dir / f"{DEFAULT_HERMES_OUTPUT_STEM}.summary.json",
|
|
308
|
+
}
|
|
309
|
+
paths["prompt"].write_text(prompt, encoding="utf-8")
|
|
310
|
+
paths["response"].write_text(response_text, encoding="utf-8")
|
|
311
|
+
payload["output_paths"] = {name: str(path) for name, path in paths.items()}
|
|
312
|
+
paths["summary"].write_text(
|
|
313
|
+
json.dumps(payload, indent=2, sort_keys=True) + "\n",
|
|
314
|
+
encoding="utf-8",
|
|
315
|
+
)
|
|
316
|
+
return payload
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def render_text(payload: Mapping[str, Any]) -> str:
|
|
320
|
+
text = gemini_cli_audit_pr.render_text(payload)
|
|
321
|
+
return text.replace("Gemini CLI audit", "Hermes CLI audit", 1)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def main(argv: list[str] | None = None) -> int:
|
|
325
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
326
|
+
parser.add_argument("--repo", required=True, help="owner/repo")
|
|
327
|
+
parser.add_argument("--pr", type=int, required=True, help="PR number")
|
|
328
|
+
parser.add_argument("--expected-head-sha", default=None)
|
|
329
|
+
parser.add_argument(
|
|
330
|
+
"--repo-path",
|
|
331
|
+
type=Path,
|
|
332
|
+
default=None,
|
|
333
|
+
help="optional local checkout to diff for archived calibration heads",
|
|
334
|
+
)
|
|
335
|
+
parser.add_argument("--base-ref", default=gemini_cli_audit_pr.DEFAULT_BASE_REF)
|
|
336
|
+
parser.add_argument(
|
|
337
|
+
"--allow-historical-head",
|
|
338
|
+
action="store_true",
|
|
339
|
+
help="allow --repo-path HEAD to differ from the current GitHub PR head",
|
|
340
|
+
)
|
|
341
|
+
parser.add_argument(
|
|
342
|
+
"--historical-calibration",
|
|
343
|
+
action="store_true",
|
|
344
|
+
help=(
|
|
345
|
+
"mark this as non-merge-authority evidence against an archived PR "
|
|
346
|
+
"head; implies --allow-historical-head for local checkouts"
|
|
347
|
+
),
|
|
348
|
+
)
|
|
349
|
+
parser.add_argument(
|
|
350
|
+
"--command",
|
|
351
|
+
default=os.environ.get("HERMES_CLI_COMMAND", DEFAULT_HERMES_COMMAND),
|
|
352
|
+
)
|
|
353
|
+
parser.add_argument("--model", default=None)
|
|
354
|
+
parser.add_argument("--provider", default=None)
|
|
355
|
+
parser.add_argument(
|
|
356
|
+
"--prompt-lenses",
|
|
357
|
+
default=",".join(code_mower_prompts.DEFAULT_REVIEW_LENSES),
|
|
358
|
+
)
|
|
359
|
+
parser.add_argument("--prompt-dir", type=Path, default=None)
|
|
360
|
+
parser.add_argument(
|
|
361
|
+
"--context-pack-file",
|
|
362
|
+
action="append",
|
|
363
|
+
type=Path,
|
|
364
|
+
default=[],
|
|
365
|
+
help="Bounded context-pack text file to append to the audit prompt.",
|
|
366
|
+
)
|
|
367
|
+
parser.add_argument(
|
|
368
|
+
"--max-diff-bytes",
|
|
369
|
+
type=int,
|
|
370
|
+
default=gemini_cli_audit_pr.DEFAULT_MAX_DIFF_BYTES,
|
|
371
|
+
)
|
|
372
|
+
parser.add_argument(
|
|
373
|
+
"--timeout",
|
|
374
|
+
type=int,
|
|
375
|
+
default=gemini_cli_audit_pr.DEFAULT_TIMEOUT_SECONDS,
|
|
376
|
+
)
|
|
377
|
+
parser.add_argument("--output-dir", type=Path, default=None)
|
|
378
|
+
parser.add_argument("--json", action="store_true")
|
|
379
|
+
args = parser.parse_args(argv)
|
|
380
|
+
|
|
381
|
+
token = gemini_cli_audit_pr.resolve_github_token()
|
|
382
|
+
if not token:
|
|
383
|
+
print(
|
|
384
|
+
"error: set GITHUB_TOKEN or authenticate gh so `gh auth token` works",
|
|
385
|
+
file=sys.stderr,
|
|
386
|
+
)
|
|
387
|
+
return 1
|
|
388
|
+
try:
|
|
389
|
+
context_pack_text = "\n\n".join(
|
|
390
|
+
path.read_text(encoding="utf-8") for path in args.context_pack_file
|
|
391
|
+
)
|
|
392
|
+
payload = run_hermes_cli_audit(
|
|
393
|
+
repo=args.repo,
|
|
394
|
+
pr_number=args.pr,
|
|
395
|
+
github_token=token,
|
|
396
|
+
command=args.command,
|
|
397
|
+
expected_head_sha=args.expected_head_sha,
|
|
398
|
+
prompt_lenses=code_mower_prompts.split_lenses(args.prompt_lenses),
|
|
399
|
+
prompt_dir=args.prompt_dir,
|
|
400
|
+
max_diff_bytes=args.max_diff_bytes,
|
|
401
|
+
timeout_seconds=args.timeout,
|
|
402
|
+
output_dir=args.output_dir,
|
|
403
|
+
repo_path=args.repo_path,
|
|
404
|
+
base_ref=args.base_ref,
|
|
405
|
+
allow_historical_head=args.allow_historical_head,
|
|
406
|
+
historical_calibration=args.historical_calibration,
|
|
407
|
+
allow_ambient_home=_env_flag_enabled(HERMES_AMBIENT_HOME_ENV),
|
|
408
|
+
model=args.model,
|
|
409
|
+
provider=args.provider,
|
|
410
|
+
context_pack_text=context_pack_text,
|
|
411
|
+
)
|
|
412
|
+
except HermesCliHeadChangedError as exc:
|
|
413
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
414
|
+
return 2
|
|
415
|
+
except (
|
|
416
|
+
HermesCliUnsupportedError,
|
|
417
|
+
OSError,
|
|
418
|
+
ValueError,
|
|
419
|
+
subprocess.CalledProcessError,
|
|
420
|
+
subprocess.TimeoutExpired,
|
|
421
|
+
urllib.error.URLError,
|
|
422
|
+
) as exc:
|
|
423
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
424
|
+
return 1
|
|
425
|
+
|
|
426
|
+
if args.json:
|
|
427
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
428
|
+
else:
|
|
429
|
+
print(render_text(payload), end="")
|
|
430
|
+
if payload.get("returncode") != 0:
|
|
431
|
+
return 1
|
|
432
|
+
return 0 if gemini_cli_audit_pr._verdict_is_usable(payload.get("verdict")) else 1
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
if __name__ == "__main__":
|
|
436
|
+
raise SystemExit(main())
|