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.
Files changed (185) hide show
  1. code_mower/__init__.py +3 -0
  2. code_mower/adapters/__init__.py +39 -0
  3. code_mower/adapters/_base.py +148 -0
  4. code_mower/adapters/cursor_bugbot.py +97 -0
  5. code_mower/adapters/gitar.py +111 -0
  6. code_mower/adapters/greptile.py +191 -0
  7. code_mower/adapters/qodo.py +140 -0
  8. code_mower/antigravity_cli_audit_pr.py +241 -0
  9. code_mower/audit_handoff_log.py +345 -0
  10. code_mower/audit_labeler_lib.py +311 -0
  11. code_mower/audit_progress.py +203 -0
  12. code_mower/blind_review_artifacts.py +562 -0
  13. code_mower/blind_review_coordinator.py +276 -0
  14. code_mower/bootstrap.py +524 -0
  15. code_mower/builder_experiment.py +539 -0
  16. code_mower/calibration/__init__.py +181 -0
  17. code_mower/calibration/arms.py +267 -0
  18. code_mower/calibration/auto_discovery.py +348 -0
  19. code_mower/calibration/commands.py +192 -0
  20. code_mower/calibration/context_inputs.py +199 -0
  21. code_mower/calibration/corpus.py +91 -0
  22. code_mower/calibration/evidence.py +20 -0
  23. code_mower/calibration/evidence_report.py +360 -0
  24. code_mower/calibration/identity.py +27 -0
  25. code_mower/calibration/metrics.py +18 -0
  26. code_mower/calibration/overlap.py +103 -0
  27. code_mower/calibration/planning.py +309 -0
  28. code_mower/calibration/policy.py +207 -0
  29. code_mower/calibration/results.py +315 -0
  30. code_mower/calibration/run_results.py +147 -0
  31. code_mower/calibration/run_status.py +64 -0
  32. code_mower/calibration/runner.py +360 -0
  33. code_mower/calibration/truth.py +188 -0
  34. code_mower/calibration/value_report.py +142 -0
  35. code_mower/checks.py +402 -0
  36. code_mower/claude_audit_pr.py +1126 -0
  37. code_mower/claude_cli_bounce.py +303 -0
  38. code_mower/claude_cli_environment.py +73 -0
  39. code_mower/clear_stale.py +374 -0
  40. code_mower/cli.py +537 -0
  41. code_mower/cloud.py +674 -0
  42. code_mower/cloud_client/__init__.py +173 -0
  43. code_mower/cloud_client/bundle.py +155 -0
  44. code_mower/cloud_client/doctor.py +206 -0
  45. code_mower/cloud_client/dogfood.py +78 -0
  46. code_mower/cloud_client/endpoints.py +114 -0
  47. code_mower/cloud_client/errors.py +7 -0
  48. code_mower/cloud_client/events.py +278 -0
  49. code_mower/cloud_client/export.py +272 -0
  50. code_mower/cloud_client/git_metadata.py +46 -0
  51. code_mower/cloud_client/manifest.py +39 -0
  52. code_mower/cloud_client/operations.py +448 -0
  53. code_mower/cloud_client/reports.py +45 -0
  54. code_mower/cloud_client/setup.py +205 -0
  55. code_mower/cloud_client/upload.py +97 -0
  56. code_mower/code_mower_calibration.py +598 -0
  57. code_mower/code_mower_context_packs.py +591 -0
  58. code_mower/code_mower_merge.py +227 -0
  59. code_mower/code_mower_telemetry.py +561 -0
  60. code_mower/coderabbit_cli_audit_pr.py +526 -0
  61. code_mower/codex_audit_env_preflight.py +220 -0
  62. code_mower/codex_audit_pr.py +1738 -0
  63. code_mower/codex_audit_schema_smoke.py +160 -0
  64. code_mower/codex_audit_verdict.schema.json +44 -0
  65. code_mower/config.py +655 -0
  66. code_mower/doctor.py +161 -0
  67. code_mower/doctor_checks/__init__.py +104 -0
  68. code_mower/doctor_checks/cloud.py +129 -0
  69. code_mower/doctor_checks/common.py +215 -0
  70. code_mower/doctor_checks/github.py +128 -0
  71. code_mower/doctor_checks/github_actions.py +11 -0
  72. code_mower/doctor_checks/github_actions_cost.py +99 -0
  73. code_mower/doctor_checks/github_actions_cost_summary.py +111 -0
  74. code_mower/doctor_checks/github_actions_failure_annotations.py +27 -0
  75. code_mower/doctor_checks/github_actions_failure_models.py +47 -0
  76. code_mower/doctor_checks/github_actions_failure_scan.py +200 -0
  77. code_mower/doctor_checks/github_actions_failure_selection.py +63 -0
  78. code_mower/doctor_checks/github_actions_failures.py +103 -0
  79. code_mower/doctor_checks/github_actions_permissions.py +55 -0
  80. code_mower/doctor_checks/github_api.py +79 -0
  81. code_mower/doctor_checks/github_branch.py +56 -0
  82. code_mower/doctor_checks/github_config.py +25 -0
  83. code_mower/doctor_checks/github_provider.py +61 -0
  84. code_mower/doctor_checks/github_repo.py +120 -0
  85. code_mower/doctor_checks/groups.py +36 -0
  86. code_mower/doctor_checks/models.py +96 -0
  87. code_mower/doctor_checks/output.py +86 -0
  88. code_mower/doctor_checks/presets.py +64 -0
  89. code_mower/doctor_checks/privacy.py +20 -0
  90. code_mower/doctor_checks/provider_api_model.py +138 -0
  91. code_mower/doctor_checks/provider_api_model_openai.py +29 -0
  92. code_mower/doctor_checks/provider_api_model_profiles.py +137 -0
  93. code_mower/doctor_checks/provider_env.py +113 -0
  94. code_mower/doctor_checks/provider_env_required.py +56 -0
  95. code_mower/doctor_checks/provider_env_tokens.py +100 -0
  96. code_mower/doctor_checks/provider_local_cli.py +162 -0
  97. code_mower/doctor_checks/provider_local_cli_commands.py +47 -0
  98. code_mower/doctor_checks/provider_local_cli_probe_config.py +70 -0
  99. code_mower/doctor_checks/provider_probe.py +20 -0
  100. code_mower/doctor_checks/provider_probe_auth.py +52 -0
  101. code_mower/doctor_checks/provider_probe_evaluation.py +109 -0
  102. code_mower/doctor_checks/provider_probe_json.py +45 -0
  103. code_mower/doctor_checks/provider_probe_remediation.py +39 -0
  104. code_mower/doctor_checks/providers.py +159 -0
  105. code_mower/doctor_checks/registry.py +69 -0
  106. code_mower/doctor_checks/runner.py +188 -0
  107. code_mower/doctor_checks/runtime.py +89 -0
  108. code_mower/doctor_checks/runtime_github_auth.py +148 -0
  109. code_mower/gemini_cli_audit_pr.py +897 -0
  110. code_mower/hermes_cli_audit_pr.py +436 -0
  111. code_mower/init.py +888 -0
  112. code_mower/lane_configs/__init__.py +37 -0
  113. code_mower/lane_configs/aider.py +32 -0
  114. code_mower/lane_configs/antigravity_cli.py +35 -0
  115. code_mower/lane_configs/claude.py +35 -0
  116. code_mower/lane_configs/codex.py +32 -0
  117. code_mower/lane_configs/devin.py +33 -0
  118. code_mower/lane_configs/gemini_cli.py +35 -0
  119. code_mower/lane_configs/hermes_cli.py +35 -0
  120. code_mower/lane_configs/local_llm.py +31 -0
  121. code_mower/local_llm_audit_pr.py +1364 -0
  122. code_mower/local_llm_bakeoff.py +458 -0
  123. code_mower/local_llm_calibration.py +441 -0
  124. code_mower/local_llm_profiles.py +66 -0
  125. code_mower/migration.py +508 -0
  126. code_mower/migration_install.py +292 -0
  127. code_mower/migration_mirror.py +392 -0
  128. code_mower/migration_readiness.py +237 -0
  129. code_mower/migration_rehearsal.py +718 -0
  130. code_mower/next_steps.py +441 -0
  131. code_mower/package.py +673 -0
  132. code_mower/package_content.py +444 -0
  133. code_mower/package_manifest.py +452 -0
  134. code_mower/package_paths.py +53 -0
  135. code_mower/package_rendering.py +90 -0
  136. code_mower/package_static.py +585 -0
  137. code_mower/prompts.py +267 -0
  138. code_mower/provider_registry.py +469 -0
  139. code_mower/provider_runners/__init__.py +60 -0
  140. code_mower/provider_runners/comments.py +31 -0
  141. code_mower/provider_runners/git.py +46 -0
  142. code_mower/provider_runners/github_auth.py +61 -0
  143. code_mower/provider_runners/github_pr.py +120 -0
  144. code_mower/provider_runners/process.py +58 -0
  145. code_mower/provider_runners/repo_paths.py +23 -0
  146. code_mower/provider_runners/text_schema.py +41 -0
  147. code_mower/provider_runners/verdict_artifacts.py +103 -0
  148. code_mower/provider_runners/workspace.py +57 -0
  149. code_mower/release_readiness.py +549 -0
  150. code_mower/reviewer_metrics.py +389 -0
  151. code_mower/saas_reviewer_labeler.py +809 -0
  152. code_mower/secrets.py +89 -0
  153. code_mower/templates/builder-experiment.example.json +55 -0
  154. code_mower/templates/calibration-corpus.example.json +129 -0
  155. code_mower/templates/calibration-corpus.json +129 -0
  156. code_mower/templates/code-mower.example.yml +423 -0
  157. code_mower/templates/context-packs.example.json +150 -0
  158. code_mower/templates/lane_prompts/base-audit.md +22 -0
  159. code_mower/templates/lane_prompts/calibration-policy.md +21 -0
  160. code_mower/templates/lane_prompts/context-driven-quality.md +21 -0
  161. code_mower/templates/lane_prompts/docs-design.md +12 -0
  162. code_mower/templates/lane_prompts/generic-programming.md +21 -0
  163. code_mower/templates/lane_prompts/operability.md +22 -0
  164. code_mower/templates/lane_prompts/package-runtime.md +12 -0
  165. code_mower/templates/lane_prompts/security-threat-model.md +22 -0
  166. code_mower/templates/product-support/code_mower +216 -0
  167. code_mower/templates/product-support/code_mower_standalone_pin.env +7 -0
  168. code_mower/templates/product-support/code_mower_standalone_shadow.sh +151 -0
  169. code_mower/templates/product-support/run_claude_audit_pr.sh +32 -0
  170. code_mower/templates/product-support/run_codex_audit_pr.sh +32 -0
  171. code_mower/templates/product-support/safe_gh_comment.py +96 -0
  172. code_mower/templates/providers.yml +454 -0
  173. code_mower/templates/reviewer-spend.example.json +28 -0
  174. code_mower/templates/reviewer-value-report.example.md +20 -0
  175. code_mower/templates/workflows/private-standalone-shadow.yml.j2 +106 -0
  176. code_mower/templates/workflows/review-clear-stale.yml.j2 +83 -0
  177. code_mower/trailer_comment_labeler.py +207 -0
  178. code_mower/versioning.py +32 -0
  179. code_mower-0.5.0b5.dist-info/METADATA +302 -0
  180. code_mower-0.5.0b5.dist-info/RECORD +185 -0
  181. code_mower-0.5.0b5.dist-info/WHEEL +5 -0
  182. code_mower-0.5.0b5.dist-info/entry_points.txt +2 -0
  183. code_mower-0.5.0b5.dist-info/licenses/LICENSE +202 -0
  184. code_mower-0.5.0b5.dist-info/licenses/NOTICE +10 -0
  185. 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())