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,508 @@
1
+ #!/usr/bin/env python3
2
+ """Rehearse migration from product-local Code Mower tools to the package."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ from dataclasses import asdict
12
+ from pathlib import Path
13
+ from typing import Any, Sequence
14
+
15
+ if __package__ in {None, ""}:
16
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
17
+ from code_mower.migration_mirror import (
18
+ PRODUCT_SUPPORT_PATTERNS,
19
+ RUNNER_ALIASES,
20
+ _default_local_command,
21
+ _line_requires_workflow_file,
22
+ _relative_existing_files,
23
+ _workflow_file_references,
24
+ _workflow_local_fallback_references,
25
+ render_mirror_removal_plan,
26
+ render_mirror_removal_text,
27
+ render_runner_aliases,
28
+ render_runner_aliases_text,
29
+ )
30
+ from code_mower.migration_rehearsal import (
31
+ FIRST_USER_ARTIFACTS,
32
+ MIRRORED_IMPLEMENTATION_PATTERNS,
33
+ PRIVACY_EXCLUDED_CONTENT,
34
+ CommandResult,
35
+ RehearsalError,
36
+ RunOutput,
37
+ _default_product_rehearsal_local_command,
38
+ _first_user_artifacts,
39
+ _first_user_readiness_scorecard,
40
+ _glob_relative_files,
41
+ _json_payload,
42
+ _load_release_readiness,
43
+ _pip_install_command,
44
+ _resolve_python_executable,
45
+ _resolve_install_package_spec,
46
+ _run,
47
+ _run_rehearsal_step,
48
+ _run_rehearsal_step_to_file,
49
+ render_package_install_rehearsal_text,
50
+ run_package_install_rehearsal,
51
+ )
52
+ else:
53
+ try:
54
+ from .migration_mirror import (
55
+ PRODUCT_SUPPORT_PATTERNS,
56
+ RUNNER_ALIASES,
57
+ _default_local_command,
58
+ _line_requires_workflow_file,
59
+ _relative_existing_files,
60
+ _workflow_file_references,
61
+ _workflow_local_fallback_references,
62
+ render_mirror_removal_plan,
63
+ render_mirror_removal_text,
64
+ render_runner_aliases,
65
+ render_runner_aliases_text,
66
+ )
67
+ from .migration_rehearsal import (
68
+ FIRST_USER_ARTIFACTS,
69
+ MIRRORED_IMPLEMENTATION_PATTERNS,
70
+ PRIVACY_EXCLUDED_CONTENT,
71
+ CommandResult,
72
+ RehearsalError,
73
+ RunOutput,
74
+ _default_product_rehearsal_local_command,
75
+ _first_user_artifacts,
76
+ _first_user_readiness_scorecard,
77
+ _glob_relative_files,
78
+ _json_payload,
79
+ _load_release_readiness,
80
+ _pip_install_command,
81
+ _resolve_python_executable,
82
+ _resolve_install_package_spec,
83
+ _run,
84
+ _run_rehearsal_step,
85
+ _run_rehearsal_step_to_file,
86
+ render_package_install_rehearsal_text,
87
+ run_package_install_rehearsal,
88
+ )
89
+ except ImportError:
90
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
91
+ from code_mower.migration_mirror import (
92
+ PRODUCT_SUPPORT_PATTERNS,
93
+ RUNNER_ALIASES,
94
+ _default_local_command,
95
+ _line_requires_workflow_file,
96
+ _relative_existing_files,
97
+ _workflow_file_references,
98
+ _workflow_local_fallback_references,
99
+ render_mirror_removal_plan,
100
+ render_mirror_removal_text,
101
+ render_runner_aliases,
102
+ render_runner_aliases_text,
103
+ )
104
+ from code_mower.migration_rehearsal import (
105
+ FIRST_USER_ARTIFACTS,
106
+ MIRRORED_IMPLEMENTATION_PATTERNS,
107
+ PRIVACY_EXCLUDED_CONTENT,
108
+ CommandResult,
109
+ RehearsalError,
110
+ RunOutput,
111
+ _default_product_rehearsal_local_command,
112
+ _first_user_artifacts,
113
+ _first_user_readiness_scorecard,
114
+ _glob_relative_files,
115
+ _json_payload,
116
+ _load_release_readiness,
117
+ _pip_install_command,
118
+ _resolve_python_executable,
119
+ _resolve_install_package_spec,
120
+ _run,
121
+ _run_rehearsal_step,
122
+ _run_rehearsal_step_to_file,
123
+ render_package_install_rehearsal_text,
124
+ run_package_install_rehearsal,
125
+ )
126
+
127
+ __all__ = [
128
+ "FIRST_USER_ARTIFACTS",
129
+ "MIRRORED_IMPLEMENTATION_PATTERNS",
130
+ "PRIVACY_EXCLUDED_CONTENT",
131
+ "PRODUCT_SUPPORT_PATTERNS",
132
+ "RUNNER_ALIASES",
133
+ "CommandResult",
134
+ "RehearsalError",
135
+ "RunOutput",
136
+ "_default_local_command",
137
+ "_default_product_rehearsal_local_command",
138
+ "_first_user_artifacts",
139
+ "_first_user_readiness_scorecard",
140
+ "_glob_relative_files",
141
+ "_json_payload",
142
+ "_line_requires_workflow_file",
143
+ "_load_release_readiness",
144
+ "_pip_install_command",
145
+ "_relative_existing_files",
146
+ "_resolve_python_executable",
147
+ "_resolve_install_package_spec",
148
+ "_run",
149
+ "_run_rehearsal_step",
150
+ "_run_rehearsal_step_to_file",
151
+ "_workflow_file_references",
152
+ "_workflow_local_fallback_references",
153
+ "render_mirror_removal_plan",
154
+ "render_mirror_removal_text",
155
+ "render_package_install_rehearsal_text",
156
+ "render_runner_aliases",
157
+ "render_runner_aliases_text",
158
+ "run_package_install_rehearsal",
159
+ ]
160
+
161
+
162
+ DEFAULT_COMMANDS = (
163
+ ("providers", "list"),
164
+ (
165
+ "prompts",
166
+ "validate",
167
+ "--lenses",
168
+ "base-audit,calibration-policy,package-runtime",
169
+ "--json",
170
+ ),
171
+ )
172
+ CALIBRATION_CANDIDATES = (
173
+ ".code-mower.generated/calibration-corpus.json",
174
+ "tools/calibration_corpus.json",
175
+ "tools/calibration_corpus.example.json",
176
+ "templates/calibration-corpus.json",
177
+ )
178
+ CALIBRATION_EVIDENCE_ADDITIVE_KEYS = frozenset(
179
+ {
180
+ "audit_input_insufficient_count",
181
+ "audit_input_insufficient_runs",
182
+ "result_category",
183
+ }
184
+ )
185
+
186
+
187
+ def _resolve_command(command_text: str) -> tuple[str, ...]:
188
+ parts = tuple(part for part in command_text.split(" ") if part)
189
+ if not parts:
190
+ raise ValueError("command must not be empty")
191
+ return parts
192
+
193
+
194
+ def _default_package_command() -> tuple[str, ...]:
195
+ resolved = shutil.which("code-mower")
196
+ return (resolved or "code-mower",)
197
+
198
+
199
+ def _prune_additive_calibration_keys(value: Any) -> Any:
200
+ if isinstance(value, dict):
201
+ return {
202
+ key: _prune_additive_calibration_keys(item)
203
+ for key, item in value.items()
204
+ if key not in CALIBRATION_EVIDENCE_ADDITIVE_KEYS
205
+ }
206
+ if isinstance(value, list):
207
+ return [_prune_additive_calibration_keys(item) for item in value]
208
+ return value
209
+
210
+
211
+ def _compatibility_for(
212
+ suffix: Sequence[str],
213
+ local: RunOutput,
214
+ package: RunOutput,
215
+ ) -> tuple[bool, str]:
216
+ if local.public.returncode != package.public.returncode:
217
+ return False, "returncode_mismatch"
218
+ if local.public.stdout_sha256 == package.public.stdout_sha256:
219
+ return True, "exact_stdout_match"
220
+ if tuple(suffix) == ("providers", "list"):
221
+ local_providers = {line.strip() for line in local.stdout.splitlines() if line.strip()}
222
+ package_providers = {line.strip() for line in package.stdout.splitlines() if line.strip()}
223
+ if local_providers and local_providers <= package_providers:
224
+ return True, "package_provider_superset"
225
+ if suffix[:2] == ("prompts", "validate"):
226
+ local_payload = _json_payload(local.stdout)
227
+ package_payload = _json_payload(package.stdout)
228
+ if isinstance(local_payload, dict) and isinstance(package_payload, dict):
229
+ local_payload.pop("prompt_dir", None)
230
+ package_payload.pop("prompt_dir", None)
231
+ if local_payload == package_payload:
232
+ return True, "prompt_dir_only_difference"
233
+ if suffix[:2] == ("calibration", "evidence"):
234
+ local_payload = _json_payload(local.stdout)
235
+ package_payload = _json_payload(package.stdout)
236
+ if (
237
+ isinstance(local_payload, dict)
238
+ and isinstance(package_payload, dict)
239
+ and _prune_additive_calibration_keys(local_payload)
240
+ == _prune_additive_calibration_keys(package_payload)
241
+ ):
242
+ return True, "calibration_evidence_additive_schema_only"
243
+ return False, "stdout_mismatch"
244
+
245
+
246
+ def _safe_commands(repo_path: Path) -> list[tuple[str, ...]]:
247
+ commands = list(DEFAULT_COMMANDS)
248
+ for candidate in CALIBRATION_CANDIDATES:
249
+ if (repo_path / candidate).is_file():
250
+ commands.append(("calibration", "evidence", candidate, "--json"))
251
+ break
252
+ return commands
253
+
254
+
255
+ def run_wrapper_rehearsal(
256
+ *,
257
+ repo_path: Path,
258
+ local_command: Sequence[str] | None = None,
259
+ package_command: Sequence[str] | None = None,
260
+ timeout: int = 60,
261
+ ) -> dict[str, Any]:
262
+ repo_path = repo_path.expanduser().resolve()
263
+ if not repo_path.is_dir():
264
+ raise ValueError(f"repo path is not a directory: {repo_path}")
265
+ local_command = tuple(local_command) if local_command else _default_local_command(repo_path)
266
+ package_command = tuple(package_command or _default_package_command())
267
+ if not local_command:
268
+ raise ValueError("could not infer local product Code Mower command; pass --local-command")
269
+
270
+ comparisons: list[dict[str, Any]] = []
271
+ for suffix in _safe_commands(repo_path):
272
+ local = _run((*local_command, *suffix), cwd=repo_path, timeout=timeout)
273
+ package = _run((*package_command, *suffix), cwd=repo_path, timeout=timeout)
274
+ match, reason = _compatibility_for(suffix, local, package)
275
+ comparisons.append(
276
+ {
277
+ "suffix": list(suffix),
278
+ "match": match,
279
+ "reason": reason,
280
+ "local": asdict(local.public),
281
+ "package": asdict(package.public),
282
+ }
283
+ )
284
+
285
+ mismatches = [item for item in comparisons if not item["match"]]
286
+ return {
287
+ "mode": "code-mower-product-wrapper-rehearsal",
288
+ "status": "pass" if not mismatches else "warn",
289
+ "repo_path": str(repo_path),
290
+ "local_command": list(local_command),
291
+ "package_command": list(package_command),
292
+ "comparison_count": len(comparisons),
293
+ "mismatch_count": len(mismatches),
294
+ "comparisons": comparisons,
295
+ "notes": [
296
+ "Only read-only commands are compared.",
297
+ "A pass means this repo is a candidate for CODE_MOWER_USE_STANDALONE shadow mode, not that local tools can be deleted yet.",
298
+ ],
299
+ }
300
+
301
+
302
+ def render_text(payload: dict[str, Any]) -> str:
303
+ lines = [
304
+ "Code Mower product wrapper rehearsal",
305
+ f"Status: {payload['status']}",
306
+ f"Repo: {payload['repo_path']}",
307
+ f"Comparisons: {payload['comparison_count']} ({payload['mismatch_count']} mismatches)",
308
+ "",
309
+ ]
310
+ for item in payload["comparisons"]:
311
+ status = "PASS" if item["match"] else "WARN"
312
+ lines.append(f"- {status} {' '.join(item['suffix'])}")
313
+ return "\n".join(lines) + "\n"
314
+
315
+
316
+ def main(argv: Sequence[str] | None = None) -> int:
317
+ parser = argparse.ArgumentParser(description=__doc__)
318
+ subparsers = parser.add_subparsers(dest="command", required=True)
319
+ wrapper = subparsers.add_parser("wrapper-rehearsal")
320
+ wrapper.add_argument("--repo-path", type=Path, default=Path.cwd())
321
+ wrapper.add_argument(
322
+ "--local-command",
323
+ default="",
324
+ help="product-local command prefix, e.g. 'python tools/code_mower_cli.py'",
325
+ )
326
+ wrapper.add_argument(
327
+ "--package-command",
328
+ default="",
329
+ help="standalone command prefix, e.g. 'code-mower'",
330
+ )
331
+ wrapper.add_argument("--timeout", type=int, default=60)
332
+ wrapper.add_argument("--json", action="store_true")
333
+ mirror = subparsers.add_parser("mirror-removal-plan")
334
+ mirror.add_argument("--repo-path", type=Path, default=Path.cwd())
335
+ mirror.add_argument("--shadow-cycles", type=int, default=0)
336
+ mirror.add_argument("--required-shadow-cycles", type=int, default=1)
337
+ mirror.add_argument("--standalone-default-cycles", type=int, default=0)
338
+ mirror.add_argument("--required-standalone-default-cycles", type=int, default=1)
339
+ mirror.add_argument("--json", action="store_true")
340
+ aliases = subparsers.add_parser("runner-aliases")
341
+ aliases.add_argument(
342
+ "--legacy",
343
+ default=None,
344
+ help="optional legacy script path or basename to filter, e.g. run_codex_audit_pr.sh",
345
+ )
346
+ aliases.add_argument("--json", action="store_true")
347
+ release = subparsers.add_parser("release-readiness")
348
+ release.add_argument("--repo-path", type=Path, default=Path.cwd())
349
+ release.add_argument("--json", action="store_true")
350
+ package_install = subparsers.add_parser("package-install-rehearsal")
351
+ package_install.add_argument(
352
+ "--package-spec",
353
+ default="code-mower",
354
+ help=(
355
+ "package spec to pip install into a clean venv; use a local path, "
356
+ "git URL, or package index name"
357
+ ),
358
+ )
359
+ package_install.add_argument(
360
+ "--repo-path",
361
+ type=Path,
362
+ default=None,
363
+ help="optional product repo to compare against the installed package",
364
+ )
365
+ package_install.add_argument(
366
+ "--local-command",
367
+ default="",
368
+ help=(
369
+ "product-local command prefix for --repo-path, e.g. "
370
+ "'env CODE_MOWER_USE_LOCAL=1 tools/code_mower'"
371
+ ),
372
+ )
373
+ package_install.add_argument(
374
+ "--python",
375
+ type=Path,
376
+ default=None,
377
+ help="Python 3.11+ executable used to create the clean rehearsal venv",
378
+ )
379
+ package_install.add_argument(
380
+ "--work-dir",
381
+ type=Path,
382
+ default=None,
383
+ help="empty or absent directory for venv, toy repo, and JSON outputs",
384
+ )
385
+ package_install.add_argument(
386
+ "--pip-index-url",
387
+ default="",
388
+ help="optional pip --index-url for package-install rehearsal",
389
+ )
390
+ package_install.add_argument(
391
+ "--pip-extra-index-url",
392
+ action="append",
393
+ default=[],
394
+ help="optional pip --extra-index-url; may be provided multiple times",
395
+ )
396
+ package_install.add_argument("--timeout", type=int, default=180)
397
+ package_install.add_argument("--shadow-cycles", type=int, default=1)
398
+ package_install.add_argument("--standalone-default-cycles", type=int, default=1)
399
+ package_install.add_argument("--json", action="store_true")
400
+ args = parser.parse_args(argv)
401
+
402
+ if args.command == "wrapper-rehearsal":
403
+ try:
404
+ payload = run_wrapper_rehearsal(
405
+ repo_path=args.repo_path,
406
+ local_command=_resolve_command(args.local_command) if args.local_command else None,
407
+ package_command=_resolve_command(args.package_command)
408
+ if args.package_command
409
+ else None,
410
+ timeout=args.timeout,
411
+ )
412
+ except (OSError, subprocess.TimeoutExpired, ValueError) as exc:
413
+ payload = {
414
+ "mode": "code-mower-product-wrapper-rehearsal",
415
+ "status": "fail",
416
+ "error": str(exc),
417
+ }
418
+ if args.json:
419
+ print(json.dumps(payload, indent=2, sort_keys=True))
420
+ else:
421
+ print(f"wrapper rehearsal failed: {exc}", file=sys.stderr)
422
+ return 1
423
+ if args.json:
424
+ print(json.dumps(payload, indent=2, sort_keys=True))
425
+ else:
426
+ print(render_text(payload), end="")
427
+ return 0 if payload["status"] == "pass" else 1
428
+
429
+ if args.command == "mirror-removal-plan":
430
+ try:
431
+ payload = render_mirror_removal_plan(
432
+ repo_path=args.repo_path,
433
+ shadow_cycles=args.shadow_cycles,
434
+ required_shadow_cycles=args.required_shadow_cycles,
435
+ standalone_default_cycles=args.standalone_default_cycles,
436
+ required_standalone_default_cycles=args.required_standalone_default_cycles,
437
+ )
438
+ except ValueError as exc:
439
+ payload = {
440
+ "mode": "code-mower-mirror-removal-plan",
441
+ "status": "fail",
442
+ "error": str(exc),
443
+ }
444
+ if args.json:
445
+ print(json.dumps(payload, indent=2, sort_keys=True))
446
+ else:
447
+ print(f"mirror-removal plan failed: {exc}", file=sys.stderr)
448
+ return 1
449
+ if args.json:
450
+ print(json.dumps(payload, indent=2, sort_keys=True))
451
+ else:
452
+ print(render_mirror_removal_text(payload), end="")
453
+ return 0
454
+
455
+ if args.command == "runner-aliases":
456
+ payload = render_runner_aliases(legacy=args.legacy)
457
+ if args.json:
458
+ print(json.dumps(payload, indent=2, sort_keys=True))
459
+ else:
460
+ print(render_runner_aliases_text(payload), end="")
461
+ return 0
462
+
463
+ if args.command == "release-readiness":
464
+ release_readiness = _load_release_readiness()
465
+ payload = release_readiness.render_release_readiness(repo_path=args.repo_path)
466
+ if args.json:
467
+ print(json.dumps(payload, indent=2, sort_keys=True))
468
+ else:
469
+ print(release_readiness.render_release_readiness_text(payload), end="")
470
+ return 0 if payload["status"] == "pass" else 1
471
+
472
+ if args.command == "package-install-rehearsal":
473
+ try:
474
+ payload = run_package_install_rehearsal(
475
+ package_spec=args.package_spec,
476
+ repo_path=args.repo_path,
477
+ local_command=_resolve_command(args.local_command) if args.local_command else None,
478
+ python=args.python,
479
+ work_dir=args.work_dir,
480
+ timeout=args.timeout,
481
+ shadow_cycles=args.shadow_cycles,
482
+ standalone_default_cycles=args.standalone_default_cycles,
483
+ pip_index_url=args.pip_index_url,
484
+ pip_extra_index_urls=args.pip_extra_index_url,
485
+ )
486
+ except (OSError, subprocess.TimeoutExpired, ValueError, RehearsalError) as exc:
487
+ payload = {
488
+ "mode": "code-mower-package-install-rehearsal",
489
+ "status": "fail",
490
+ "error": str(exc),
491
+ "steps": getattr(exc, "steps", []),
492
+ }
493
+ if args.json:
494
+ print(json.dumps(payload, indent=2, sort_keys=True))
495
+ else:
496
+ print(f"package-install rehearsal failed: {exc}", file=sys.stderr)
497
+ return 1
498
+ if args.json:
499
+ print(json.dumps(payload, indent=2, sort_keys=True))
500
+ else:
501
+ print(render_package_install_rehearsal_text(payload), end="")
502
+ return 0
503
+
504
+ raise AssertionError(f"unhandled migration command: {args.command}")
505
+
506
+
507
+ if __name__ == "__main__":
508
+ raise SystemExit(main())