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,173 @@
1
+ """CodeMower.com client internals."""
2
+
3
+ from .bundle import (
4
+ BUNDLE_MANIFEST_FILENAME,
5
+ BUNDLE_SCHEMA,
6
+ EXCLUDED_CONTENT,
7
+ EXPECTED_BUNDLE_ENTRIES,
8
+ MAX_EVENT_COUNT,
9
+ MAX_REPORT_UPLOAD_BYTES,
10
+ SAFE_EVENT_TYPES,
11
+ SAFE_REPORT_KINDS,
12
+ is_bundle_manifest,
13
+ validate_metadata_payload,
14
+ )
15
+ from .endpoints import (
16
+ DEFAULT_DASHBOARD_PATH,
17
+ DEFAULT_HEALTH_PATH,
18
+ dashboard_url_for_endpoint,
19
+ health_url_for_endpoint,
20
+ is_local_http_endpoint,
21
+ probe_cloud_service,
22
+ validate_upload_endpoint,
23
+ )
24
+ from .export import (
25
+ build_cloud_bundle,
26
+ existing_bundle_manifest,
27
+ plan_report,
28
+ render_bundle_readme,
29
+ safe_target_name,
30
+ stage_reports,
31
+ swap_reports,
32
+ unexpected_bundle_entries,
33
+ )
34
+ from .dogfood import (
35
+ DEFAULT_DOGFOOD_REPORTS,
36
+ DogfoodPlan,
37
+ build_dogfood_dry_run_preview,
38
+ build_dogfood_plan,
39
+ default_dogfood_reports,
40
+ )
41
+ from .doctor import (
42
+ render_cloud_doctor_text,
43
+ run_cloud_doctor,
44
+ )
45
+ from .events import (
46
+ EVENT_SCHEMA,
47
+ GITHUB_RUN_LIST_FIELDS,
48
+ build_dogfood_event,
49
+ build_workflow_run_event,
50
+ event_id_from_github_run,
51
+ load_event_file,
52
+ normalize_event,
53
+ parse_event_args,
54
+ run_gh_run_list,
55
+ safe_event_type,
56
+ safe_kind,
57
+ utc_now,
58
+ )
59
+ from .git_metadata import (
60
+ detect_repo_slug,
61
+ repo_slug_from_remote,
62
+ run_git,
63
+ )
64
+ from .manifest import (
65
+ load_bundle_manifest,
66
+ report_path_from_manifest,
67
+ )
68
+ from .reports import included_report_payloads
69
+ from .operations import (
70
+ catch_up_upload,
71
+ dogfood_upload,
72
+ parse_repo_sync_spec,
73
+ repo_sync_output_name,
74
+ repo_sync_upload,
75
+ reviewer_runs_upload,
76
+ )
77
+ from .errors import CloudBundleError
78
+ from .setup import (
79
+ DEFAULT_INSTALL_ID_ENV,
80
+ DEFAULT_SETUP_INSTALL_ID,
81
+ DEFAULT_TEAM_ID_ENV,
82
+ DEFAULT_TOKEN_ENV,
83
+ DEFAULT_UPLOAD_ENDPOINT,
84
+ default_setup_path,
85
+ read_token_file,
86
+ render_setup_env,
87
+ resolve_setup_token,
88
+ run_cloud_setup,
89
+ safe_config_stem,
90
+ token_prefix,
91
+ write_setup_env_file,
92
+ )
93
+ from .upload import (
94
+ UPLOAD_SCHEMA,
95
+ build_upload_payload,
96
+ post_upload_payload,
97
+ )
98
+
99
+ __all__ = [
100
+ "BUNDLE_MANIFEST_FILENAME",
101
+ "BUNDLE_SCHEMA",
102
+ "DEFAULT_DASHBOARD_PATH",
103
+ "DEFAULT_HEALTH_PATH",
104
+ "EXCLUDED_CONTENT",
105
+ "EXPECTED_BUNDLE_ENTRIES",
106
+ "DEFAULT_DOGFOOD_REPORTS",
107
+ "MAX_EVENT_COUNT",
108
+ "MAX_REPORT_UPLOAD_BYTES",
109
+ "SAFE_EVENT_TYPES",
110
+ "SAFE_REPORT_KINDS",
111
+ "UPLOAD_SCHEMA",
112
+ "CloudBundleError",
113
+ "DEFAULT_INSTALL_ID_ENV",
114
+ "DEFAULT_SETUP_INSTALL_ID",
115
+ "DEFAULT_TEAM_ID_ENV",
116
+ "DEFAULT_TOKEN_ENV",
117
+ "DEFAULT_UPLOAD_ENDPOINT",
118
+ "DogfoodPlan",
119
+ "EVENT_SCHEMA",
120
+ "GITHUB_RUN_LIST_FIELDS",
121
+ "build_dogfood_event",
122
+ "build_dogfood_dry_run_preview",
123
+ "build_dogfood_plan",
124
+ "build_workflow_run_event",
125
+ "build_cloud_bundle",
126
+ "build_upload_payload",
127
+ "catch_up_upload",
128
+ "dashboard_url_for_endpoint",
129
+ "default_setup_path",
130
+ "detect_repo_slug",
131
+ "default_dogfood_reports",
132
+ "dogfood_upload",
133
+ "event_id_from_github_run",
134
+ "existing_bundle_manifest",
135
+ "health_url_for_endpoint",
136
+ "included_report_payloads",
137
+ "is_local_http_endpoint",
138
+ "is_bundle_manifest",
139
+ "load_bundle_manifest",
140
+ "load_event_file",
141
+ "normalize_event",
142
+ "plan_report",
143
+ "parse_repo_sync_spec",
144
+ "parse_event_args",
145
+ "post_upload_payload",
146
+ "probe_cloud_service",
147
+ "read_token_file",
148
+ "render_bundle_readme",
149
+ "render_cloud_doctor_text",
150
+ "render_setup_env",
151
+ "repo_slug_from_remote",
152
+ "repo_sync_output_name",
153
+ "repo_sync_upload",
154
+ "report_path_from_manifest",
155
+ "resolve_setup_token",
156
+ "reviewer_runs_upload",
157
+ "run_cloud_doctor",
158
+ "run_cloud_setup",
159
+ "run_gh_run_list",
160
+ "run_git",
161
+ "safe_target_name",
162
+ "safe_config_stem",
163
+ "safe_event_type",
164
+ "safe_kind",
165
+ "stage_reports",
166
+ "swap_reports",
167
+ "token_prefix",
168
+ "unexpected_bundle_entries",
169
+ "utc_now",
170
+ "validate_metadata_payload",
171
+ "validate_upload_endpoint",
172
+ "write_setup_env_file",
173
+ ]
@@ -0,0 +1,155 @@
1
+ """Cloud bundle schema and safety metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+ from .errors import CloudBundleError
9
+
10
+
11
+ BUNDLE_SCHEMA = "code_mower.cloudBenchmarkBundle.v1"
12
+ BUNDLE_MANIFEST_FILENAME = "code-mower-cloud-bundle.json"
13
+ MAX_REPORT_UPLOAD_BYTES = 1_000_000
14
+ MAX_EVENT_COUNT = 500
15
+ MAX_METADATA_DEPTH = 32
16
+ SAFE_REPORT_KINDS = {
17
+ "authoring-runs",
18
+ "calibration-runs",
19
+ "lane-policy",
20
+ "reviewer-metrics",
21
+ "spend",
22
+ "value-report",
23
+ }
24
+ SAFE_EVENT_TYPES = {
25
+ "calibration_run",
26
+ "dogfood_upload",
27
+ "lane_policy_snapshot",
28
+ "reviewer_run",
29
+ "value_report_snapshot",
30
+ "workflow_run",
31
+ }
32
+ EXCLUDED_CONTENT = (
33
+ "source_code",
34
+ "raw_diffs",
35
+ "raw_model_transcripts",
36
+ "raw_stdout_stderr",
37
+ "auth_probe_output",
38
+ "secrets",
39
+ )
40
+ EXPECTED_BUNDLE_ENTRIES = {
41
+ "README.md",
42
+ BUNDLE_MANIFEST_FILENAME,
43
+ "reports",
44
+ ".README.md.tmp",
45
+ f".{BUNDLE_MANIFEST_FILENAME}.tmp",
46
+ ".reports.tmp",
47
+ }
48
+ UNSAFE_METADATA_KEY_FRAGMENTS = (
49
+ "api_key",
50
+ "apikey",
51
+ "auth_output",
52
+ "auth_preview",
53
+ "auth_probe_output",
54
+ "authorization",
55
+ "cookie",
56
+ "output_preview",
57
+ "password",
58
+ "private_key",
59
+ "raw_diff",
60
+ "raw_diffs",
61
+ "raw_model_transcript",
62
+ "raw_model_transcripts",
63
+ "raw_output",
64
+ "raw_stderr",
65
+ "raw_stdout",
66
+ "raw_stdout_stderr",
67
+ "secret",
68
+ "session_id",
69
+ "source_code",
70
+ "stderr",
71
+ "stdout",
72
+ "token_prefix",
73
+ "token_value",
74
+ "transcript",
75
+ )
76
+ UNSAFE_METADATA_VALUE_PATTERNS = tuple(
77
+ re.compile(pattern, re.IGNORECASE)
78
+ for pattern in (
79
+ r"gh[pousr]_[A-Za-z0-9_]{20,}",
80
+ r"github_pat_[A-Za-z0-9_]{20,}",
81
+ r"sk-[A-Za-z0-9_-]{20,}",
82
+ r"cmw_(?:live|test)_[A-Za-z0-9_-]{8,}",
83
+ r"AIza[0-9A-Za-z\-_]{20,}",
84
+ r"-----BEGIN [A-Z ]*PRIVATE KEY-----",
85
+ r"Bearer\s+[A-Za-z0-9._~+/=-]{12,}",
86
+ r"(?:ANTHROPIC_API_KEY|CODE_MOWER_CLOUD_TOKEN|GEMINI_API_KEY|GITHUB_TOKEN|OPENAI_API_KEY)=",
87
+ )
88
+ )
89
+
90
+
91
+ def is_bundle_manifest(manifest: Any) -> bool:
92
+ return isinstance(manifest, dict) and manifest.get("schema") == BUNDLE_SCHEMA
93
+
94
+
95
+ def _is_unsafe_metadata_key(key: str) -> bool:
96
+ with_word_boundaries = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "_", key.strip())
97
+ normalized = re.sub(r"[^A-Za-z0-9]+", "_", with_word_boundaries).lower().strip("_")
98
+ if normalized == "token" or normalized.endswith("_token"):
99
+ return True
100
+ return any(fragment in normalized for fragment in UNSAFE_METADATA_KEY_FRAGMENTS)
101
+
102
+
103
+ def _unsafe_metadata_value_reason(value: str) -> str:
104
+ for pattern in UNSAFE_METADATA_VALUE_PATTERNS:
105
+ if pattern.search(value):
106
+ return pattern.pattern
107
+ return ""
108
+
109
+
110
+ def validate_metadata_payload(
111
+ value: Any,
112
+ *,
113
+ path: str = "$",
114
+ depth: int = 0,
115
+ max_depth: int = MAX_METADATA_DEPTH,
116
+ ) -> None:
117
+ """Reject structured cloud metadata that looks like raw output or secrets."""
118
+
119
+ if depth > max_depth:
120
+ raise CloudBundleError(
121
+ "structured cloud metadata is too deeply nested at "
122
+ f"{path!r}; max depth is {max_depth}"
123
+ )
124
+ if isinstance(value, dict):
125
+ for key, item in value.items():
126
+ key_text = str(key)
127
+ child_path = f"{path}.{key_text}"
128
+ if _is_unsafe_metadata_key(key_text):
129
+ raise CloudBundleError(
130
+ "structured cloud metadata contains unsafe field "
131
+ f"{child_path!r}; exclude raw output, auth state, and secrets"
132
+ )
133
+ validate_metadata_payload(
134
+ item,
135
+ path=child_path,
136
+ depth=depth + 1,
137
+ max_depth=max_depth,
138
+ )
139
+ return
140
+ if isinstance(value, list):
141
+ for index, item in enumerate(value):
142
+ validate_metadata_payload(
143
+ item,
144
+ path=f"{path}[{index}]",
145
+ depth=depth + 1,
146
+ max_depth=max_depth,
147
+ )
148
+ return
149
+ if isinstance(value, str):
150
+ reason = _unsafe_metadata_value_reason(value)
151
+ if reason:
152
+ raise CloudBundleError(
153
+ "structured cloud metadata contains a secret-like value at "
154
+ f"{path!r}; matched safety pattern {reason!r}"
155
+ )
@@ -0,0 +1,206 @@
1
+ """Cloud upload readiness diagnostics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shlex
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from .bundle import BUNDLE_MANIFEST_FILENAME
11
+ from .endpoints import (
12
+ dashboard_url_for_endpoint,
13
+ health_url_for_endpoint,
14
+ is_local_http_endpoint,
15
+ probe_cloud_service,
16
+ validate_upload_endpoint,
17
+ )
18
+ from .errors import CloudBundleError
19
+ from .upload import build_upload_payload
20
+
21
+
22
+ def _validate_upload_endpoint(endpoint: str) -> None:
23
+ try:
24
+ validate_upload_endpoint(endpoint)
25
+ except ValueError as exc:
26
+ raise CloudBundleError(str(exc)) from exc
27
+
28
+
29
+ def run_cloud_doctor(
30
+ *,
31
+ bundle_dir: Path,
32
+ endpoint: str,
33
+ token_env: str,
34
+ require_token: bool = True,
35
+ probe_service: bool = False,
36
+ timeout: float = 5.0,
37
+ ) -> dict[str, Any]:
38
+ checks: list[dict[str, Any]] = []
39
+ endpoint_is_valid = False
40
+
41
+ try:
42
+ _validate_upload_endpoint(endpoint)
43
+ endpoint_is_valid = True
44
+ checks.append(
45
+ {
46
+ "name": "endpoint",
47
+ "status": "pass",
48
+ "message": f"upload endpoint is allowed: {endpoint}",
49
+ }
50
+ )
51
+ except CloudBundleError as exc:
52
+ checks.append(
53
+ {
54
+ "name": "endpoint",
55
+ "status": "fail",
56
+ "message": str(exc),
57
+ }
58
+ )
59
+
60
+ if probe_service and endpoint_is_valid:
61
+ checks.append(probe_cloud_service(endpoint, timeout=timeout))
62
+ elif endpoint_is_valid:
63
+ checks.append(
64
+ {
65
+ "name": "service",
66
+ "status": "skip",
67
+ "message": "service health probe skipped; pass --probe-service to check it",
68
+ "detail": {"health_url": health_url_for_endpoint(endpoint)},
69
+ }
70
+ )
71
+
72
+ token_present = bool(os.environ.get(token_env, ""))
73
+ if token_present:
74
+ checks.append(
75
+ {
76
+ "name": "token",
77
+ "status": "pass",
78
+ "message": f"{token_env} is set",
79
+ }
80
+ )
81
+ elif is_local_http_endpoint(endpoint):
82
+ checks.append(
83
+ {
84
+ "name": "token",
85
+ "status": "warn",
86
+ "message": (
87
+ f"{token_env} is not set; local configless ingest may still work"
88
+ ),
89
+ }
90
+ )
91
+ elif not require_token:
92
+ checks.append(
93
+ {
94
+ "name": "token",
95
+ "status": "warn",
96
+ "message": f"{token_env} is not set; upload will remain disabled",
97
+ "remediation": (
98
+ "Run `code-mower cloud setup --token-stdin` or export "
99
+ f"{token_env} before using --yes."
100
+ ),
101
+ }
102
+ )
103
+ else:
104
+ checks.append(
105
+ {
106
+ "name": "token",
107
+ "status": "fail",
108
+ "message": f"{token_env} is not set",
109
+ "remediation": (
110
+ "Set CODE_MOWER_CLOUD_TOKEN to a team ingest token before "
111
+ "running cloud upload --yes."
112
+ ),
113
+ }
114
+ )
115
+
116
+ manifest_path = bundle_dir / BUNDLE_MANIFEST_FILENAME
117
+ if manifest_path.is_file():
118
+ try:
119
+ payload = build_upload_payload(bundle_dir=bundle_dir, include_reports=False)
120
+ checks.append(
121
+ {
122
+ "name": "bundle",
123
+ "status": "pass",
124
+ "message": (
125
+ f"bundle is readable with {len(payload['reports'])} report summaries "
126
+ f"and {len(payload['events'])} structured events"
127
+ ),
128
+ }
129
+ )
130
+ except CloudBundleError as exc:
131
+ checks.append(
132
+ {
133
+ "name": "bundle",
134
+ "status": "fail",
135
+ "message": str(exc),
136
+ }
137
+ )
138
+ else:
139
+ checks.append(
140
+ {
141
+ "name": "bundle",
142
+ "status": "warn",
143
+ "message": (
144
+ f"bundle manifest not found at {manifest_path}; run cloud export first"
145
+ ),
146
+ }
147
+ )
148
+
149
+ failures = sum(1 for check in checks if check["status"] == "fail")
150
+ warnings = sum(1 for check in checks if check["status"] == "warn")
151
+ return {
152
+ "mode": "cloud-doctor",
153
+ "status": "fail" if failures else "pass",
154
+ "endpoint": endpoint,
155
+ "dashboard_url": dashboard_url_for_endpoint(endpoint),
156
+ "health_url": health_url_for_endpoint(endpoint),
157
+ "token_env": token_env,
158
+ "bundle_dir": str(bundle_dir),
159
+ "checks": checks,
160
+ "failures": failures,
161
+ "warnings": warnings,
162
+ "next_steps": [
163
+ {
164
+ "id": "open-dashboard",
165
+ "label": "Open the Code Mower Cloud dashboard",
166
+ "url": dashboard_url_for_endpoint(endpoint),
167
+ },
168
+ {
169
+ "id": "setup-token",
170
+ "label": "Store a dashboard-issued token locally",
171
+ "command": (
172
+ "code-mower cloud setup --token-stdin "
173
+ "--team-id YOUR_TEAM_SLUG --install-id YOUR_INSTALL_ID"
174
+ ),
175
+ },
176
+ {
177
+ "id": "dry-run-upload",
178
+ "label": "Preview the metadata upload without network transfer",
179
+ "command": f"code-mower cloud upload {shlex.quote(str(bundle_dir))} --dry-run --json",
180
+ },
181
+ {
182
+ "id": "upload",
183
+ "label": "Upload metadata after inspecting the dry run",
184
+ "command": f"code-mower cloud upload {shlex.quote(str(bundle_dir))} --yes --json",
185
+ },
186
+ ],
187
+ }
188
+
189
+
190
+ def render_cloud_doctor_text(report: dict[str, Any]) -> str:
191
+ lines = [
192
+ "Code Mower cloud doctor",
193
+ f"Status: {report['status']}",
194
+ f"Endpoint: {report['endpoint']}",
195
+ f"Dashboard: {report.get('dashboard_url', '')}",
196
+ f"Token env: {report['token_env']}",
197
+ f"Bundle: {report['bundle_dir']}",
198
+ "",
199
+ ]
200
+ for check in report["checks"]:
201
+ lines.append(
202
+ f"- {check['status'].upper()} {check['name']}: {check['message']}"
203
+ )
204
+ if check.get("remediation"):
205
+ lines.append(f" remediation: {check['remediation']}")
206
+ return "\n".join(lines) + "\n"
@@ -0,0 +1,78 @@
1
+ """Routine dogfood upload planning helpers.
2
+
3
+ The cloud CLI remains the public API. These helpers keep the repeatable
4
+ dogfood bundle shape separate from network posting and argument parsing.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+
14
+ DEFAULT_DOGFOOD_REPORTS: tuple[tuple[str, str], ...] = (
15
+ ("docs/reviewer-value-report.md", "value-report"),
16
+ ("docs/lane-promotion-policy.md", "lane-policy"),
17
+ (".code-mower/reviewer-value-report.md", "value-report"),
18
+ (".code-mower/reviewer-metrics.json", "reviewer-metrics"),
19
+ )
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class DogfoodPlan:
24
+ repo_slug: str
25
+ team_id: str
26
+ install_id: str
27
+ source: str
28
+ report_count: int
29
+ extra_event_count: int
30
+
31
+
32
+ def default_dogfood_reports(repo_path: Path) -> list[tuple[Path, str]]:
33
+ seen: set[Path] = set()
34
+ reports: list[tuple[Path, str]] = []
35
+ for relative_path, kind in DEFAULT_DOGFOOD_REPORTS:
36
+ path = repo_path / relative_path
37
+ if path in seen or not path.is_file():
38
+ continue
39
+ seen.add(path)
40
+ reports.append((path, kind))
41
+ return reports
42
+
43
+
44
+ def build_dogfood_plan(
45
+ *,
46
+ repo_slug: str,
47
+ team_id: str,
48
+ install_id: str,
49
+ source: str,
50
+ reports: list[tuple[Path, str]],
51
+ events: list[dict[str, Any]],
52
+ ) -> DogfoodPlan:
53
+ return DogfoodPlan(
54
+ repo_slug=repo_slug,
55
+ team_id=team_id,
56
+ install_id=install_id,
57
+ source=source,
58
+ report_count=len(reports),
59
+ extra_event_count=len(events),
60
+ )
61
+
62
+
63
+ def build_dogfood_dry_run_preview(
64
+ *,
65
+ endpoint: str,
66
+ payload: dict[str, Any],
67
+ ) -> dict[str, Any]:
68
+ return {
69
+ "mode": "cloud-upload-dry-run",
70
+ "endpoint": endpoint,
71
+ "would_upload": False,
72
+ "requires_yes": True,
73
+ "upload_mode": payload["upload_mode"],
74
+ "report_count": len(payload["reports"]),
75
+ "event_count": len(payload["events"]),
76
+ "privacy_mode": payload["privacy_mode"],
77
+ "excluded_content": payload["excluded_content"],
78
+ }