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
code_mower/doctor.py ADDED
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env python3
2
+ """Run provider-neutral Code Mower setup and runtime checks."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Sequence
11
+
12
+ if __package__ in {None, ""}:
13
+ module_dir = Path(__file__).resolve().parent
14
+ sys.path.insert(0, str(module_dir.parent))
15
+ if module_dir.name == "code_mower": # pragma: no cover - extracted direct CLI.
16
+ from code_mower import config as code_mower_config
17
+ from code_mower import doctor_checks as _doctor_checks
18
+ from code_mower import package as code_mower_package
19
+ else:
20
+ from tools import code_mower_config, code_mower_package, doctor_checks as _doctor_checks
21
+ elif __package__ == "tools":
22
+ from tools import code_mower_config, code_mower_package, doctor_checks as _doctor_checks
23
+ else: # pragma: no cover - exercised after package extraction.
24
+ from . import config as code_mower_config
25
+ from . import doctor_checks as _doctor_checks
26
+ from . import package as code_mower_package
27
+
28
+
29
+ ACTIONS_COST_SAMPLE_DEFAULT = _doctor_checks.ACTIONS_COST_SAMPLE_DEFAULT
30
+ ACTIONS_COST_SAMPLE_MAX = _doctor_checks.ACTIONS_COST_SAMPLE_MAX
31
+ DEFAULT_CLOUD_TOKEN_DIR = _doctor_checks.DEFAULT_CLOUD_TOKEN_DIR
32
+ DEFAULT_CLOUD_TOKEN_ENV = _doctor_checks.DEFAULT_CLOUD_TOKEN_ENV
33
+ STATUS_FAIL = _doctor_checks.STATUS_FAIL
34
+ STATUS_PASS = _doctor_checks.STATUS_PASS
35
+ STATUS_SKIP = _doctor_checks.STATUS_SKIP
36
+ STATUS_WARN = _doctor_checks.STATUS_WARN
37
+ DoctorCheck = _doctor_checks.DoctorCheck
38
+ DoctorReport = _doctor_checks.DoctorReport
39
+ _auth_probe_output_detail = _doctor_checks.auth_probe_output_detail
40
+ _check_cloud_token_surface = _doctor_checks.check_cloud_token_surface
41
+ _evaluate_json_probe = _doctor_checks.evaluate_json_probe
42
+ _local_cli_probe_remediation = _doctor_checks.local_cli_probe_remediation
43
+ render_doctor_text = _doctor_checks.render_doctor_text
44
+ resolve_doctor_config_path = _doctor_checks.resolve_doctor_config_path
45
+ resolve_doctor_config_path_for_script = _doctor_checks.resolve_doctor_config_path_for_script
46
+ resolve_doctor_provider_templates_path = _doctor_checks.resolve_doctor_provider_templates_path
47
+ run_doctor = _doctor_checks.run_doctor
48
+ _token_file_mentions_cloud_token = _doctor_checks.token_file_mentions_cloud_token
49
+ _apply_first_run_defaults = _doctor_checks.apply_first_run_defaults
50
+
51
+
52
+ _DOCTOR_COMPAT_EXPORTS = (
53
+ DEFAULT_CLOUD_TOKEN_DIR,
54
+ DEFAULT_CLOUD_TOKEN_ENV,
55
+ STATUS_FAIL,
56
+ STATUS_PASS,
57
+ STATUS_SKIP,
58
+ STATUS_WARN,
59
+ DoctorCheck,
60
+ DoctorReport,
61
+ _auth_probe_output_detail,
62
+ _check_cloud_token_surface,
63
+ _evaluate_json_probe,
64
+ _apply_first_run_defaults,
65
+ _local_cli_probe_remediation,
66
+ resolve_doctor_config_path_for_script,
67
+ resolve_doctor_provider_templates_path,
68
+ _token_file_mentions_cloud_token,
69
+ )
70
+
71
+
72
+ def main(argv: Sequence[str] | None = None) -> int:
73
+ parser = argparse.ArgumentParser(description=__doc__)
74
+ parser.add_argument("config", nargs="?", default="code-mower.yml")
75
+ parser.add_argument(
76
+ "--provider-templates",
77
+ default=code_mower_package.DEFAULT_PROVIDER_TEMPLATES,
78
+ )
79
+ parser.add_argument("--profile", default=None)
80
+ parser.add_argument(
81
+ "--easy",
82
+ action="store_true",
83
+ help=(
84
+ "first-run alias for --profile recommended; if code-mower.yml is "
85
+ "absent, use the packaged example config"
86
+ ),
87
+ )
88
+ parser.add_argument(
89
+ "--v05",
90
+ action="store_true",
91
+ help=(
92
+ "v0.5 early-adopter preset: --easy --profile recommended "
93
+ "--probe-runtime --github --cloud"
94
+ ),
95
+ )
96
+ parser.add_argument(
97
+ "--preflight",
98
+ action="store_true",
99
+ help=(
100
+ "friendly alias for the v0.5 first-run preset: --easy "
101
+ "--profile recommended --probe-runtime --github --cloud"
102
+ ),
103
+ )
104
+ parser.add_argument("--probe-runtime", action="store_true")
105
+ parser.add_argument(
106
+ "--github",
107
+ action="store_true",
108
+ help="inspect GitHub repo visibility, branch protection, and provider setup hints",
109
+ )
110
+ parser.add_argument(
111
+ "--cloud",
112
+ action="store_true",
113
+ help="check optional Code Mower Cloud token setup without reading or printing token values",
114
+ )
115
+ parser.add_argument("--http-timeout", type=int, default=5)
116
+ parser.add_argument(
117
+ "--actions-cost-sample",
118
+ type=int,
119
+ default=ACTIONS_COST_SAMPLE_DEFAULT,
120
+ help=(
121
+ "number of recent Actions runs to sample for private-repo cost "
122
+ f"diagnostics, capped at {ACTIONS_COST_SAMPLE_MAX}"
123
+ ),
124
+ )
125
+ parser.add_argument("--strict", action="store_true")
126
+ parser.add_argument("--json", action="store_true")
127
+ args = parser.parse_args(argv)
128
+
129
+ _apply_first_run_defaults(args)
130
+ if args.easy and args.profile is None:
131
+ args.profile = "recommended"
132
+
133
+ try:
134
+ provider_templates_path = resolve_doctor_provider_templates_path(args.provider_templates)
135
+ report = run_doctor(
136
+ config_path=resolve_doctor_config_path(args.config, easy=args.easy),
137
+ provider_templates_path=provider_templates_path,
138
+ profile=args.profile,
139
+ probe_runtime=args.probe_runtime,
140
+ github=args.github,
141
+ cloud=args.cloud,
142
+ http_timeout=args.http_timeout,
143
+ actions_cost_sample=args.actions_cost_sample,
144
+ )
145
+ except code_mower_config.ConfigError as exc:
146
+ print(f"error: {exc}", file=sys.stderr)
147
+ return 1
148
+
149
+ if args.json:
150
+ print(json.dumps(report.as_dict(), indent=2, sort_keys=True))
151
+ else:
152
+ print(render_doctor_text(report), end="")
153
+ if report.failures:
154
+ return 1
155
+ if args.strict and report.warnings:
156
+ return 1
157
+ return 0
158
+
159
+
160
+ if __name__ == "__main__":
161
+ raise SystemExit(main())
@@ -0,0 +1,104 @@
1
+ """Doctor check registry primitives.
2
+
3
+ The public CLI remains ``code-mower doctor``. This package keeps the
4
+ check/report data model independent from the large command adapter so runtime,
5
+ GitHub, provider, cloud, and output checks can move behind registries without
6
+ changing the CLI contract.
7
+ """
8
+
9
+ from .cloud import (
10
+ DEFAULT_CLOUD_TOKEN_DIR,
11
+ DEFAULT_CLOUD_TOKEN_ENV,
12
+ check_cloud_token_surface,
13
+ token_file_mentions_cloud_token,
14
+ )
15
+ from .common import (
16
+ ACTIONS_COST_SAMPLE_DEFAULT,
17
+ ACTIONS_COST_SAMPLE_MAX,
18
+ load_inputs,
19
+ )
20
+ from .github import check_github_setup
21
+ from .groups import GROUP_LABELS, doctor_check_group_id
22
+ from .models import (
23
+ STATUS_FAIL,
24
+ STATUS_PASS,
25
+ STATUS_SKIP,
26
+ STATUS_WARN,
27
+ DoctorCheck,
28
+ DoctorReport,
29
+ )
30
+ from .output import doctor_output_group, render_doctor_text
31
+ from .presets import (
32
+ apply_first_run_defaults,
33
+ resolve_doctor_config_path,
34
+ resolve_doctor_config_path_for_script,
35
+ resolve_doctor_provider_templates_path,
36
+ )
37
+ from .providers import (
38
+ check_lane_runtime,
39
+ effective_lane,
40
+ provider_template_coverage,
41
+ selected_lanes,
42
+ )
43
+ from .provider_probe import evaluate_json_probe, local_cli_probe_remediation
44
+ from .privacy import auth_probe_output_detail
45
+ from .registry import (
46
+ DEFAULT_CHECK_GROUPS,
47
+ BASE_DOCTOR_STAGES,
48
+ OPTIONAL_DOCTOR_STAGES,
49
+ DoctorCheckGroup,
50
+ DoctorCheckStage,
51
+ build_doctor_run_plan,
52
+ default_check_group_ids,
53
+ )
54
+ from .runtime import (
55
+ check_github_auth_surface,
56
+ check_pytest,
57
+ check_python_runtime,
58
+ check_ripgrep,
59
+ )
60
+ from .runner import run_doctor
61
+
62
+ __all__ = [
63
+ "DEFAULT_CHECK_GROUPS",
64
+ "GROUP_LABELS",
65
+ "BASE_DOCTOR_STAGES",
66
+ "OPTIONAL_DOCTOR_STAGES",
67
+ "ACTIONS_COST_SAMPLE_DEFAULT",
68
+ "ACTIONS_COST_SAMPLE_MAX",
69
+ "DEFAULT_CLOUD_TOKEN_DIR",
70
+ "DEFAULT_CLOUD_TOKEN_ENV",
71
+ "DoctorCheck",
72
+ "DoctorCheckGroup",
73
+ "DoctorCheckStage",
74
+ "DoctorReport",
75
+ "STATUS_FAIL",
76
+ "STATUS_PASS",
77
+ "STATUS_SKIP",
78
+ "STATUS_WARN",
79
+ "apply_first_run_defaults",
80
+ "auth_probe_output_detail",
81
+ "build_doctor_run_plan",
82
+ "check_cloud_token_surface",
83
+ "check_github_auth_surface",
84
+ "check_github_setup",
85
+ "check_lane_runtime",
86
+ "check_pytest",
87
+ "check_python_runtime",
88
+ "check_ripgrep",
89
+ "default_check_group_ids",
90
+ "doctor_check_group_id",
91
+ "doctor_output_group",
92
+ "effective_lane",
93
+ "evaluate_json_probe",
94
+ "load_inputs",
95
+ "local_cli_probe_remediation",
96
+ "provider_template_coverage",
97
+ "render_doctor_text",
98
+ "resolve_doctor_config_path",
99
+ "resolve_doctor_config_path_for_script",
100
+ "resolve_doctor_provider_templates_path",
101
+ "run_doctor",
102
+ "selected_lanes",
103
+ "token_file_mentions_cloud_token",
104
+ ]
@@ -0,0 +1,129 @@
1
+ """Code Mower Cloud doctor checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import stat
7
+ from pathlib import Path
8
+
9
+ from .models import STATUS_PASS, STATUS_SKIP, STATUS_WARN, DoctorCheck
10
+
11
+ DEFAULT_CLOUD_TOKEN_ENV = "CODE_MOWER_CLOUD_TOKEN"
12
+ DEFAULT_CLOUD_TOKEN_DIR = Path("~/.config/code-mower/tokens")
13
+
14
+
15
+ def token_file_mentions_cloud_token(path: Path, token_env: str) -> bool | None:
16
+ try:
17
+ text = path.read_text(encoding="utf-8")
18
+ except (OSError, UnicodeDecodeError):
19
+ return None
20
+ for line in text.splitlines():
21
+ stripped = line.strip()
22
+ if not stripped or stripped.startswith("#"):
23
+ continue
24
+ if stripped.startswith("export "):
25
+ stripped = stripped.removeprefix("export ").strip()
26
+ if stripped.startswith(f"{token_env}="):
27
+ return True
28
+ return False
29
+
30
+
31
+ def check_cloud_token_surface(
32
+ *,
33
+ token_env: str = DEFAULT_CLOUD_TOKEN_ENV,
34
+ token_dir: Path | None = None,
35
+ ) -> DoctorCheck:
36
+ """Check optional Code Mower Cloud token setup without exposing secrets."""
37
+
38
+ if os.environ.get(token_env):
39
+ return DoctorCheck(
40
+ name="cloud.token",
41
+ status=STATUS_PASS,
42
+ message="Code Mower Cloud token env is set",
43
+ detail={"token_env": token_env, "source": "env"},
44
+ )
45
+
46
+ directory = (token_dir or DEFAULT_CLOUD_TOKEN_DIR).expanduser()
47
+ redacted_dir = "~/.config/code-mower/tokens"
48
+ if not directory.exists():
49
+ return DoctorCheck(
50
+ name="cloud.token",
51
+ status=STATUS_SKIP,
52
+ message="optional Code Mower Cloud token is not configured",
53
+ detail={"token_env": token_env, "token_dir": redacted_dir},
54
+ remediation=(
55
+ "Cloud upload is optional. To enable it, create or receive a "
56
+ "developer/team token, then run `code-mower cloud setup "
57
+ "--token-stdin` and source the generated env file."
58
+ ),
59
+ )
60
+
61
+ token_files: list[str] = []
62
+ unreadable_files: list[str] = []
63
+ insecure_files: list[str] = []
64
+ for path in sorted(directory.glob("*.env")):
65
+ has_token = token_file_mentions_cloud_token(path, token_env)
66
+ if has_token is None:
67
+ unreadable_files.append(path.name)
68
+ continue
69
+ if not has_token:
70
+ continue
71
+ token_files.append(path.name)
72
+ try:
73
+ mode = stat.S_IMODE(path.stat().st_mode)
74
+ except OSError:
75
+ unreadable_files.append(path.name)
76
+ continue
77
+ if mode & 0o077:
78
+ insecure_files.append(path.name)
79
+
80
+ if unreadable_files:
81
+ return DoctorCheck(
82
+ name="cloud.token",
83
+ status=STATUS_WARN,
84
+ message="could not inspect one or more Code Mower Cloud token files",
85
+ detail={
86
+ "token_env": token_env,
87
+ "token_dir": redacted_dir,
88
+ "unreadable_file_count": len(unreadable_files),
89
+ "unreadable_files": unreadable_files,
90
+ },
91
+ remediation=(
92
+ "Verify token files under ~/.config/code-mower/tokens are UTF-8 "
93
+ "env files owned by the current user."
94
+ ),
95
+ )
96
+
97
+ if not token_files:
98
+ return DoctorCheck(
99
+ name="cloud.token",
100
+ status=STATUS_SKIP,
101
+ message="optional Code Mower Cloud token file was not found",
102
+ detail={"token_env": token_env, "token_dir": redacted_dir},
103
+ remediation=(
104
+ "Cloud upload is optional. To enable it, run `code-mower cloud "
105
+ "setup --token-stdin` and source the generated env file."
106
+ ),
107
+ )
108
+
109
+ detail = {
110
+ "token_env": token_env,
111
+ "token_dir": redacted_dir,
112
+ "token_file_count": len(token_files),
113
+ "token_files": token_files,
114
+ }
115
+ if insecure_files:
116
+ return DoctorCheck(
117
+ name="cloud.token",
118
+ status=STATUS_WARN,
119
+ message="Code Mower Cloud token file permissions are too broad",
120
+ detail={**detail, "insecure_files": insecure_files},
121
+ remediation="Run `chmod 600 ~/.config/code-mower/tokens/*.env`.",
122
+ )
123
+
124
+ return DoctorCheck(
125
+ name="cloud.token",
126
+ status=STATUS_PASS,
127
+ message="Code Mower Cloud token file is configured",
128
+ detail=detail,
129
+ )
@@ -0,0 +1,215 @@
1
+ """Shared doctor helper functions and dependency shims."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any, Mapping, Sequence
8
+
9
+ from .models import STATUS_FAIL, STATUS_PASS, STATUS_SKIP, STATUS_WARN, DoctorCheck
10
+
11
+ if __package__ in {None, ""}:
12
+ module_dir = Path(__file__).resolve().parent
13
+ sys.path.insert(0, str(module_dir.parents[1]))
14
+ if module_dir.parent.name == "code_mower": # pragma: no cover - direct package path.
15
+ from code_mower import config as code_mower_config
16
+ from code_mower import local_llm_profiles
17
+ from code_mower import package as code_mower_package
18
+ from code_mower import secrets as code_mower_secrets
19
+ else: # pragma: no cover - extracted direct CLI path.
20
+ from tools import (
21
+ code_mower_config,
22
+ code_mower_package,
23
+ code_mower_secrets,
24
+ local_llm_profiles,
25
+ )
26
+ elif __package__.startswith("tools"):
27
+ from .. import (
28
+ code_mower_config,
29
+ code_mower_package,
30
+ code_mower_secrets,
31
+ local_llm_profiles,
32
+ )
33
+ else: # pragma: no cover - exercised after package extraction.
34
+ from .. import config as code_mower_config
35
+ from .. import local_llm_profiles
36
+ from .. import package as code_mower_package
37
+ from .. import secrets as code_mower_secrets
38
+
39
+
40
+ SUPPORTED_TOKEN_FILE_ENV_NAMES = frozenset({"GEMINI_API_KEY", "GOOGLE_API_KEY"})
41
+ TRUTHY_ENV_VALUES = frozenset({"1", "true", "yes", "on"})
42
+ ACTIONS_BILLING_BLOCK_PATTERNS = (
43
+ "recent account payments have failed",
44
+ "spending limit needs to be increased",
45
+ )
46
+ ACTIONS_INSPECTABLE_FAILURE_CONCLUSIONS = frozenset(
47
+ {"failure", "action_required", "timed_out", "startup_failure"}
48
+ )
49
+ MAX_ACTIONS_FAILED_RUNS_TO_INSPECT = 5
50
+ MAX_ACTIONS_FAILED_JOBS_TO_INSPECT = 20
51
+ ACTIONS_COST_SAMPLE_DEFAULT = 100
52
+ ACTIONS_COST_SAMPLE_MAX = 100
53
+ ACTIONS_METADATA_WORKFLOW_MARKERS = (
54
+ "audit-labeler",
55
+ "labeler",
56
+ "clear-stale",
57
+ "audit-label-cleanup",
58
+ "devin-audit-bridge",
59
+ )
60
+
61
+ __all__ = [
62
+ "ACTIONS_BILLING_BLOCK_PATTERNS",
63
+ "ACTIONS_COST_SAMPLE_DEFAULT",
64
+ "ACTIONS_COST_SAMPLE_MAX",
65
+ "ACTIONS_INSPECTABLE_FAILURE_CONCLUSIONS",
66
+ "ACTIONS_METADATA_WORKFLOW_MARKERS",
67
+ "DoctorCheck",
68
+ "MAX_ACTIONS_FAILED_JOBS_TO_INSPECT",
69
+ "MAX_ACTIONS_FAILED_RUNS_TO_INSPECT",
70
+ "STATUS_FAIL",
71
+ "STATUS_PASS",
72
+ "STATUS_SKIP",
73
+ "STATUS_WARN",
74
+ "SUPPORTED_TOKEN_FILE_ENV_NAMES",
75
+ "TRUTHY_ENV_VALUES",
76
+ "as_mapping",
77
+ "as_sequence",
78
+ "code_mower_config",
79
+ "code_mower_package",
80
+ "code_mower_secrets",
81
+ "join_names",
82
+ "load_inputs",
83
+ "local_cli_remediation",
84
+ "local_llm_profiles",
85
+ "token_remediation",
86
+ ]
87
+
88
+
89
+ def as_mapping(value: Any, name: str) -> Mapping[str, Any]:
90
+ if isinstance(value, Mapping):
91
+ return value
92
+ raise code_mower_config.ConfigError(f"{name} must be a mapping")
93
+
94
+
95
+ def as_sequence(value: Any) -> tuple[Any, ...]:
96
+ if isinstance(value, (list, tuple)):
97
+ return tuple(value)
98
+ return ()
99
+
100
+
101
+ def join_names(names: Sequence[str]) -> str:
102
+ return ", ".join(str(name) for name in names)
103
+
104
+
105
+ def token_remediation(
106
+ missing: Sequence[str],
107
+ missing_any: Sequence[Sequence[str]],
108
+ ) -> str:
109
+ parts: list[str] = []
110
+ if missing:
111
+ parts.append(
112
+ "set "
113
+ + join_names(missing)
114
+ + " in your shell or GitHub secret store before enabling this lane"
115
+ )
116
+ for group in missing_any:
117
+ names = [str(name) for name in group]
118
+ if not names:
119
+ continue
120
+ if set(names) & {"GEMINI_API_KEY", "GOOGLE_API_KEY"}:
121
+ parts.append(
122
+ "set GEMINI_API_KEY or GOOGLE_API_KEY, or run "
123
+ "`code-mower init auth gemini` and export GEMINI_API_KEY_FILE"
124
+ )
125
+ else:
126
+ parts.append("set one of " + join_names(names))
127
+ if not parts:
128
+ return ""
129
+ return "; ".join(parts) + "."
130
+
131
+
132
+ def local_cli_remediation(commands: Sequence[str], command_env: str = "") -> str:
133
+ command_text = " or ".join(str(command) for command in commands if command)
134
+ if not command_text:
135
+ return (
136
+ "Configure provider_config.command or a non-empty provider id for "
137
+ "this lane, then rerun doctor."
138
+ )
139
+ if command_env:
140
+ return (
141
+ f"Install {command_text}, or set {command_env} to the absolute path "
142
+ "of the provider CLI, then rerun doctor."
143
+ )
144
+ return f"Install {command_text} and ensure it is on PATH, then rerun doctor."
145
+
146
+
147
+ def load_inputs(
148
+ config_path: Path,
149
+ provider_templates_path: Path,
150
+ ) -> tuple[Mapping[str, Any] | None, Mapping[str, Any] | None, list[DoctorCheck]]:
151
+ checks: list[DoctorCheck] = []
152
+ config: Mapping[str, Any] | None = None
153
+ templates: Mapping[str, Any] | None = None
154
+
155
+ try:
156
+ config = code_mower_config.load_config(config_path)
157
+ issues = code_mower_config.validate_config(config)
158
+ except (OSError, code_mower_config.ConfigError) as exc:
159
+ checks.append(
160
+ DoctorCheck(
161
+ name="config.load",
162
+ status=STATUS_FAIL,
163
+ message=f"cannot load config: {exc}",
164
+ remediation=(
165
+ "Run `code-mower init --easy` to render a starter config, "
166
+ "or pass an existing config path to doctor."
167
+ ),
168
+ )
169
+ )
170
+ else:
171
+ if issues:
172
+ checks.append(
173
+ DoctorCheck(
174
+ name="config.validate",
175
+ status=STATUS_FAIL,
176
+ message=code_mower_config._format_issues(issues),
177
+ remediation=(
178
+ "Fix the listed config issues or regenerate the starter "
179
+ "setup with `code-mower init --easy`."
180
+ ),
181
+ )
182
+ )
183
+ else:
184
+ checks.append(
185
+ DoctorCheck(
186
+ name="config.validate",
187
+ status=STATUS_PASS,
188
+ message="config validates",
189
+ )
190
+ )
191
+
192
+ try:
193
+ templates = code_mower_package.load_provider_templates(provider_templates_path)
194
+ except (OSError, code_mower_config.ConfigError) as exc:
195
+ checks.append(
196
+ DoctorCheck(
197
+ name="provider_templates.load",
198
+ status=STATUS_FAIL,
199
+ message=f"cannot load provider templates: {exc}",
200
+ remediation=(
201
+ "Run from the repository root, install the packaged templates, "
202
+ "or pass --provider-templates with a readable catalog path."
203
+ ),
204
+ )
205
+ )
206
+ else:
207
+ checks.append(
208
+ DoctorCheck(
209
+ name="provider_templates.load",
210
+ status=STATUS_PASS,
211
+ message="provider templates load",
212
+ )
213
+ )
214
+
215
+ return config, templates, checks