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,46 @@
1
+ """Safe Git metadata helpers for Code Mower Cloud uploads."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+
9
+ def run_git(repo_path: Path, args: list[str]) -> str:
10
+ """Return stdout for a best-effort git command, or an empty string."""
11
+ try:
12
+ completed = subprocess.run(
13
+ ["git", *args],
14
+ cwd=repo_path,
15
+ text=True,
16
+ capture_output=True,
17
+ check=False,
18
+ )
19
+ except OSError:
20
+ return ""
21
+ if completed.returncode != 0:
22
+ return ""
23
+ return completed.stdout.strip()
24
+
25
+
26
+ def repo_slug_from_remote(remote_url: str) -> str:
27
+ remote = remote_url.strip()
28
+ if not remote:
29
+ return ""
30
+ if remote.startswith("git@github.com:"):
31
+ remote = remote.removeprefix("git@github.com:")
32
+ elif remote.startswith("https://github.com/"):
33
+ remote = remote.removeprefix("https://github.com/")
34
+ elif remote.startswith("http://github.com/"):
35
+ remote = remote.removeprefix("http://github.com/")
36
+ else:
37
+ return ""
38
+ remote = remote.removesuffix(".git").strip("/")
39
+ parts = remote.split("/")
40
+ if len(parts) >= 2 and parts[0] and parts[1]:
41
+ return f"{parts[0]}/{parts[1]}"
42
+ return ""
43
+
44
+
45
+ def detect_repo_slug(repo_path: Path) -> str:
46
+ return repo_slug_from_remote(run_git(repo_path, ["config", "--get", "remote.origin.url"]))
@@ -0,0 +1,39 @@
1
+ """Bundle manifest loading and path validation helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .bundle import BUNDLE_MANIFEST_FILENAME, is_bundle_manifest
10
+ from .errors import CloudBundleError
11
+
12
+
13
+ def load_bundle_manifest(bundle_dir: Path) -> dict[str, Any]:
14
+ manifest_path = bundle_dir / BUNDLE_MANIFEST_FILENAME
15
+ if not manifest_path.is_file():
16
+ raise CloudBundleError(f"bundle manifest not found: {manifest_path}")
17
+ try:
18
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
19
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
20
+ raise CloudBundleError(f"unable to read bundle manifest {manifest_path}: {exc}") from exc
21
+ if not is_bundle_manifest(manifest):
22
+ raise CloudBundleError(f"unsupported bundle manifest schema in {manifest_path}")
23
+ return manifest
24
+
25
+
26
+ def report_path_from_manifest(bundle_dir: Path, target: str) -> Path:
27
+ if not target or target.startswith("/") or ".." in Path(target).parts:
28
+ raise CloudBundleError(f"unsafe report target in bundle manifest: {target!r}")
29
+ path = bundle_dir / target
30
+ try:
31
+ resolved = path.resolve()
32
+ bundle_resolved = bundle_dir.resolve()
33
+ except OSError as exc:
34
+ raise CloudBundleError(f"unable to resolve bundle report path {path}: {exc}") from exc
35
+ if not resolved.is_relative_to(bundle_resolved):
36
+ raise CloudBundleError(f"report target escapes bundle directory: {target!r}")
37
+ if not resolved.is_file():
38
+ raise CloudBundleError(f"bundle report file is missing: {target!r}")
39
+ return resolved
@@ -0,0 +1,448 @@
1
+ """High-level CodeMower.com upload operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .. import code_mower_telemetry
10
+ from .bundle import MAX_EVENT_COUNT
11
+ from .doctor import run_cloud_doctor
12
+ from .dogfood import build_dogfood_dry_run_preview, build_dogfood_plan, default_dogfood_reports
13
+ from .endpoints import is_local_http_endpoint
14
+ from .errors import CloudBundleError
15
+ from .events import (
16
+ build_dogfood_event,
17
+ build_workflow_run_event,
18
+ run_gh_run_list,
19
+ )
20
+ from .export import build_cloud_bundle
21
+ from .git_metadata import detect_repo_slug
22
+ from .setup import DEFAULT_INSTALL_ID_ENV, DEFAULT_TEAM_ID_ENV
23
+ from .upload import build_upload_payload, post_upload_payload
24
+
25
+
26
+ def dogfood_upload(
27
+ *,
28
+ repo_path: Path,
29
+ output_dir: Path,
30
+ reports: list[tuple[Path, str]],
31
+ events: list[dict[str, Any]],
32
+ repo_slug: str,
33
+ team_id: str,
34
+ install_id: str,
35
+ source: str,
36
+ endpoint: str,
37
+ token_env: str,
38
+ include_reports: bool,
39
+ yes: bool,
40
+ timeout: float,
41
+ ) -> dict[str, Any]:
42
+ repo_path = repo_path.expanduser().resolve()
43
+ detected_repo_slug = repo_slug or detect_repo_slug(repo_path)
44
+ if not detected_repo_slug:
45
+ raise CloudBundleError(
46
+ "unable to detect repo slug; pass --repo-slug OWNER/REPO"
47
+ )
48
+ resolved_team_id = team_id or os.environ.get(DEFAULT_TEAM_ID_ENV, "")
49
+ resolved_install_id = install_id or os.environ.get(DEFAULT_INSTALL_ID_ENV, "")
50
+ resolved_reports = reports or default_dogfood_reports(repo_path)
51
+ dogfood_plan = build_dogfood_plan(
52
+ repo_slug=detected_repo_slug,
53
+ team_id=resolved_team_id,
54
+ install_id=resolved_install_id,
55
+ source=source,
56
+ reports=resolved_reports,
57
+ events=events,
58
+ )
59
+ all_events = [
60
+ build_dogfood_event(
61
+ repo_path=repo_path,
62
+ plan=dogfood_plan,
63
+ ),
64
+ *events,
65
+ ]
66
+ export_result = build_cloud_bundle(
67
+ reports=resolved_reports,
68
+ events=all_events,
69
+ output_dir=output_dir,
70
+ repo_slug=detected_repo_slug,
71
+ team_id=resolved_team_id,
72
+ install_id=resolved_install_id,
73
+ anonymous=False,
74
+ )
75
+ doctor_result = run_cloud_doctor(
76
+ bundle_dir=output_dir,
77
+ endpoint=endpoint,
78
+ token_env=token_env,
79
+ require_token=yes,
80
+ )
81
+ if doctor_result["failures"]:
82
+ return {
83
+ "mode": "cloud-dogfood",
84
+ "status": "doctor_failed",
85
+ "export": export_result,
86
+ "doctor": doctor_result,
87
+ }
88
+ payload = build_upload_payload(
89
+ bundle_dir=output_dir,
90
+ include_reports=include_reports,
91
+ )
92
+ if not yes:
93
+ return {
94
+ "mode": "cloud-dogfood",
95
+ "status": "dry_run",
96
+ "export": export_result,
97
+ "doctor": doctor_result,
98
+ "upload": build_dogfood_dry_run_preview(endpoint=endpoint, payload=payload),
99
+ }
100
+ token = os.environ.get(token_env, "")
101
+ if not token and not is_local_http_endpoint(endpoint):
102
+ raise CloudBundleError(
103
+ f"{token_env} is not set; refusing non-local upload without a token"
104
+ )
105
+ return {
106
+ "mode": "cloud-dogfood",
107
+ "status": "uploaded",
108
+ "export": export_result,
109
+ "doctor": doctor_result,
110
+ "upload": post_upload_payload(
111
+ payload=payload,
112
+ endpoint=endpoint,
113
+ token=token,
114
+ timeout=timeout,
115
+ ),
116
+ }
117
+
118
+
119
+ def catch_up_upload(
120
+ *,
121
+ repo_path: Path,
122
+ output_dir: Path,
123
+ repo_slug: str,
124
+ team_id: str,
125
+ install_id: str,
126
+ source: str,
127
+ limit: int,
128
+ endpoint: str,
129
+ token_env: str,
130
+ yes: bool,
131
+ timeout: float,
132
+ include_git_ref: bool,
133
+ ) -> dict[str, Any]:
134
+ if limit < 1 or limit > MAX_EVENT_COUNT:
135
+ raise CloudBundleError(f"--limit must be between 1 and {MAX_EVENT_COUNT}")
136
+ repo_path = repo_path.expanduser().resolve()
137
+ detected_repo_slug = repo_slug or detect_repo_slug(repo_path)
138
+ if not detected_repo_slug:
139
+ raise CloudBundleError(
140
+ "unable to detect repo slug; pass --repo-slug OWNER/REPO"
141
+ )
142
+ resolved_team_id = team_id or os.environ.get(DEFAULT_TEAM_ID_ENV, "")
143
+ resolved_install_id = install_id or os.environ.get(DEFAULT_INSTALL_ID_ENV, "")
144
+ runs = run_gh_run_list(
145
+ repo_slug=detected_repo_slug,
146
+ limit=limit,
147
+ repo_path=repo_path,
148
+ )
149
+ events = [
150
+ build_workflow_run_event(
151
+ repo_slug=detected_repo_slug,
152
+ team_id=resolved_team_id,
153
+ install_id=resolved_install_id,
154
+ source=source,
155
+ run=run,
156
+ include_git_ref=include_git_ref,
157
+ )
158
+ for run in runs
159
+ ]
160
+ export_result = build_cloud_bundle(
161
+ reports=[],
162
+ events=events,
163
+ output_dir=output_dir,
164
+ repo_slug=detected_repo_slug,
165
+ team_id=resolved_team_id,
166
+ install_id=resolved_install_id,
167
+ anonymous=False,
168
+ )
169
+ doctor_result = run_cloud_doctor(
170
+ bundle_dir=output_dir,
171
+ endpoint=endpoint,
172
+ token_env=token_env,
173
+ require_token=yes,
174
+ )
175
+ if doctor_result["failures"]:
176
+ return {
177
+ "mode": "cloud-catch-up",
178
+ "status": "doctor_failed",
179
+ "repo_slug": detected_repo_slug,
180
+ "run_count": len(runs),
181
+ "export": export_result,
182
+ "doctor": doctor_result,
183
+ }
184
+ payload = build_upload_payload(bundle_dir=output_dir, include_reports=False)
185
+ if not yes:
186
+ return {
187
+ "mode": "cloud-catch-up",
188
+ "status": "dry_run",
189
+ "repo_slug": detected_repo_slug,
190
+ "run_count": len(runs),
191
+ "export": export_result,
192
+ "doctor": doctor_result,
193
+ "upload": build_dogfood_dry_run_preview(endpoint=endpoint, payload=payload),
194
+ }
195
+ token = os.environ.get(token_env, "")
196
+ if not token and not is_local_http_endpoint(endpoint):
197
+ raise CloudBundleError(
198
+ f"{token_env} is not set; refusing non-local upload without a token"
199
+ )
200
+ return {
201
+ "mode": "cloud-catch-up",
202
+ "status": "uploaded",
203
+ "repo_slug": detected_repo_slug,
204
+ "run_count": len(runs),
205
+ "export": export_result,
206
+ "doctor": doctor_result,
207
+ "upload": post_upload_payload(
208
+ payload=payload,
209
+ endpoint=endpoint,
210
+ token=token,
211
+ timeout=timeout,
212
+ ),
213
+ }
214
+
215
+
216
+ def reviewer_runs_upload(
217
+ *,
218
+ repo_path: Path,
219
+ verdicts: Path,
220
+ output_dir: Path,
221
+ repo_slug: str,
222
+ team_id: str,
223
+ install_id: str,
224
+ limit: int,
225
+ endpoint: str,
226
+ token_env: str,
227
+ yes: bool,
228
+ timeout: float,
229
+ include_git_ref: bool,
230
+ ) -> dict[str, Any]:
231
+ if limit < 1 or limit > MAX_EVENT_COUNT:
232
+ raise CloudBundleError(f"--limit must be between 1 and {MAX_EVENT_COUNT}")
233
+ repo_path = repo_path.expanduser().resolve()
234
+ detected_repo_slug = repo_slug or detect_repo_slug(repo_path)
235
+ if not detected_repo_slug:
236
+ raise CloudBundleError(
237
+ "unable to detect repo slug; pass --repo-slug OWNER/REPO"
238
+ )
239
+ resolved_team_id = team_id or os.environ.get(DEFAULT_TEAM_ID_ENV, "")
240
+ resolved_install_id = install_id or os.environ.get(DEFAULT_INSTALL_ID_ENV, "")
241
+ try:
242
+ events = code_mower_telemetry.export_reviewer_run_events_from_verdicts(
243
+ verdicts,
244
+ repo=detected_repo_slug,
245
+ limit=limit,
246
+ include_git_ref=include_git_ref,
247
+ )
248
+ except ValueError as exc:
249
+ raise CloudBundleError(str(exc)) from exc
250
+ if not events:
251
+ return {
252
+ "mode": "cloud-reviewer-runs",
253
+ "status": "no_events",
254
+ "repo_slug": detected_repo_slug,
255
+ "event_count": 0,
256
+ "verdicts": str(verdicts.expanduser()),
257
+ "git_ref_included": include_git_ref,
258
+ }
259
+ export_result = build_cloud_bundle(
260
+ reports=[],
261
+ events=events,
262
+ output_dir=output_dir,
263
+ repo_slug=detected_repo_slug,
264
+ team_id=resolved_team_id,
265
+ install_id=resolved_install_id,
266
+ anonymous=False,
267
+ )
268
+ doctor_result = run_cloud_doctor(
269
+ bundle_dir=output_dir,
270
+ endpoint=endpoint,
271
+ token_env=token_env,
272
+ require_token=yes,
273
+ )
274
+ if doctor_result["failures"]:
275
+ return {
276
+ "mode": "cloud-reviewer-runs",
277
+ "status": "doctor_failed",
278
+ "repo_slug": detected_repo_slug,
279
+ "event_count": len(events),
280
+ "export": export_result,
281
+ "doctor": doctor_result,
282
+ }
283
+ payload = build_upload_payload(bundle_dir=output_dir, include_reports=False)
284
+ if not yes:
285
+ return {
286
+ "mode": "cloud-reviewer-runs",
287
+ "status": "dry_run",
288
+ "repo_slug": detected_repo_slug,
289
+ "event_count": len(events),
290
+ "export": export_result,
291
+ "doctor": doctor_result,
292
+ "upload": build_dogfood_dry_run_preview(endpoint=endpoint, payload=payload),
293
+ }
294
+ token = os.environ.get(token_env, "")
295
+ if not token and not is_local_http_endpoint(endpoint):
296
+ raise CloudBundleError(
297
+ f"{token_env} is not set; refusing non-local upload without a token"
298
+ )
299
+ return {
300
+ "mode": "cloud-reviewer-runs",
301
+ "status": "uploaded",
302
+ "repo_slug": detected_repo_slug,
303
+ "event_count": len(events),
304
+ "export": export_result,
305
+ "doctor": doctor_result,
306
+ "upload": post_upload_payload(
307
+ payload=payload,
308
+ endpoint=endpoint,
309
+ token=token,
310
+ timeout=timeout,
311
+ ),
312
+ }
313
+
314
+
315
+ def parse_repo_sync_spec(spec: str) -> tuple[str, Path]:
316
+ if "=" not in spec:
317
+ return "", Path(spec)
318
+ repo_slug, repo_path = spec.split("=", 1)
319
+ repo_slug = repo_slug.strip()
320
+ repo_path = repo_path.strip()
321
+ if not repo_slug or not repo_path:
322
+ raise CloudBundleError(
323
+ "--repo entries must be PATH or OWNER/REPO=PATH for repo-sync"
324
+ )
325
+ return repo_slug, Path(repo_path)
326
+
327
+
328
+ def repo_sync_output_name(repo_slug: str, repo_path: Path, index: int) -> str:
329
+ raw = repo_slug.replace("/", "__") if repo_slug else repo_path.name
330
+ cleaned = "".join(
331
+ ch.lower() if ch.isalnum() else "-" for ch in raw.strip()
332
+ ).strip("-")
333
+ return f"{cleaned or 'repo'}-{index + 1}"
334
+
335
+
336
+ def repo_sync_upload(
337
+ *,
338
+ repo_specs: list[str],
339
+ output_dir: Path,
340
+ modes: list[str],
341
+ team_id: str,
342
+ install_id: str,
343
+ source_prefix: str,
344
+ limit: int,
345
+ endpoint: str,
346
+ token_env: str,
347
+ include_reports: bool,
348
+ include_git_ref: bool,
349
+ yes: bool,
350
+ timeout: float,
351
+ ) -> dict[str, Any]:
352
+ selected_modes = modes or ["dogfood", "reviewer-runs"]
353
+ repos: list[dict[str, Any]] = []
354
+ error_count = 0
355
+ step_statuses: list[str] = []
356
+
357
+ for index, spec in enumerate(repo_specs):
358
+ repo_slug, repo_path = parse_repo_sync_spec(spec)
359
+ repo_path = repo_path.expanduser().resolve()
360
+ repo_output_dir = output_dir / repo_sync_output_name(repo_slug, repo_path, index)
361
+ repo_result: dict[str, Any] = {
362
+ "repo_spec": spec,
363
+ "repo_slug": repo_slug,
364
+ "repo_path": str(repo_path),
365
+ "steps": [],
366
+ }
367
+
368
+ for mode in selected_modes:
369
+ try:
370
+ if mode == "dogfood":
371
+ step_result = dogfood_upload(
372
+ repo_path=repo_path,
373
+ output_dir=repo_output_dir / "dogfood",
374
+ reports=[],
375
+ events=[],
376
+ repo_slug=repo_slug,
377
+ team_id=team_id,
378
+ install_id=install_id,
379
+ source=f"{source_prefix}-dogfood",
380
+ endpoint=endpoint,
381
+ token_env=token_env,
382
+ include_reports=include_reports,
383
+ yes=yes,
384
+ timeout=timeout,
385
+ )
386
+ elif mode == "catch-up":
387
+ step_result = catch_up_upload(
388
+ repo_path=repo_path,
389
+ output_dir=repo_output_dir / "catch-up",
390
+ repo_slug=repo_slug,
391
+ team_id=team_id,
392
+ install_id=install_id,
393
+ source=f"{source_prefix}-catch-up",
394
+ limit=limit,
395
+ endpoint=endpoint,
396
+ token_env=token_env,
397
+ yes=yes,
398
+ timeout=timeout,
399
+ include_git_ref=include_git_ref,
400
+ )
401
+ elif mode == "reviewer-runs":
402
+ step_result = reviewer_runs_upload(
403
+ repo_path=repo_path,
404
+ verdicts=code_mower_telemetry.default_verdict_artifact_dir(),
405
+ output_dir=repo_output_dir / "reviewer-runs",
406
+ repo_slug=repo_slug,
407
+ team_id=team_id,
408
+ install_id=install_id,
409
+ limit=limit,
410
+ endpoint=endpoint,
411
+ token_env=token_env,
412
+ yes=yes,
413
+ timeout=timeout,
414
+ include_git_ref=include_git_ref,
415
+ )
416
+ else: # pragma: no cover - argparse constrains modes.
417
+ raise CloudBundleError(f"unsupported repo-sync mode: {mode}")
418
+ except CloudBundleError as exc:
419
+ step_result = {
420
+ "mode": f"cloud-{mode}",
421
+ "status": "error",
422
+ "error": str(exc),
423
+ }
424
+ repo_result["repo_slug"] = repo_result["repo_slug"] or str(
425
+ step_result.get("repo_slug") or ""
426
+ )
427
+ repo_result["steps"].append(step_result)
428
+ step_status = str(step_result.get("status") or "")
429
+ step_statuses.append(step_status)
430
+ if step_result.get("status") in {"error", "doctor_failed"}:
431
+ error_count += 1
432
+
433
+ repos.append(repo_result)
434
+
435
+ status = "dry_run" if not yes else "uploaded"
436
+ if error_count:
437
+ status = "partial"
438
+ elif yes and not any(step_status == "uploaded" for step_status in step_statuses):
439
+ status = "no_events"
440
+ return {
441
+ "mode": "cloud-repo-sync",
442
+ "status": status,
443
+ "repo_count": len(repos),
444
+ "step_count": sum(len(repo["steps"]) for repo in repos),
445
+ "error_count": error_count,
446
+ "modes": selected_modes,
447
+ "repos": repos,
448
+ }
@@ -0,0 +1,45 @@
1
+ """Bundle report payload construction helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from .bundle import MAX_REPORT_UPLOAD_BYTES
9
+ from .errors import CloudBundleError
10
+ from .manifest import report_path_from_manifest
11
+
12
+
13
+ def included_report_payloads(
14
+ manifest: dict[str, Any],
15
+ bundle_dir: Path,
16
+ *,
17
+ include_reports: bool,
18
+ ) -> list[dict[str, Any]]:
19
+ payloads: list[dict[str, Any]] = []
20
+ for entry in manifest.get("included_reports", []):
21
+ if not isinstance(entry, dict):
22
+ raise CloudBundleError("bundle manifest has a non-object included_reports entry")
23
+ target = str(entry.get("target", ""))
24
+ report_payload = {
25
+ "kind": entry.get("kind", ""),
26
+ "target": target,
27
+ "bytes": entry.get("bytes", 0),
28
+ "source_basename": entry.get("source_basename", ""),
29
+ }
30
+ if include_reports:
31
+ path = report_path_from_manifest(bundle_dir, target)
32
+ size = path.stat().st_size
33
+ if size > MAX_REPORT_UPLOAD_BYTES:
34
+ raise CloudBundleError(
35
+ f"refusing to upload {target}: {size} bytes exceeds "
36
+ f"{MAX_REPORT_UPLOAD_BYTES} byte limit"
37
+ )
38
+ try:
39
+ report_payload["text"] = path.read_text(encoding="utf-8")
40
+ except UnicodeDecodeError as exc:
41
+ raise CloudBundleError(f"report is not UTF-8 text: {target}") from exc
42
+ except OSError as exc:
43
+ raise CloudBundleError(f"unable to read report {target}: {exc}") from exc
44
+ payloads.append(report_payload)
45
+ return payloads