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,292 @@
1
+ #!/usr/bin/env python3
2
+ """Install and command primitives for Code Mower migration rehearsals."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import importlib
8
+ import json
9
+ import os
10
+ import shutil
11
+ import subprocess
12
+ import sys
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Any, Sequence
16
+
17
+ MIRRORED_IMPLEMENTATION_PATTERNS = (
18
+ "tools/code_mower_*.py",
19
+ "tools/*_audit_pr.py",
20
+ "tools/*_labeler.py",
21
+ "tools/lane_prompts/*.md",
22
+ "tools/calibration_corpus*.json",
23
+ "tools/reviewer_spend*.json",
24
+ "tools/context_packs*.json",
25
+ "tools/CODE_MOWER*.md",
26
+ )
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class CommandResult:
31
+ command: tuple[str, ...]
32
+ returncode: int
33
+ stdout_sha256: str
34
+ stderr_preview: str
35
+ stdout_preview: str
36
+
37
+ @classmethod
38
+ def from_completed(
39
+ cls,
40
+ command: Sequence[str],
41
+ completed: subprocess.CompletedProcess[str],
42
+ ) -> "CommandResult":
43
+ return cls(
44
+ command=tuple(command),
45
+ returncode=int(completed.returncode),
46
+ stdout_sha256=hashlib.sha256(
47
+ completed.stdout.encode("utf-8", errors="replace")
48
+ ).hexdigest(),
49
+ stdout_preview=completed.stdout[:800],
50
+ stderr_preview=completed.stderr[:1200],
51
+ )
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class RunOutput:
56
+ public: CommandResult
57
+ stdout: str
58
+
59
+
60
+ class RehearsalError(RuntimeError):
61
+ def __init__(self, message: str, steps: list[dict[str, Any]]) -> None:
62
+ super().__init__(message)
63
+ self.steps = steps
64
+
65
+
66
+ def _default_product_rehearsal_local_command(repo_path: Path) -> tuple[str, ...]:
67
+ """Prefer local fallback before mirror removal, wrapper default after it."""
68
+
69
+ wrapper = repo_path / "tools" / "code_mower"
70
+ if not wrapper.is_file():
71
+ return ("env", "CODE_MOWER_USE_LOCAL=1", "tools/code_mower")
72
+ mirrored_candidates = _glob_relative_files(repo_path, MIRRORED_IMPLEMENTATION_PATTERNS)
73
+ if mirrored_candidates:
74
+ return ("env", "CODE_MOWER_USE_LOCAL=1", "tools/code_mower")
75
+ return ("tools/code_mower",)
76
+
77
+
78
+ def _venv_python(venv_dir: Path) -> Path:
79
+ unix_python = venv_dir / "bin" / "python"
80
+ if unix_python.exists():
81
+ return unix_python
82
+ return venv_dir / "Scripts" / "python.exe"
83
+
84
+
85
+ def _venv_code_mower(venv_dir: Path) -> Path:
86
+ if os.name != "nt":
87
+ return venv_dir / "bin" / "code-mower"
88
+ return venv_dir / "Scripts" / "code-mower.exe"
89
+
90
+
91
+ def _run(command: Sequence[str], *, cwd: Path, timeout: int) -> RunOutput:
92
+ completed = subprocess.run(
93
+ list(command),
94
+ cwd=cwd,
95
+ text=True,
96
+ stdout=subprocess.PIPE,
97
+ stderr=subprocess.PIPE,
98
+ timeout=timeout,
99
+ check=False,
100
+ )
101
+ return RunOutput(
102
+ public=CommandResult.from_completed(command, completed),
103
+ stdout=completed.stdout,
104
+ )
105
+
106
+
107
+ def _run_rehearsal_step(
108
+ command: Sequence[str],
109
+ *,
110
+ cwd: Path,
111
+ env: dict[str, str] | None,
112
+ steps: list[dict[str, Any]],
113
+ timeout: int,
114
+ ) -> subprocess.CompletedProcess[str]:
115
+ completed = subprocess.run(
116
+ list(command),
117
+ cwd=cwd,
118
+ env=env,
119
+ text=True,
120
+ stdout=subprocess.PIPE,
121
+ stderr=subprocess.PIPE,
122
+ timeout=timeout,
123
+ check=False,
124
+ )
125
+ step = {
126
+ "command": list(command),
127
+ "cwd": str(cwd),
128
+ "returncode": completed.returncode,
129
+ "stdout_preview": completed.stdout[-4000:],
130
+ "stderr_preview": completed.stderr[-4000:],
131
+ }
132
+ steps.append(step)
133
+ if completed.returncode != 0:
134
+ raise RehearsalError(
135
+ f"command failed: {' '.join(str(part) for part in command)}",
136
+ steps,
137
+ )
138
+ return completed
139
+
140
+
141
+ def _run_rehearsal_step_to_file(
142
+ command: Sequence[str],
143
+ *,
144
+ cwd: Path,
145
+ env: dict[str, str] | None,
146
+ steps: list[dict[str, Any]],
147
+ timeout: int,
148
+ stdout_path: Path,
149
+ ) -> subprocess.CompletedProcess[str]:
150
+ completed = _run_rehearsal_step(
151
+ command,
152
+ cwd=cwd,
153
+ env=env,
154
+ steps=steps,
155
+ timeout=timeout,
156
+ )
157
+ stdout_path.parent.mkdir(parents=True, exist_ok=True)
158
+ stdout_path.write_text(completed.stdout, encoding="utf-8")
159
+ return completed
160
+
161
+
162
+ def _write_json(path: Path, data: Any) -> None:
163
+ path.parent.mkdir(parents=True, exist_ok=True)
164
+ path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
165
+
166
+
167
+ def _load_release_readiness() -> Any:
168
+ """Load the release-readiness helper without breaking legacy tools imports."""
169
+
170
+ if __package__ in {None, ""}:
171
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
172
+ module_names: list[str] = []
173
+ if __package__:
174
+ module_names.append(f"{__package__}.release_readiness")
175
+ module_names.extend(["code_mower.release_readiness", "release_readiness"])
176
+ last_error: ImportError | None = None
177
+ for module_name in module_names:
178
+ try:
179
+ return importlib.import_module(module_name)
180
+ except ImportError as exc:
181
+ last_error = exc
182
+ raise ImportError("unable to import release_readiness helper") from last_error
183
+
184
+
185
+ def _resolve_install_package_spec(package_spec: str, *, base_dir: Path | None = None) -> str:
186
+ candidate_text = package_spec.strip()
187
+ if not candidate_text:
188
+ return package_spec
189
+ if candidate_text.startswith(("git+", "http://", "https://")):
190
+ return package_spec
191
+ looks_path_like = (
192
+ candidate_text.startswith((".", "/", "~"))
193
+ or os.sep in candidate_text
194
+ or (os.altsep is not None and os.altsep in candidate_text)
195
+ )
196
+ if not looks_path_like:
197
+ return package_spec
198
+
199
+ base = (base_dir or Path.cwd()).expanduser().resolve()
200
+ candidate = Path(candidate_text).expanduser()
201
+ if not candidate.is_absolute():
202
+ candidate = base / candidate
203
+ candidate = candidate.resolve()
204
+ if not candidate.exists():
205
+ return package_spec
206
+ return str(candidate)
207
+
208
+
209
+ def _pip_install_command(
210
+ venv_python: Path,
211
+ package_spec: str,
212
+ *,
213
+ pip_index_url: str = "",
214
+ pip_extra_index_urls: Sequence[str] | None = None,
215
+ ) -> list[str]:
216
+ command = [str(venv_python), "-m", "pip", "install"]
217
+ if pip_index_url:
218
+ command.extend(["--index-url", pip_index_url])
219
+ for extra_index_url in pip_extra_index_urls or ():
220
+ if extra_index_url:
221
+ command.extend(["--extra-index-url", extra_index_url])
222
+ command.append(package_spec)
223
+ return command
224
+
225
+
226
+ def _write_public_rehearsal_toy_repo(
227
+ toy_repo: Path,
228
+ *,
229
+ steps: list[dict[str, Any]],
230
+ env: dict[str, str],
231
+ timeout: int,
232
+ ) -> None:
233
+ toy_repo.mkdir(parents=True)
234
+ git = shutil.which("git")
235
+ if not git:
236
+ (toy_repo / "README.md").write_text(
237
+ "# Code Mower package-install rehearsal\n",
238
+ encoding="utf-8",
239
+ )
240
+ return
241
+ _run_rehearsal_step([git, "init", "-q"], cwd=toy_repo, env=env, steps=steps, timeout=timeout)
242
+ _run_rehearsal_step(
243
+ [git, "config", "user.name", "Code Mower Rehearsal"],
244
+ cwd=toy_repo,
245
+ env=env,
246
+ steps=steps,
247
+ timeout=timeout,
248
+ )
249
+ _run_rehearsal_step(
250
+ [git, "config", "user.email", "rehearsal@example.com"],
251
+ cwd=toy_repo,
252
+ env=env,
253
+ steps=steps,
254
+ timeout=timeout,
255
+ )
256
+ _run_rehearsal_step(
257
+ [git, "config", "commit.gpgSign", "false"],
258
+ cwd=toy_repo,
259
+ env=env,
260
+ steps=steps,
261
+ timeout=timeout,
262
+ )
263
+ (toy_repo / "README.md").write_text(
264
+ "# Code Mower package-install rehearsal\n",
265
+ encoding="utf-8",
266
+ )
267
+ _run_rehearsal_step(
268
+ [git, "add", "README.md"], cwd=toy_repo, env=env, steps=steps, timeout=timeout
269
+ )
270
+ _run_rehearsal_step(
271
+ [git, "-c", "commit.gpgSign=false", "commit", "-q", "-m", "Initial rehearsal repo"],
272
+ cwd=toy_repo,
273
+ env=env,
274
+ steps=steps,
275
+ timeout=timeout,
276
+ )
277
+
278
+
279
+ def _json_payload(text: str) -> Any | None:
280
+ try:
281
+ return json.loads(text)
282
+ except json.JSONDecodeError:
283
+ return None
284
+
285
+
286
+ def _glob_relative_files(repo_path: Path, patterns: Sequence[str]) -> list[str]:
287
+ found: set[str] = set()
288
+ for pattern in patterns:
289
+ for path in repo_path.glob(pattern):
290
+ if path.is_file():
291
+ found.add(path.relative_to(repo_path).as_posix())
292
+ return sorted(found)
@@ -0,0 +1,392 @@
1
+ #!/usr/bin/env python3
2
+ """Mirror-removal planning helpers for Code Mower migration gates."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any, Sequence
10
+
11
+ from .migration_rehearsal import (
12
+ MIRRORED_IMPLEMENTATION_PATTERNS,
13
+ _glob_relative_files,
14
+ )
15
+
16
+ PRODUCT_SUPPORT_PATTERNS = (
17
+ "tools/run_code_mower_tests.sh",
18
+ "tools/run_code_mower_standalone_rehearsal.sh",
19
+ "tools/run_codex_audit_pr.sh",
20
+ "tools/run_claude_audit_pr.sh",
21
+ "tools/devin_audit_bridge.py",
22
+ "tools/audit_handoff_log.py",
23
+ "tools/request_review.py",
24
+ "tools/safe_gh_comment.py",
25
+ "tools/codex_audit_env.sh",
26
+ )
27
+
28
+ RUNNER_ALIASES = (
29
+ {
30
+ "legacy": "tools/gemini_cli_audit_pr.py",
31
+ "standalone": "code-mower gemini-cli",
32
+ "status": "supported",
33
+ "notes": "Gemini CLI compatibility runner.",
34
+ },
35
+ {
36
+ "legacy": "tools/antigravity_cli_audit_pr.py",
37
+ "standalone": "code-mower antigravity-cli",
38
+ "status": "supported",
39
+ "notes": "Preferred Google CLI lane after Antigravity migration.",
40
+ },
41
+ {
42
+ "legacy": "tools/hermes_cli_audit_pr.py",
43
+ "standalone": "code-mower hermes-cli",
44
+ "status": "supported",
45
+ "notes": "Hermes Agent calibration runner; requires explicit ambient-home opt-in.",
46
+ },
47
+ {
48
+ "legacy": "tools/coderabbit_cli_audit_pr.py",
49
+ "standalone": "code-mower coderabbit-cli",
50
+ "status": "supported",
51
+ "notes": "Manual informational CodeRabbit CLI evidence capture.",
52
+ },
53
+ {
54
+ "legacy": "tools/local_llm_audit_pr.py",
55
+ "standalone": "code-mower local-llm audit",
56
+ "status": "supported",
57
+ "notes": "OpenAI-compatible local model audit runner.",
58
+ },
59
+ {
60
+ "legacy": "tools/trailer_comment_labeler.py",
61
+ "standalone": "code-mower trailer-comment-labeler",
62
+ "status": "supported",
63
+ "notes": "Use for structured audit trailer/comment label state.",
64
+ },
65
+ {
66
+ "legacy": "tools/saas_reviewer_labeler.py",
67
+ "standalone": "code-mower saas-reviewer-labeler",
68
+ "status": "supported",
69
+ "notes": "Use for SaaS reviewer event label state.",
70
+ },
71
+ {
72
+ "legacy": "tools/run_codex_audit_pr.sh",
73
+ "standalone": "",
74
+ "status": "product-wrapper",
75
+ "notes": (
76
+ "No generic standalone Codex authoring runner exists yet. Keep the "
77
+ "product wrapper for model invocation/repost artifacts, and use "
78
+ "`code-mower trailer-comment-labeler --lane codex` for merge-bar "
79
+ "label state."
80
+ ),
81
+ },
82
+ {
83
+ "legacy": "tools/run_claude_audit_pr.sh",
84
+ "standalone": "",
85
+ "status": "product-wrapper",
86
+ "notes": (
87
+ "No generic standalone Claude authoring runner exists yet. Keep the "
88
+ "product wrapper for model invocation/repost artifacts, and use "
89
+ "`code-mower trailer-comment-labeler --lane claude` for merge-bar "
90
+ "label state."
91
+ ),
92
+ },
93
+ )
94
+
95
+
96
+ def _default_local_command(repo_path: Path) -> tuple[str, ...] | None:
97
+ command_candidate = repo_path / "tools" / "code_mower"
98
+ if command_candidate.is_file():
99
+ return (str(command_candidate),)
100
+ candidate = repo_path / "tools" / "code_mower_cli.py"
101
+ if candidate.is_file():
102
+ return (sys.executable, str(candidate))
103
+ return None
104
+
105
+
106
+ def _relative_existing_files(repo_path: Path, candidates: Sequence[str]) -> list[str]:
107
+ return [candidate for candidate in candidates if (repo_path / candidate).exists()]
108
+
109
+
110
+ def _workflow_file_references(
111
+ repo_path: Path,
112
+ relative_files: Sequence[str],
113
+ ) -> list[dict[str, Any]]:
114
+ workflow_root = repo_path / ".github" / "workflows"
115
+ if not workflow_root.is_dir():
116
+ return []
117
+ tracked = set(relative_files)
118
+ references: list[dict[str, Any]] = []
119
+ for workflow in sorted(
120
+ path
121
+ for pattern in ("*.yml", "*.yaml")
122
+ for path in workflow_root.glob(pattern)
123
+ if path.is_file()
124
+ ):
125
+ try:
126
+ lines = workflow.read_text(encoding="utf-8").splitlines()
127
+ except UnicodeDecodeError:
128
+ lines = workflow.read_text(encoding="utf-8", errors="replace").splitlines()
129
+ for line_number, line in enumerate(lines, start=1):
130
+ stripped = line.strip()
131
+ if not stripped or stripped.startswith("#"):
132
+ continue
133
+ for relative_file in tracked:
134
+ if relative_file in line and _line_requires_workflow_file(
135
+ line,
136
+ relative_file,
137
+ ):
138
+ references.append(
139
+ {
140
+ "workflow": workflow.relative_to(repo_path).as_posix(),
141
+ "line": line_number,
142
+ "file": relative_file,
143
+ "text": stripped[:240],
144
+ }
145
+ )
146
+ return references
147
+
148
+
149
+ def _workflow_local_fallback_references(repo_path: Path) -> list[dict[str, Any]]:
150
+ workflow_root = repo_path / ".github" / "workflows"
151
+ if not workflow_root.is_dir():
152
+ return []
153
+ references: list[dict[str, Any]] = []
154
+ for workflow in sorted(
155
+ path
156
+ for pattern in ("*.yml", "*.yaml")
157
+ for path in workflow_root.glob(pattern)
158
+ if path.is_file()
159
+ ):
160
+ try:
161
+ lines = workflow.read_text(encoding="utf-8").splitlines()
162
+ except UnicodeDecodeError:
163
+ lines = workflow.read_text(encoding="utf-8", errors="replace").splitlines()
164
+ for line_number, line in enumerate(lines, start=1):
165
+ stripped = line.strip()
166
+ if not stripped or stripped.startswith("#"):
167
+ continue
168
+ if "CODE_MOWER_USE_LOCAL=1" in line and "tools/code_mower" in line:
169
+ references.append(
170
+ {
171
+ "workflow": workflow.relative_to(repo_path).as_posix(),
172
+ "line": line_number,
173
+ "text": stripped[:240],
174
+ }
175
+ )
176
+ return references
177
+
178
+
179
+ def _line_requires_workflow_file(line: str, relative_file: str) -> bool:
180
+ escaped = re.escape(relative_file)
181
+ return bool(
182
+ re.search(rf"\bpython3?\b[^\n]*{escaped}", line)
183
+ or re.search(rf"\[\s*!\s*-f\s+{escaped}\s*\]", line)
184
+ or re.search(rf"\btest\s+!\s+-f\s+{escaped}\b", line)
185
+ or re.search(rf"\btest\s+-f\s+{escaped}\b", line)
186
+ )
187
+
188
+
189
+ def render_mirror_removal_plan(
190
+ *,
191
+ repo_path: Path,
192
+ shadow_cycles: int,
193
+ required_shadow_cycles: int,
194
+ standalone_default_cycles: int,
195
+ required_standalone_default_cycles: int,
196
+ ) -> dict[str, Any]:
197
+ repo_path = repo_path.expanduser().resolve()
198
+ if not repo_path.is_dir():
199
+ raise ValueError(f"repo path is not a directory: {repo_path}")
200
+
201
+ support_files = _relative_existing_files(
202
+ repo_path,
203
+ (
204
+ "tools/code_mower",
205
+ "tools/code_mower_standalone_pin.env",
206
+ "tools/code_mower_standalone_shadow.sh",
207
+ ),
208
+ )
209
+ product_support_files = _relative_existing_files(
210
+ repo_path,
211
+ PRODUCT_SUPPORT_PATTERNS,
212
+ )
213
+ local_command = _default_local_command(repo_path)
214
+ mirrored_candidates = _glob_relative_files(
215
+ repo_path,
216
+ MIRRORED_IMPLEMENTATION_PATTERNS,
217
+ )
218
+ workflow_mirror_references = _workflow_file_references(
219
+ repo_path,
220
+ mirrored_candidates,
221
+ )
222
+ workflow_local_fallback_references = _workflow_local_fallback_references(
223
+ repo_path,
224
+ )
225
+ mirrors_absent = (
226
+ not mirrored_candidates
227
+ and not workflow_mirror_references
228
+ and not workflow_local_fallback_references
229
+ )
230
+ ready_for_shadow = {
231
+ "standalone_pin_file_present": "tools/code_mower_standalone_pin.env" in support_files,
232
+ "standalone_shadow_wrapper_present": "tools/code_mower_standalone_shadow.sh"
233
+ in support_files,
234
+ "product_local_command_present": local_command is not None,
235
+ "mirrored_files_detected": bool(mirrored_candidates),
236
+ }
237
+ support_ready = (
238
+ ready_for_shadow["standalone_pin_file_present"]
239
+ and ready_for_shadow["standalone_shadow_wrapper_present"]
240
+ and ready_for_shadow["product_local_command_present"]
241
+ )
242
+ shadow_ready = support_ready and shadow_cycles >= required_shadow_cycles
243
+ cycle_ready_for_removal = (
244
+ shadow_ready and standalone_default_cycles >= required_standalone_default_cycles
245
+ )
246
+ removal_ready = (
247
+ cycle_ready_for_removal
248
+ and not workflow_mirror_references
249
+ and not workflow_local_fallback_references
250
+ )
251
+ blockers = []
252
+ if not mirrors_absent and not ready_for_shadow["standalone_pin_file_present"]:
253
+ blockers.append("add tools/code_mower_standalone_pin.env")
254
+ if not mirrors_absent and not ready_for_shadow["standalone_shadow_wrapper_present"]:
255
+ blockers.append("add tools/code_mower_standalone_shadow.sh")
256
+ if not mirrors_absent and not ready_for_shadow["product_local_command_present"]:
257
+ blockers.append("identify the product-local Code Mower command")
258
+ if not mirrors_absent and shadow_cycles < required_shadow_cycles:
259
+ blockers.append(
260
+ f"complete {required_shadow_cycles - shadow_cycles} more clean shadow cycle(s)"
261
+ )
262
+ if shadow_ready and standalone_default_cycles < required_standalone_default_cycles:
263
+ blockers.append(
264
+ "flip to pinned standalone by default and complete "
265
+ f"{required_standalone_default_cycles - standalone_default_cycles} "
266
+ "clean standalone-default cycle(s)"
267
+ )
268
+ if workflow_mirror_references:
269
+ blockers.append(
270
+ "migrate workflow references from removable mirrored files to "
271
+ "standalone wrapper commands before deleting mirrors"
272
+ )
273
+ if workflow_local_fallback_references:
274
+ blockers.append(
275
+ "remove CODE_MOWER_USE_LOCAL=1 workflow fallback calls before "
276
+ "deleting mirrors; private repos need a public/package install path "
277
+ "or authenticated standalone checkout for Actions"
278
+ )
279
+ if mirrors_absent and support_ready:
280
+ status = "mirrors_removed"
281
+ elif mirrors_absent:
282
+ status = "no_mirrors_detected"
283
+ elif removal_ready:
284
+ status = "ready_to_remove_mirrors"
285
+ elif cycle_ready_for_removal and workflow_local_fallback_references:
286
+ status = "local_fallback_dependency_blocks_mirror_removal"
287
+ elif cycle_ready_for_removal:
288
+ status = "workflow_entrypoint_migration_required"
289
+ elif shadow_ready:
290
+ status = "ready_to_flip_default"
291
+ else:
292
+ status = "shadow_required"
293
+ return {
294
+ "mode": "code-mower-mirror-removal-plan",
295
+ "repo_path": str(repo_path),
296
+ "status": status,
297
+ "shadow_cycles": shadow_cycles,
298
+ "required_shadow_cycles": required_shadow_cycles,
299
+ "standalone_default_cycles": standalone_default_cycles,
300
+ "required_standalone_default_cycles": required_standalone_default_cycles,
301
+ "support_files": support_files,
302
+ "support_file_count": len(support_files),
303
+ "product_support_files": product_support_files,
304
+ "product_support_file_count": len(product_support_files),
305
+ "local_command": list(local_command or ()),
306
+ "mirrored_file_count": len(mirrored_candidates),
307
+ "mirrored_files": mirrored_candidates,
308
+ "workflow_mirrored_file_reference_count": len(workflow_mirror_references),
309
+ "workflow_mirrored_file_references": workflow_mirror_references,
310
+ "workflow_local_fallback_reference_count": len(workflow_local_fallback_references),
311
+ "workflow_local_fallback_references": workflow_local_fallback_references,
312
+ "readiness": ready_for_shadow,
313
+ "mirrors_absent": mirrors_absent,
314
+ "blockers": blockers,
315
+ "steps": [
316
+ "Run code-mower migration wrapper-rehearsal against the pinned standalone release and require mismatch_count: 0.",
317
+ "Run at least the required number of clean product release cycles with the pinned standalone wrapper available.",
318
+ "Move workflow calls from mirrored Python files to tools/code_mower standalone wrapper subcommands.",
319
+ "Flip product workflows or wrapper defaults to the pinned standalone command while keeping the product-local mirrors in place.",
320
+ "For private standalone repos, configure authenticated Actions checkout or wait for a public/package install path before removing local mirrors.",
321
+ "Run the normal product merge gates and post-merge deploy checks.",
322
+ "Remove mirrored implementation files in a dedicated PR after the standalone default cycle stays clean.",
323
+ ],
324
+ "notes": [
325
+ "This plan is intentionally conservative: mirrored files are inventory, not deletion approval.",
326
+ "Keep support wrappers such as tools/code_mower and the standalone pin/shadow files during mirror removal.",
327
+ "Product-specific support files may remain after mirrored implementation files are removed.",
328
+ "CODE_MOWER_USE_LOCAL=1 workflow calls are allowed for private-repo safety, but they intentionally depend on repo-local mirror files.",
329
+ "Keep product feature work independent from mirror-removal PRs.",
330
+ ],
331
+ }
332
+
333
+
334
+ def render_mirror_removal_text(payload: dict[str, Any]) -> str:
335
+ lines = [
336
+ "Code Mower mirror-removal plan",
337
+ f"Status: {payload['status']}",
338
+ f"Repo: {payload['repo_path']}",
339
+ f"Shadow cycles: {payload['shadow_cycles']}/{payload['required_shadow_cycles']}",
340
+ "Standalone-default cycles: "
341
+ f"{payload['standalone_default_cycles']}/"
342
+ f"{payload['required_standalone_default_cycles']}",
343
+ f"Mirrored files detected: {payload['mirrored_file_count']}",
344
+ "Workflow mirrored-file references: "
345
+ f"{payload.get('workflow_mirrored_file_reference_count', 0)}",
346
+ "Workflow local-fallback references: "
347
+ f"{payload.get('workflow_local_fallback_reference_count', 0)}",
348
+ "",
349
+ "Next steps:",
350
+ ]
351
+ for step in payload["steps"]:
352
+ lines.append(f"- {step}")
353
+ if payload.get("mirrors_absent"):
354
+ lines.append("")
355
+ lines.append(
356
+ "Mirror inventory is empty: no removable mirrored implementation files "
357
+ "or workflow references were detected."
358
+ )
359
+ if payload["blockers"]:
360
+ lines.append("")
361
+ lines.append("Blockers:")
362
+ for blocker in payload["blockers"]:
363
+ lines.append(f"- {blocker}")
364
+ return "\n".join(lines) + "\n"
365
+
366
+
367
+ def render_runner_aliases(*, legacy: str | None = None) -> dict[str, Any]:
368
+ aliases = [dict(row) for row in RUNNER_ALIASES]
369
+ if legacy:
370
+ needle = legacy.strip()
371
+ aliases = [
372
+ row for row in aliases if row["legacy"] == needle or Path(row["legacy"]).name == needle
373
+ ]
374
+ return {
375
+ "mode": "code-mower-runner-aliases",
376
+ "status": "pass",
377
+ "aliases": aliases,
378
+ }
379
+
380
+
381
+ def render_runner_aliases_text(payload: dict[str, Any]) -> str:
382
+ lines = ["Code Mower runner aliases", ""]
383
+ aliases = payload.get("aliases", [])
384
+ if not aliases:
385
+ lines.append("No aliases matched.")
386
+ return "\n".join(lines) + "\n"
387
+ for row in aliases:
388
+ standalone = row.get("standalone") or "(no generic standalone alias)"
389
+ lines.append(f"- {row['legacy']} -> {standalone}")
390
+ lines.append(f" status: {row['status']}")
391
+ lines.append(f" notes: {row['notes']}")
392
+ return "\n".join(lines) + "\n"