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,159 @@
1
+ """Provider catalog coverage and runtime check orchestration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Mapping
6
+
7
+ from .common import (
8
+ DoctorCheck,
9
+ STATUS_FAIL,
10
+ STATUS_PASS,
11
+ STATUS_SKIP,
12
+ STATUS_WARN,
13
+ as_mapping,
14
+ as_sequence,
15
+ code_mower_config,
16
+ )
17
+ from .provider_api_model import check_api_model
18
+ from .provider_env import check_required_env, check_token_env
19
+ from .provider_local_cli import (
20
+ check_local_cli,
21
+ check_local_cli_probe,
22
+ )
23
+ from .provider_probe import (
24
+ evaluate_json_probe,
25
+ local_cli_probe_remediation,
26
+ )
27
+
28
+ __all__ = [
29
+ "check_lane_runtime",
30
+ "effective_lane",
31
+ "evaluate_json_probe",
32
+ "local_cli_probe_remediation",
33
+ "provider_template_coverage",
34
+ "selected_lanes",
35
+ ]
36
+
37
+
38
+ def selected_lanes(
39
+ config: Mapping[str, Any],
40
+ profile: str | None,
41
+ ) -> tuple[str, ...]:
42
+ lanes = as_mapping(config.get("lanes"), "lanes")
43
+ if not profile:
44
+ return tuple(str(lane_id) for lane_id in sorted(lanes))
45
+ profiles = as_mapping(config.get("profiles", {}), "profiles")
46
+ profile_config = profiles.get(profile)
47
+ if not isinstance(profile_config, Mapping):
48
+ known = ", ".join(sorted(str(item) for item in profiles))
49
+ raise code_mower_config.ConfigError(
50
+ f"unknown profile {profile!r}; known profiles: {known}"
51
+ )
52
+ return tuple(str(lane_id) for lane_id in as_sequence(profile_config.get("lanes", [])))
53
+
54
+
55
+ def provider_template_coverage(
56
+ lanes: tuple[str, ...],
57
+ templates: Mapping[str, Any],
58
+ ) -> DoctorCheck:
59
+ provider_templates = as_mapping(
60
+ templates.get("provider_templates"),
61
+ "provider_templates",
62
+ )
63
+ missing = sorted(set(lanes) - set(str(item) for item in provider_templates))
64
+ if missing:
65
+ return DoctorCheck(
66
+ name="provider_templates.coverage",
67
+ status=STATUS_FAIL,
68
+ message=f"provider templates missing selected lanes: {', '.join(missing)}",
69
+ detail={"missing_lanes": missing},
70
+ remediation=(
71
+ "Add these lane ids to the provider catalog or remove them from "
72
+ "the selected profile before running audits."
73
+ ),
74
+ )
75
+ return DoctorCheck(
76
+ name="provider_templates.coverage",
77
+ status=STATUS_PASS,
78
+ message="provider templates cover selected lanes",
79
+ )
80
+
81
+
82
+ def _merge_mapping_defaults(
83
+ defaults: Mapping[str, Any],
84
+ overrides: Mapping[str, Any],
85
+ ) -> dict[str, Any]:
86
+ merged = dict(defaults)
87
+ merged.update(overrides)
88
+ return merged
89
+
90
+
91
+ def effective_lane(
92
+ lane_id: str,
93
+ lane: Mapping[str, Any],
94
+ provider_templates: Mapping[str, Any],
95
+ ) -> Mapping[str, Any]:
96
+ template = provider_templates.get(lane_id, {})
97
+ if not isinstance(template, Mapping):
98
+ template = {}
99
+ merged = _merge_mapping_defaults(template, lane)
100
+ for key in ("labels", "provider_config", "review_hygiene"):
101
+ template_value = template.get(key, {})
102
+ lane_value = lane.get(key, {})
103
+ if isinstance(template_value, Mapping) and isinstance(lane_value, Mapping):
104
+ merged[key] = _merge_mapping_defaults(template_value, lane_value)
105
+ return merged
106
+
107
+
108
+ def check_lane_runtime(
109
+ lane_id: str,
110
+ lane: Mapping[str, Any],
111
+ *,
112
+ probe_runtime: bool,
113
+ http_timeout: int,
114
+ ) -> list[DoctorCheck]:
115
+ checks = check_token_env(lane_id, lane)
116
+ checks.extend(check_required_env(lane_id, lane))
117
+ driver = str(lane.get("driver", ""))
118
+ if driver == "local_cli":
119
+ checks.append(check_local_cli(lane_id, lane))
120
+ checks.append(
121
+ check_local_cli_probe(
122
+ lane_id,
123
+ lane,
124
+ probe_runtime=probe_runtime,
125
+ http_timeout=http_timeout,
126
+ )
127
+ )
128
+ elif driver == "api_model":
129
+ checks.extend(
130
+ check_api_model(
131
+ lane_id,
132
+ lane,
133
+ probe_runtime=probe_runtime,
134
+ http_timeout=http_timeout,
135
+ )
136
+ )
137
+ elif driver in {"manual", "hosted_bridge", "saas_event"}:
138
+ checks.append(
139
+ DoctorCheck(
140
+ name="runtime.probe",
141
+ status=STATUS_SKIP,
142
+ lane=lane_id,
143
+ message=f"{driver} lanes do not have a local runtime probe yet",
144
+ )
145
+ )
146
+ else:
147
+ checks.append(
148
+ DoctorCheck(
149
+ name="runtime.probe",
150
+ status=STATUS_WARN,
151
+ lane=lane_id,
152
+ message=f"unknown driver {driver!r}; no runtime probe available",
153
+ remediation=(
154
+ "Use a supported driver: local_cli, api_model, hosted_bridge, "
155
+ "saas_event, or manual."
156
+ ),
157
+ )
158
+ )
159
+ return checks
@@ -0,0 +1,69 @@
1
+ """Named doctor check groups and run-plan vocabulary.
2
+
3
+ The public doctor surface is intentionally simple: callers receive a flat list
4
+ of checks. Internally, doctor now has explicit groups and stages so new
5
+ diagnostics can be added behind stable names instead of growing another large
6
+ command adapter.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class DoctorCheckGroup:
16
+ id: str
17
+ description: str
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class DoctorCheckStage:
22
+ id: str
23
+ group_id: str
24
+ description: str
25
+ optional: bool = False
26
+
27
+
28
+ DEFAULT_CHECK_GROUPS = (
29
+ DoctorCheckGroup("runtime", "Python, package, and local toolchain checks"),
30
+ DoctorCheckGroup("github", "GitHub auth, workflow, and private-repo cost checks"),
31
+ DoctorCheckGroup("providers", "Reviewer provider CLI and secret checks"),
32
+ DoctorCheckGroup("cloud", "Optional CodeMower.com token and service checks"),
33
+ DoctorCheckGroup("output", "Report, JSON, and human-readable output checks"),
34
+ )
35
+
36
+ BASE_DOCTOR_STAGES = (
37
+ DoctorCheckStage("load-inputs", "runtime", "Load Code Mower config and provider templates"),
38
+ DoctorCheckStage("select-profile", "runtime", "Resolve the requested profile and lane set"),
39
+ DoctorCheckStage("runtime", "runtime", "Inspect Python, pytest, GitHub CLI, and ripgrep"),
40
+ DoctorCheckStage("providers", "providers", "Inspect selected provider lanes"),
41
+ )
42
+
43
+ OPTIONAL_DOCTOR_STAGES = (
44
+ DoctorCheckStage(
45
+ "github",
46
+ "github",
47
+ "Inspect GitHub repository metadata, branch rules, workflows, and Actions cost",
48
+ optional=True,
49
+ ),
50
+ DoctorCheckStage(
51
+ "cloud",
52
+ "cloud",
53
+ "Inspect optional CodeMower.com token setup",
54
+ optional=True,
55
+ ),
56
+ )
57
+
58
+
59
+ def default_check_group_ids() -> tuple[str, ...]:
60
+ return tuple(group.id for group in DEFAULT_CHECK_GROUPS)
61
+
62
+
63
+ def build_doctor_run_plan(*, github: bool = False, cloud: bool = False) -> tuple[DoctorCheckStage, ...]:
64
+ """Return the named stages that a doctor run will execute."""
65
+
66
+ stages = list(BASE_DOCTOR_STAGES)
67
+ optional_flags = {"github": github, "cloud": cloud}
68
+ stages.extend(stage for stage in OPTIONAL_DOCTOR_STAGES if optional_flags.get(stage.id, False))
69
+ return tuple(stages)
@@ -0,0 +1,188 @@
1
+ """Doctor report orchestration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any, Mapping
7
+
8
+ from code_mower import config as code_mower_config
9
+
10
+ from .cloud import check_cloud_token_surface
11
+ from .common import ACTIONS_COST_SAMPLE_DEFAULT, load_inputs
12
+ from .github import check_github_setup
13
+ from .models import STATUS_FAIL, STATUS_PASS, DoctorCheck, DoctorReport
14
+ from .providers import check_lane_runtime, effective_lane, provider_template_coverage, selected_lanes
15
+ from .registry import DoctorCheckStage, build_doctor_run_plan
16
+ from .runtime import (
17
+ check_github_auth_surface,
18
+ check_pytest,
19
+ check_python_runtime,
20
+ check_ripgrep,
21
+ )
22
+
23
+
24
+ def _global_runtime_checks(
25
+ *,
26
+ probe_runtime: bool,
27
+ http_timeout: int,
28
+ ) -> tuple[DoctorCheck, ...]:
29
+ return (
30
+ check_python_runtime(),
31
+ check_pytest(),
32
+ check_github_auth_surface(
33
+ probe_runtime=probe_runtime,
34
+ http_timeout=http_timeout,
35
+ ),
36
+ check_ripgrep(),
37
+ )
38
+
39
+
40
+ def _run_plan_check(
41
+ plan: tuple[DoctorCheckStage, ...],
42
+ *,
43
+ probe_runtime: bool,
44
+ actions_cost_sample: int,
45
+ ) -> DoctorCheck:
46
+ return DoctorCheck(
47
+ name="doctor.plan",
48
+ status=STATUS_PASS,
49
+ message="doctor run plan: " + ", ".join(stage.id for stage in plan),
50
+ detail={
51
+ "stages": [
52
+ {
53
+ "id": stage.id,
54
+ "group": stage.group_id,
55
+ "optional": stage.optional,
56
+ }
57
+ for stage in plan
58
+ ],
59
+ "probe_runtime": probe_runtime,
60
+ "actions_cost_sample": actions_cost_sample,
61
+ },
62
+ )
63
+
64
+
65
+ def run_doctor(
66
+ *,
67
+ config_path: Path,
68
+ provider_templates_path: Path,
69
+ profile: str | None,
70
+ probe_runtime: bool = False,
71
+ github: bool = False,
72
+ cloud: bool = False,
73
+ http_timeout: int = 5,
74
+ actions_cost_sample: int = ACTIONS_COST_SAMPLE_DEFAULT,
75
+ ) -> DoctorReport:
76
+ plan = build_doctor_run_plan(github=github, cloud=cloud)
77
+ enabled_stages = {stage.id for stage in plan}
78
+ config, templates, checks = load_inputs(config_path, provider_templates_path)
79
+ checks.append(
80
+ _run_plan_check(
81
+ plan,
82
+ probe_runtime=probe_runtime,
83
+ actions_cost_sample=actions_cost_sample,
84
+ )
85
+ )
86
+ if config is None or templates is None:
87
+ return DoctorReport(
88
+ config_path=str(config_path),
89
+ provider_templates_path=str(provider_templates_path),
90
+ profile=profile,
91
+ checks=tuple(checks),
92
+ )
93
+
94
+ try:
95
+ lanes = selected_lanes(config, profile)
96
+ except code_mower_config.ConfigError as exc:
97
+ checks.append(
98
+ DoctorCheck(
99
+ name="profile.select",
100
+ status=STATUS_FAIL,
101
+ message=str(exc),
102
+ remediation=(
103
+ "Choose an existing profile from code-mower.yml or run "
104
+ "`code-mower init --easy` to inspect the recommended profile."
105
+ ),
106
+ )
107
+ )
108
+ return DoctorReport(
109
+ config_path=str(config_path),
110
+ provider_templates_path=str(provider_templates_path),
111
+ profile=profile,
112
+ checks=tuple(checks),
113
+ )
114
+
115
+ checks.append(
116
+ DoctorCheck(
117
+ name="profile.select",
118
+ status=STATUS_PASS,
119
+ message=(
120
+ f"selected profile {profile}: {', '.join(lanes)}"
121
+ if profile
122
+ else f"selected all lanes: {', '.join(lanes)}"
123
+ ),
124
+ detail={"lanes": list(lanes)},
125
+ )
126
+ )
127
+ checks.append(provider_template_coverage(lanes, templates))
128
+ checks.extend(
129
+ _global_runtime_checks(
130
+ probe_runtime=probe_runtime,
131
+ http_timeout=http_timeout,
132
+ )
133
+ )
134
+
135
+ lane_configs = config.get("lanes")
136
+ if not isinstance(lane_configs, Mapping):
137
+ raise code_mower_config.ConfigError("lanes must be a mapping")
138
+ provider_templates = templates.get("provider_templates")
139
+ if not isinstance(provider_templates, Mapping):
140
+ raise code_mower_config.ConfigError("provider_templates must be a mapping")
141
+
142
+ effective_lanes: list[tuple[str, Mapping[str, Any]]] = []
143
+ for lane_id in lanes:
144
+ lane = lane_configs.get(lane_id)
145
+ if not isinstance(lane, Mapping):
146
+ checks.append(
147
+ DoctorCheck(
148
+ name="lane.load",
149
+ status=STATUS_FAIL,
150
+ lane=lane_id,
151
+ message="selected lane is missing from config",
152
+ remediation=(
153
+ "Add the lane to code-mower.yml or remove it from the "
154
+ "selected profile."
155
+ ),
156
+ )
157
+ )
158
+ continue
159
+ effective = effective_lane(lane_id, lane, provider_templates)
160
+ effective_lanes.append((lane_id, effective))
161
+ checks.extend(
162
+ check_lane_runtime(
163
+ lane_id,
164
+ effective,
165
+ probe_runtime=probe_runtime,
166
+ http_timeout=http_timeout,
167
+ )
168
+ )
169
+
170
+ if "github" in enabled_stages:
171
+ checks.extend(
172
+ check_github_setup(
173
+ config=config,
174
+ lanes=effective_lanes,
175
+ http_timeout=http_timeout,
176
+ actions_cost_sample=actions_cost_sample,
177
+ )
178
+ )
179
+
180
+ if "cloud" in enabled_stages:
181
+ checks.append(check_cloud_token_surface())
182
+
183
+ return DoctorReport(
184
+ config_path=str(config_path),
185
+ provider_templates_path=str(provider_templates_path),
186
+ profile=profile,
187
+ checks=tuple(checks),
188
+ )
@@ -0,0 +1,89 @@
1
+ """Runtime and local toolchain doctor checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import shutil
7
+ import sys
8
+
9
+ from .models import STATUS_FAIL, STATUS_PASS, STATUS_WARN, DoctorCheck
10
+ from .privacy import auth_probe_output_detail
11
+ from .runtime_github_auth import check_github_auth_surface
12
+
13
+ __all__ = (
14
+ "auth_probe_output_detail",
15
+ "check_github_auth_surface",
16
+ "check_pytest",
17
+ "check_python_runtime",
18
+ "check_ripgrep",
19
+ )
20
+
21
+
22
+ def check_python_runtime() -> DoctorCheck:
23
+ version = ".".join(str(part) for part in sys.version_info[:3])
24
+ status = STATUS_PASS if sys.version_info >= (3, 11) else STATUS_FAIL
25
+ return DoctorCheck(
26
+ name="runtime.python",
27
+ status=status,
28
+ message=(
29
+ f"Python {version} satisfies Code Mower's >=3.11 requirement"
30
+ if status == STATUS_PASS
31
+ else f"Python {version} is too old; Code Mower requires >=3.11"
32
+ ),
33
+ detail={
34
+ "executable": sys.executable,
35
+ "version": version,
36
+ "required": ">=3.11",
37
+ },
38
+ remediation=(
39
+ None
40
+ if status == STATUS_PASS
41
+ else "Run Code Mower with Python >=3.11, then rerun doctor."
42
+ ),
43
+ )
44
+
45
+
46
+ def check_ripgrep() -> DoctorCheck:
47
+ path = shutil.which("rg")
48
+ if path:
49
+ return DoctorCheck(
50
+ name="runtime.ripgrep",
51
+ status=STATUS_PASS,
52
+ message="rg found",
53
+ detail={"command": "rg", "path": path},
54
+ )
55
+ return DoctorCheck(
56
+ name="runtime.ripgrep",
57
+ status=STATUS_WARN,
58
+ message="rg was not found; reviewer CLIs may fall back to slower grep tools",
59
+ detail={"command": "rg"},
60
+ remediation=(
61
+ "Install ripgrep, for example `brew install ripgrep` on macOS or "
62
+ "`apt-get install ripgrep` on Ubuntu, and ensure rg is on PATH."
63
+ ),
64
+ )
65
+
66
+
67
+ def check_pytest() -> DoctorCheck:
68
+ spec = importlib.util.find_spec("pytest")
69
+ if spec is not None:
70
+ return DoctorCheck(
71
+ name="runtime.pytest",
72
+ status=STATUS_PASS,
73
+ message="pytest import is available for product-side test wrappers",
74
+ detail={"module": "pytest"},
75
+ )
76
+ return DoctorCheck(
77
+ name="runtime.pytest",
78
+ status=STATUS_WARN,
79
+ message=(
80
+ "pytest is not installed in this Python environment; standalone "
81
+ "easy-mode does not require it, but product-side Code Mower test "
82
+ "wrappers often do"
83
+ ),
84
+ detail={"module": "pytest"},
85
+ remediation=(
86
+ "Install pytest in the product repository virtualenv before running "
87
+ "product-side wrapper tests, for example `python -m pip install pytest`."
88
+ ),
89
+ )
@@ -0,0 +1,148 @@
1
+ """GitHub CLI/token auth doctor probe."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+
9
+ from .models import STATUS_PASS, STATUS_WARN, DoctorCheck
10
+ from .privacy import auth_probe_output_detail
11
+
12
+
13
+ def _github_auth_probe(
14
+ gh_path: str,
15
+ *,
16
+ http_timeout: int,
17
+ ) -> tuple[int | None, str, str | None]:
18
+ try:
19
+ completed = subprocess.run(
20
+ [gh_path, "auth", "status"],
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, "", str(exc)
28
+ output = (completed.stdout or completed.stderr or "").strip()
29
+ return completed.returncode, output, None
30
+
31
+
32
+ def _github_auth_probe_check(
33
+ *,
34
+ token_env: list[str],
35
+ gh_path: str,
36
+ http_timeout: int,
37
+ ) -> DoctorCheck:
38
+ returncode, output, error = _github_auth_probe(
39
+ gh_path,
40
+ http_timeout=http_timeout,
41
+ )
42
+ if error is not None:
43
+ qualifier = "token env is set, but " if token_env else ""
44
+ return DoctorCheck(
45
+ name="runtime.github_auth",
46
+ status=STATUS_WARN,
47
+ message=f"GitHub {qualifier}auth probe failed: {error}",
48
+ detail={"token_env": token_env, "gh_path": gh_path},
49
+ remediation=(
50
+ "Run `gh auth status` locally, refresh the token if needed, "
51
+ "or export a valid GITHUB_TOKEN/GH_TOKEN."
52
+ if token_env
53
+ else "Run `gh auth login` or export GITHUB_TOKEN/GH_TOKEN, "
54
+ "then rerun doctor."
55
+ ),
56
+ )
57
+ if returncode == 0:
58
+ message = (
59
+ "GitHub token env and CLI auth probe are valid"
60
+ if token_env
61
+ else "GitHub CLI auth probe succeeded"
62
+ )
63
+ return DoctorCheck(
64
+ name="runtime.github_auth",
65
+ status=STATUS_PASS,
66
+ message=message,
67
+ detail={
68
+ "token_env": token_env,
69
+ "gh_path": gh_path,
70
+ "returncode": returncode,
71
+ **auth_probe_output_detail(output),
72
+ },
73
+ )
74
+ message = (
75
+ f"GitHub token env is set, but auth probe exited {returncode}"
76
+ if token_env
77
+ else f"GitHub CLI auth probe exited {returncode}"
78
+ )
79
+ return DoctorCheck(
80
+ name="runtime.github_auth",
81
+ status=STATUS_WARN,
82
+ message=message,
83
+ detail={
84
+ "token_env": token_env,
85
+ "gh_path": gh_path,
86
+ "returncode": returncode,
87
+ **auth_probe_output_detail(output),
88
+ },
89
+ remediation=(
90
+ "Run `gh auth status`, refresh CLI auth with `gh auth login`, "
91
+ "or export a valid GITHUB_TOKEN/GH_TOKEN."
92
+ if token_env
93
+ else "Run `gh auth login` or export GITHUB_TOKEN/GH_TOKEN, "
94
+ "then rerun doctor."
95
+ ),
96
+ )
97
+
98
+
99
+ def check_github_auth_surface(
100
+ *,
101
+ probe_runtime: bool,
102
+ http_timeout: int,
103
+ ) -> DoctorCheck:
104
+ token_env = [name for name in ("GITHUB_TOKEN", "GH_TOKEN") if os.environ.get(name)]
105
+ gh_path = shutil.which("gh")
106
+ if token_env:
107
+ if probe_runtime and gh_path:
108
+ return _github_auth_probe_check(
109
+ token_env=token_env,
110
+ gh_path=gh_path,
111
+ http_timeout=http_timeout,
112
+ )
113
+ return DoctorCheck(
114
+ name="runtime.github_auth",
115
+ status=STATUS_PASS,
116
+ message="GitHub token env is set",
117
+ detail={"token_env": token_env, "gh_path": gh_path or ""},
118
+ )
119
+ if gh_path:
120
+ if probe_runtime:
121
+ return _github_auth_probe_check(
122
+ token_env=[],
123
+ gh_path=gh_path,
124
+ http_timeout=http_timeout,
125
+ )
126
+ return DoctorCheck(
127
+ name="runtime.github_auth",
128
+ status=STATUS_WARN,
129
+ message="GitHub CLI is available, but token env is not set and auth was not probed",
130
+ detail={"token_env": [], "gh_path": gh_path},
131
+ remediation=(
132
+ "Run with --probe-runtime to verify gh auth, or export "
133
+ "GITHUB_TOKEN/GH_TOKEN before enabling GitHub-backed lanes."
134
+ ),
135
+ )
136
+ return DoctorCheck(
137
+ name="runtime.github_auth",
138
+ status=STATUS_WARN,
139
+ message="neither GITHUB_TOKEN/GH_TOKEN nor gh CLI was found",
140
+ detail={"token_env": [], "gh_path": ""},
141
+ remediation=(
142
+ "Install the GitHub CLI and run `gh auth login`, or export "
143
+ "GITHUB_TOKEN/GH_TOKEN."
144
+ ),
145
+ )
146
+
147
+
148
+ __all__ = ("check_github_auth_surface",)