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,103 @@
1
+ """GitHub Actions failure and billing-block diagnostics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .common import DoctorCheck, STATUS_PASS, STATUS_WARN
6
+ from .github_actions_failure_annotations import (
7
+ _annotation_mentions_actions_billing_block,
8
+ _check_run_id_from_actions_job,
9
+ )
10
+ from .github_actions_failure_scan import (
11
+ inspect_recent_actions_failures,
12
+ )
13
+
14
+
15
+ def _check_recent_actions_billing_blocks(
16
+ *,
17
+ gh_path: str,
18
+ slug: str,
19
+ http_timeout: int,
20
+ ) -> DoctorCheck:
21
+ inspection = inspect_recent_actions_failures(
22
+ gh_path=gh_path,
23
+ slug=slug,
24
+ http_timeout=http_timeout,
25
+ )
26
+ if inspection.unavailable_detail is not None:
27
+ return DoctorCheck(
28
+ name="github.actions.recent_failures",
29
+ status=STATUS_WARN,
30
+ message=f"could not inspect recent GitHub Actions runs for {slug}",
31
+ detail={"repo": slug, **inspection.unavailable_detail},
32
+ remediation=(
33
+ "Verify gh auth can read Actions run metadata, then rerun "
34
+ "`code-mower doctor --github`."
35
+ ),
36
+ )
37
+
38
+ if inspection.missing_workflow_runs:
39
+ return DoctorCheck(
40
+ name="github.actions.recent_failures",
41
+ status=STATUS_WARN,
42
+ message=f"recent Actions response for {slug} did not include workflow_runs",
43
+ detail={"repo": slug},
44
+ )
45
+
46
+ if inspection.has_billing_blocks:
47
+ return DoctorCheck(
48
+ name="github.actions.recent_failures",
49
+ status=STATUS_WARN,
50
+ message=(
51
+ f"{slug} has recent Actions jobs blocked by billing "
52
+ "or spending limits"
53
+ ),
54
+ detail={
55
+ "repo": slug,
56
+ "billing_block_count": len(inspection.billing_blocks),
57
+ "billing_blocks": [block.as_detail() for block in inspection.billing_blocks],
58
+ "inspected_failed_runs": inspection.inspected_failed_runs,
59
+ "inspected_failed_jobs": inspection.inspected_failed_jobs,
60
+ },
61
+ remediation=(
62
+ "Fix GitHub billing or Actions spending limits, then rerun "
63
+ "failed workflows before relying on branch protection or "
64
+ "deploy checks."
65
+ ),
66
+ )
67
+
68
+ if inspection.incomplete_inspections:
69
+ return DoctorCheck(
70
+ name="github.actions.recent_failures",
71
+ status=STATUS_WARN,
72
+ message=f"{slug} has recent failed Actions runs that doctor could not fully inspect",
73
+ detail={
74
+ "repo": slug,
75
+ "incomplete_inspection_count": inspection.incomplete_inspection_count,
76
+ "incomplete_inspections": list(inspection.incomplete_inspections[:5]),
77
+ "inspected_failed_runs": inspection.inspected_failed_runs,
78
+ "inspected_failed_jobs": inspection.inspected_failed_jobs,
79
+ },
80
+ remediation=(
81
+ "Verify gh auth can read workflow jobs and annotations, or inspect "
82
+ "recent failed Actions runs manually before treating doctor as a "
83
+ "green setup signal."
84
+ ),
85
+ )
86
+
87
+ return DoctorCheck(
88
+ name="github.actions.recent_failures",
89
+ status=STATUS_PASS,
90
+ message=f"{slug} has no recent Actions billing-block annotations",
91
+ detail={
92
+ "repo": slug,
93
+ "inspected_failed_runs": inspection.inspected_failed_runs,
94
+ "inspected_failed_jobs": inspection.inspected_failed_jobs,
95
+ },
96
+ )
97
+
98
+
99
+ __all__ = (
100
+ "_annotation_mentions_actions_billing_block",
101
+ "_check_recent_actions_billing_blocks",
102
+ "_check_run_id_from_actions_job",
103
+ )
@@ -0,0 +1,55 @@
1
+ """GitHub Actions enablement doctor checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .common import DoctorCheck, STATUS_PASS, STATUS_WARN
6
+ from .github_api import _github_api_json
7
+
8
+
9
+ def check_actions_permissions(
10
+ *,
11
+ gh_path: str,
12
+ slug: str,
13
+ http_timeout: int,
14
+ ) -> DoctorCheck:
15
+ actions_payload, actions_detail = _github_api_json(
16
+ gh_path,
17
+ f"repos/{slug}/actions/permissions",
18
+ http_timeout=http_timeout,
19
+ )
20
+ if actions_payload is None:
21
+ return DoctorCheck(
22
+ name="github.actions.permissions",
23
+ status=STATUS_WARN,
24
+ message=f"could not inspect GitHub Actions permissions for {slug}",
25
+ detail={"repo": slug, **actions_detail},
26
+ remediation=(
27
+ "A repo admin should verify Actions are enabled and workflow "
28
+ "token permissions can write issues/labels or that PAT "
29
+ "fallback secrets are configured."
30
+ ),
31
+ )
32
+
33
+ enabled = bool(actions_payload.get("enabled"))
34
+ return DoctorCheck(
35
+ name="github.actions.permissions",
36
+ status=STATUS_PASS if enabled else STATUS_WARN,
37
+ message=(
38
+ f"{slug} GitHub Actions are enabled and inspectable"
39
+ if enabled
40
+ else f"{slug} GitHub Actions appear disabled"
41
+ ),
42
+ detail={
43
+ "repo": slug,
44
+ "enabled": enabled,
45
+ "allowed_actions": str(actions_payload.get("allowed_actions") or ""),
46
+ },
47
+ remediation=(
48
+ None
49
+ if enabled
50
+ else (
51
+ "Enable GitHub Actions for this repository before expecting "
52
+ "Code Mower labelers or audit workflows to run."
53
+ )
54
+ ),
55
+ )
@@ -0,0 +1,79 @@
1
+ """Redacted GitHub API helpers for doctor checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ from typing import Any, Mapping
8
+
9
+ from .privacy import auth_probe_output_detail
10
+
11
+
12
+ def _github_api_payload(
13
+ gh_path: str,
14
+ endpoint: str,
15
+ *,
16
+ http_timeout: int,
17
+ ) -> tuple[Any | None, dict[str, Any]]:
18
+ try:
19
+ completed = subprocess.run(
20
+ [gh_path, "api", endpoint],
21
+ capture_output=True,
22
+ text=True,
23
+ check=False,
24
+ timeout=http_timeout,
25
+ )
26
+ except (OSError, subprocess.TimeoutExpired) as exc:
27
+ return None, {"error_type": exc.__class__.__name__}
28
+ output = (completed.stdout or completed.stderr or "").strip()
29
+ detail: dict[str, Any] = {
30
+ "endpoint": endpoint,
31
+ "returncode": completed.returncode,
32
+ **auth_probe_output_detail(output),
33
+ }
34
+ if completed.returncode != 0:
35
+ return None, detail
36
+ try:
37
+ payload = json.loads(completed.stdout or "{}")
38
+ except json.JSONDecodeError:
39
+ detail["parse_error"] = "json"
40
+ return None, detail
41
+ return payload, detail
42
+
43
+
44
+ def _github_api_json(
45
+ gh_path: str,
46
+ endpoint: str,
47
+ *,
48
+ http_timeout: int,
49
+ ) -> tuple[Mapping[str, Any] | None, dict[str, Any]]:
50
+ payload, detail = _github_api_payload(
51
+ gh_path,
52
+ endpoint,
53
+ http_timeout=http_timeout,
54
+ )
55
+ if payload is None:
56
+ return None, detail
57
+ if not isinstance(payload, Mapping):
58
+ detail["parse_error"] = "not_object"
59
+ return None, detail
60
+ return payload, detail
61
+
62
+
63
+ def _github_api_list(
64
+ gh_path: str,
65
+ endpoint: str,
66
+ *,
67
+ http_timeout: int,
68
+ ) -> tuple[list[Any] | None, dict[str, Any]]:
69
+ payload, detail = _github_api_payload(
70
+ gh_path,
71
+ endpoint,
72
+ http_timeout=http_timeout,
73
+ )
74
+ if payload is None:
75
+ return None, detail
76
+ if not isinstance(payload, list):
77
+ detail["parse_error"] = "not_list"
78
+ return None, detail
79
+ return payload, detail
@@ -0,0 +1,56 @@
1
+ """GitHub branch protection doctor checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import urllib.parse
6
+ from typing import Mapping
7
+
8
+ from .common import DoctorCheck, STATUS_PASS, STATUS_WARN
9
+ from .github_api import _github_api_json
10
+
11
+
12
+ def check_branch_protection(
13
+ *,
14
+ gh_path: str,
15
+ slug: str,
16
+ default_branch: str,
17
+ http_timeout: int,
18
+ ) -> DoctorCheck:
19
+ encoded_branch = urllib.parse.quote(default_branch, safe="")
20
+ protection_payload, protection_detail = _github_api_json(
21
+ gh_path,
22
+ f"repos/{slug}/branches/{encoded_branch}/protection",
23
+ http_timeout=http_timeout,
24
+ )
25
+ if protection_payload is None:
26
+ return DoctorCheck(
27
+ name="github.branch_protection",
28
+ status=STATUS_WARN,
29
+ message=f"could not confirm branch protection for {slug}@{default_branch}",
30
+ detail={
31
+ "repo": slug,
32
+ "default_branch": default_branch,
33
+ **protection_detail,
34
+ },
35
+ remediation=(
36
+ "Before enabling autonomous merge, protect the default branch "
37
+ "and make required checks explicit."
38
+ ),
39
+ )
40
+
41
+ required_checks = protection_payload.get("required_status_checks")
42
+ contexts: list[str] = []
43
+ if isinstance(required_checks, Mapping):
44
+ raw_contexts = required_checks.get("contexts")
45
+ if isinstance(raw_contexts, list):
46
+ contexts = [str(item) for item in raw_contexts]
47
+ return DoctorCheck(
48
+ name="github.branch_protection",
49
+ status=STATUS_PASS,
50
+ message=f"{slug}@{default_branch} branch protection is inspectable",
51
+ detail={
52
+ "repo": slug,
53
+ "default_branch": default_branch,
54
+ "required_status_check_count": len(contexts),
55
+ },
56
+ )
@@ -0,0 +1,25 @@
1
+ """GitHub doctor configuration helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Mapping, Sequence
6
+
7
+ from .common import as_sequence
8
+
9
+
10
+ def configured_repositories(config: Mapping[str, Any]) -> tuple[Mapping[str, Any], ...]:
11
+ repos: list[Mapping[str, Any]] = []
12
+ for repo in as_sequence(config.get("repositories", [])):
13
+ if isinstance(repo, Mapping) and repo.get("slug"):
14
+ repos.append(repo)
15
+ return tuple(repos)
16
+
17
+
18
+ def selected_saas_or_hosted_lanes(
19
+ lanes: Sequence[tuple[str, Mapping[str, Any]]],
20
+ ) -> list[str]:
21
+ selected: list[str] = []
22
+ for lane_id, lane in lanes:
23
+ if str(lane.get("driver", "")) in {"saas_event", "hosted_bridge"}:
24
+ selected.append(lane_id)
25
+ return selected
@@ -0,0 +1,61 @@
1
+ """Hosted-provider GitHub repository compatibility checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+
7
+ from .common import DoctorCheck, STATUS_PASS, STATUS_WARN
8
+
9
+
10
+ def check_private_repo_provider_surface(
11
+ *,
12
+ private_repos: Sequence[str],
13
+ unknown_visibility_repos: Sequence[str],
14
+ selected_saas_or_hosted: Sequence[str],
15
+ ) -> DoctorCheck | None:
16
+ if private_repos and selected_saas_or_hosted:
17
+ return DoctorCheck(
18
+ name="github.provider.private_repo",
19
+ status=STATUS_WARN,
20
+ message=(
21
+ "private repos selected with SaaS/hosted lanes: "
22
+ + ", ".join(selected_saas_or_hosted)
23
+ ),
24
+ detail={
25
+ "private_repo_count": len(private_repos),
26
+ "lanes": list(selected_saas_or_hosted),
27
+ },
28
+ remediation=(
29
+ "Install each provider's GitHub App for the selected private "
30
+ "repositories, confirm plan support, and decide whether sending "
31
+ "diffs/source to that provider is acceptable."
32
+ ),
33
+ )
34
+
35
+ if unknown_visibility_repos and selected_saas_or_hosted:
36
+ return DoctorCheck(
37
+ name="github.provider.private_repo",
38
+ status=STATUS_WARN,
39
+ message=(
40
+ "could not determine repository visibility for SaaS/hosted lanes: "
41
+ + ", ".join(selected_saas_or_hosted)
42
+ ),
43
+ detail={
44
+ "unknown_repo_count": len(unknown_visibility_repos),
45
+ "lanes": list(selected_saas_or_hosted),
46
+ },
47
+ remediation=(
48
+ "Verify gh auth can read repository metadata before deciding "
49
+ "whether hosted provider apps need private-repo access."
50
+ ),
51
+ )
52
+
53
+ if selected_saas_or_hosted:
54
+ return DoctorCheck(
55
+ name="github.provider.private_repo",
56
+ status=STATUS_PASS,
57
+ message="selected SaaS/hosted lanes have no private repos in config",
58
+ detail={"lanes": list(selected_saas_or_hosted)},
59
+ )
60
+
61
+ return None
@@ -0,0 +1,120 @@
1
+ """GitHub repository metadata and permission doctor checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Mapping
7
+
8
+ from .common import DoctorCheck, STATUS_PASS, STATUS_WARN
9
+ from .github_api import _github_api_json
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class GitHubRepoMetadata:
14
+ slug: str
15
+ is_private: bool
16
+ default_branch: str
17
+ payload: Mapping[str, Any]
18
+
19
+
20
+ def check_repo_metadata(
21
+ *,
22
+ gh_path: str,
23
+ slug: str,
24
+ configured_default_branch: str,
25
+ http_timeout: int,
26
+ ) -> tuple[DoctorCheck, GitHubRepoMetadata | None]:
27
+ repo_payload, repo_detail = _github_api_json(
28
+ gh_path,
29
+ f"repos/{slug}",
30
+ http_timeout=http_timeout,
31
+ )
32
+ if repo_payload is None:
33
+ return (
34
+ DoctorCheck(
35
+ name="github.repo.metadata",
36
+ status=STATUS_WARN,
37
+ message=f"could not read GitHub repository metadata for {slug}",
38
+ detail={"repo": slug, **repo_detail},
39
+ remediation=(
40
+ "Verify gh auth can read this repo. Private repos need "
41
+ "a token or GitHub App installation with repository access."
42
+ ),
43
+ ),
44
+ None,
45
+ )
46
+
47
+ is_private = bool(repo_payload.get("private"))
48
+ default_branch = str(
49
+ repo_payload.get("default_branch") or configured_default_branch or "main"
50
+ )
51
+ return (
52
+ DoctorCheck(
53
+ name="github.repo.metadata",
54
+ status=STATUS_PASS,
55
+ message=(
56
+ f"{slug} is reachable "
57
+ f"({'private' if is_private else 'public'} repository)"
58
+ ),
59
+ detail={
60
+ "repo": slug,
61
+ "private": is_private,
62
+ "visibility": str(repo_payload.get("visibility") or ""),
63
+ "default_branch": str(repo_payload.get("default_branch") or ""),
64
+ "archived": bool(repo_payload.get("archived")),
65
+ "fork": bool(repo_payload.get("fork")),
66
+ },
67
+ ),
68
+ GitHubRepoMetadata(
69
+ slug=slug,
70
+ is_private=is_private,
71
+ default_branch=default_branch,
72
+ payload=repo_payload,
73
+ ),
74
+ )
75
+
76
+
77
+ def check_repo_permissions(*, slug: str, repo_payload: Mapping[str, Any]) -> DoctorCheck:
78
+ permissions = repo_payload.get("permissions")
79
+ if isinstance(permissions, Mapping):
80
+ write_like = any(
81
+ bool(permissions.get(name))
82
+ for name in ("admin", "maintain", "push", "triage")
83
+ )
84
+ return DoctorCheck(
85
+ name="github.repo.permissions",
86
+ status=STATUS_PASS if write_like else STATUS_WARN,
87
+ message=(
88
+ f"{slug} token has repository write-adjacent permission"
89
+ if write_like
90
+ else f"{slug} token appears read-only for repository metadata"
91
+ ),
92
+ detail={
93
+ "repo": slug,
94
+ "admin": bool(permissions.get("admin")),
95
+ "maintain": bool(permissions.get("maintain")),
96
+ "push": bool(permissions.get("push")),
97
+ "triage": bool(permissions.get("triage")),
98
+ "pull": bool(permissions.get("pull")),
99
+ },
100
+ remediation=(
101
+ None
102
+ if write_like
103
+ else (
104
+ "Configure a fine-grained PAT or GitHub App token with "
105
+ "Issues read/write and Pull requests read before expecting "
106
+ "Code Mower to apply labels or comments."
107
+ )
108
+ ),
109
+ )
110
+
111
+ return DoctorCheck(
112
+ name="github.repo.permissions",
113
+ status=STATUS_WARN,
114
+ message=f"{slug} metadata did not include token permissions",
115
+ detail={"repo": slug},
116
+ remediation=(
117
+ "If label writes fail, configure the lane token secrets "
118
+ "documented by the provider matrix."
119
+ ),
120
+ )
@@ -0,0 +1,36 @@
1
+ """Doctor check grouping helpers shared by JSON and text output."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import OrderedDict
6
+
7
+
8
+ GROUP_LABELS = OrderedDict(
9
+ (
10
+ ("setup", "Setup"),
11
+ ("runtime", "Runtime"),
12
+ ("providers", "Provider lanes"),
13
+ ("github", "GitHub"),
14
+ ("cloud", "Code Mower Cloud"),
15
+ ("output", "Output"),
16
+ ("other", "Other"),
17
+ )
18
+ )
19
+
20
+
21
+ def doctor_check_group_id(name: str, lane: str | None = None) -> str:
22
+ """Return the stable reporting group for a doctor check name."""
23
+
24
+ if name.startswith("github."):
25
+ return "github"
26
+ if name.startswith("cloud."):
27
+ return "cloud"
28
+ if name.startswith("output."):
29
+ return "output"
30
+ if lane or name.startswith(("env.", "provider.", "runtime.local_cli")):
31
+ return "providers"
32
+ if name.startswith("runtime."):
33
+ return "runtime"
34
+ if name.startswith(("config.", "provider_templates.", "profile.", "doctor.")):
35
+ return "setup"
36
+ return "other"
@@ -0,0 +1,96 @@
1
+ """Structured doctor check results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass
6
+ from typing import Any, Mapping
7
+
8
+ from .groups import GROUP_LABELS, doctor_check_group_id
9
+
10
+
11
+ STATUS_PASS = "pass"
12
+ STATUS_WARN = "warn"
13
+ STATUS_FAIL = "fail"
14
+ STATUS_SKIP = "skip"
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class DoctorCheck:
19
+ name: str
20
+ status: str
21
+ message: str
22
+ lane: str | None = None
23
+ detail: Mapping[str, Any] | None = None
24
+ remediation: str | None = None
25
+
26
+ def as_dict(self) -> dict[str, Any]:
27
+ data = asdict(self)
28
+ if self.detail is None:
29
+ data.pop("detail")
30
+ if self.lane is None:
31
+ data.pop("lane")
32
+ if self.remediation is None:
33
+ data.pop("remediation")
34
+ return data
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class DoctorReport:
39
+ config_path: str
40
+ provider_templates_path: str
41
+ profile: str | None
42
+ checks: tuple[DoctorCheck, ...]
43
+
44
+ @property
45
+ def failures(self) -> int:
46
+ return sum(1 for check in self.checks if check.status == STATUS_FAIL)
47
+
48
+ @property
49
+ def warnings(self) -> int:
50
+ return sum(1 for check in self.checks if check.status == STATUS_WARN)
51
+
52
+ @property
53
+ def status(self) -> str:
54
+ if self.failures:
55
+ return STATUS_FAIL
56
+ if self.warnings:
57
+ return STATUS_WARN
58
+ return STATUS_PASS
59
+
60
+ def group_summary(self) -> dict[str, dict[str, int | str]]:
61
+ summary: dict[str, dict[str, int | str]] = {}
62
+ for group_id, label in GROUP_LABELS.items():
63
+ group_checks = [
64
+ check
65
+ for check in self.checks
66
+ if doctor_check_group_id(check.name, check.lane) == group_id
67
+ ]
68
+ if not group_checks:
69
+ continue
70
+ failures = sum(1 for check in group_checks if check.status == STATUS_FAIL)
71
+ warnings = sum(1 for check in group_checks if check.status == STATUS_WARN)
72
+ skipped = sum(1 for check in group_checks if check.status == STATUS_SKIP)
73
+ summary[group_id] = {
74
+ "label": label,
75
+ "checks": len(group_checks),
76
+ "failures": failures,
77
+ "warnings": warnings,
78
+ "skipped": skipped,
79
+ }
80
+ return summary
81
+
82
+ def as_dict(self) -> dict[str, Any]:
83
+ return {
84
+ "mode": "doctor",
85
+ "status": self.status,
86
+ "profile": self.profile,
87
+ "config_path": self.config_path,
88
+ "provider_templates_path": self.provider_templates_path,
89
+ "summary": {
90
+ "checks": len(self.checks),
91
+ "failures": self.failures,
92
+ "warnings": self.warnings,
93
+ },
94
+ "groups": self.group_summary(),
95
+ "checks": [check.as_dict() for check in self.checks],
96
+ }