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.
- code_mower/__init__.py +3 -0
- code_mower/adapters/__init__.py +39 -0
- code_mower/adapters/_base.py +148 -0
- code_mower/adapters/cursor_bugbot.py +97 -0
- code_mower/adapters/gitar.py +111 -0
- code_mower/adapters/greptile.py +191 -0
- code_mower/adapters/qodo.py +140 -0
- code_mower/antigravity_cli_audit_pr.py +241 -0
- code_mower/audit_handoff_log.py +345 -0
- code_mower/audit_labeler_lib.py +311 -0
- code_mower/audit_progress.py +203 -0
- code_mower/blind_review_artifacts.py +562 -0
- code_mower/blind_review_coordinator.py +276 -0
- code_mower/bootstrap.py +524 -0
- code_mower/builder_experiment.py +539 -0
- code_mower/calibration/__init__.py +181 -0
- code_mower/calibration/arms.py +267 -0
- code_mower/calibration/auto_discovery.py +348 -0
- code_mower/calibration/commands.py +192 -0
- code_mower/calibration/context_inputs.py +199 -0
- code_mower/calibration/corpus.py +91 -0
- code_mower/calibration/evidence.py +20 -0
- code_mower/calibration/evidence_report.py +360 -0
- code_mower/calibration/identity.py +27 -0
- code_mower/calibration/metrics.py +18 -0
- code_mower/calibration/overlap.py +103 -0
- code_mower/calibration/planning.py +309 -0
- code_mower/calibration/policy.py +207 -0
- code_mower/calibration/results.py +315 -0
- code_mower/calibration/run_results.py +147 -0
- code_mower/calibration/run_status.py +64 -0
- code_mower/calibration/runner.py +360 -0
- code_mower/calibration/truth.py +188 -0
- code_mower/calibration/value_report.py +142 -0
- code_mower/checks.py +402 -0
- code_mower/claude_audit_pr.py +1126 -0
- code_mower/claude_cli_bounce.py +303 -0
- code_mower/claude_cli_environment.py +73 -0
- code_mower/clear_stale.py +374 -0
- code_mower/cli.py +537 -0
- code_mower/cloud.py +674 -0
- code_mower/cloud_client/__init__.py +173 -0
- code_mower/cloud_client/bundle.py +155 -0
- code_mower/cloud_client/doctor.py +206 -0
- code_mower/cloud_client/dogfood.py +78 -0
- code_mower/cloud_client/endpoints.py +114 -0
- code_mower/cloud_client/errors.py +7 -0
- code_mower/cloud_client/events.py +278 -0
- code_mower/cloud_client/export.py +272 -0
- code_mower/cloud_client/git_metadata.py +46 -0
- code_mower/cloud_client/manifest.py +39 -0
- code_mower/cloud_client/operations.py +448 -0
- code_mower/cloud_client/reports.py +45 -0
- code_mower/cloud_client/setup.py +205 -0
- code_mower/cloud_client/upload.py +97 -0
- code_mower/code_mower_calibration.py +598 -0
- code_mower/code_mower_context_packs.py +591 -0
- code_mower/code_mower_merge.py +227 -0
- code_mower/code_mower_telemetry.py +561 -0
- code_mower/coderabbit_cli_audit_pr.py +526 -0
- code_mower/codex_audit_env_preflight.py +220 -0
- code_mower/codex_audit_pr.py +1738 -0
- code_mower/codex_audit_schema_smoke.py +160 -0
- code_mower/codex_audit_verdict.schema.json +44 -0
- code_mower/config.py +655 -0
- code_mower/doctor.py +161 -0
- code_mower/doctor_checks/__init__.py +104 -0
- code_mower/doctor_checks/cloud.py +129 -0
- code_mower/doctor_checks/common.py +215 -0
- code_mower/doctor_checks/github.py +128 -0
- code_mower/doctor_checks/github_actions.py +11 -0
- code_mower/doctor_checks/github_actions_cost.py +99 -0
- code_mower/doctor_checks/github_actions_cost_summary.py +111 -0
- code_mower/doctor_checks/github_actions_failure_annotations.py +27 -0
- code_mower/doctor_checks/github_actions_failure_models.py +47 -0
- code_mower/doctor_checks/github_actions_failure_scan.py +200 -0
- code_mower/doctor_checks/github_actions_failure_selection.py +63 -0
- code_mower/doctor_checks/github_actions_failures.py +103 -0
- code_mower/doctor_checks/github_actions_permissions.py +55 -0
- code_mower/doctor_checks/github_api.py +79 -0
- code_mower/doctor_checks/github_branch.py +56 -0
- code_mower/doctor_checks/github_config.py +25 -0
- code_mower/doctor_checks/github_provider.py +61 -0
- code_mower/doctor_checks/github_repo.py +120 -0
- code_mower/doctor_checks/groups.py +36 -0
- code_mower/doctor_checks/models.py +96 -0
- code_mower/doctor_checks/output.py +86 -0
- code_mower/doctor_checks/presets.py +64 -0
- code_mower/doctor_checks/privacy.py +20 -0
- code_mower/doctor_checks/provider_api_model.py +138 -0
- code_mower/doctor_checks/provider_api_model_openai.py +29 -0
- code_mower/doctor_checks/provider_api_model_profiles.py +137 -0
- code_mower/doctor_checks/provider_env.py +113 -0
- code_mower/doctor_checks/provider_env_required.py +56 -0
- code_mower/doctor_checks/provider_env_tokens.py +100 -0
- code_mower/doctor_checks/provider_local_cli.py +162 -0
- code_mower/doctor_checks/provider_local_cli_commands.py +47 -0
- code_mower/doctor_checks/provider_local_cli_probe_config.py +70 -0
- code_mower/doctor_checks/provider_probe.py +20 -0
- code_mower/doctor_checks/provider_probe_auth.py +52 -0
- code_mower/doctor_checks/provider_probe_evaluation.py +109 -0
- code_mower/doctor_checks/provider_probe_json.py +45 -0
- code_mower/doctor_checks/provider_probe_remediation.py +39 -0
- code_mower/doctor_checks/providers.py +159 -0
- code_mower/doctor_checks/registry.py +69 -0
- code_mower/doctor_checks/runner.py +188 -0
- code_mower/doctor_checks/runtime.py +89 -0
- code_mower/doctor_checks/runtime_github_auth.py +148 -0
- code_mower/gemini_cli_audit_pr.py +897 -0
- code_mower/hermes_cli_audit_pr.py +436 -0
- code_mower/init.py +888 -0
- code_mower/lane_configs/__init__.py +37 -0
- code_mower/lane_configs/aider.py +32 -0
- code_mower/lane_configs/antigravity_cli.py +35 -0
- code_mower/lane_configs/claude.py +35 -0
- code_mower/lane_configs/codex.py +32 -0
- code_mower/lane_configs/devin.py +33 -0
- code_mower/lane_configs/gemini_cli.py +35 -0
- code_mower/lane_configs/hermes_cli.py +35 -0
- code_mower/lane_configs/local_llm.py +31 -0
- code_mower/local_llm_audit_pr.py +1364 -0
- code_mower/local_llm_bakeoff.py +458 -0
- code_mower/local_llm_calibration.py +441 -0
- code_mower/local_llm_profiles.py +66 -0
- code_mower/migration.py +508 -0
- code_mower/migration_install.py +292 -0
- code_mower/migration_mirror.py +392 -0
- code_mower/migration_readiness.py +237 -0
- code_mower/migration_rehearsal.py +718 -0
- code_mower/next_steps.py +441 -0
- code_mower/package.py +673 -0
- code_mower/package_content.py +444 -0
- code_mower/package_manifest.py +452 -0
- code_mower/package_paths.py +53 -0
- code_mower/package_rendering.py +90 -0
- code_mower/package_static.py +585 -0
- code_mower/prompts.py +267 -0
- code_mower/provider_registry.py +469 -0
- code_mower/provider_runners/__init__.py +60 -0
- code_mower/provider_runners/comments.py +31 -0
- code_mower/provider_runners/git.py +46 -0
- code_mower/provider_runners/github_auth.py +61 -0
- code_mower/provider_runners/github_pr.py +120 -0
- code_mower/provider_runners/process.py +58 -0
- code_mower/provider_runners/repo_paths.py +23 -0
- code_mower/provider_runners/text_schema.py +41 -0
- code_mower/provider_runners/verdict_artifacts.py +103 -0
- code_mower/provider_runners/workspace.py +57 -0
- code_mower/release_readiness.py +549 -0
- code_mower/reviewer_metrics.py +389 -0
- code_mower/saas_reviewer_labeler.py +809 -0
- code_mower/secrets.py +89 -0
- code_mower/templates/builder-experiment.example.json +55 -0
- code_mower/templates/calibration-corpus.example.json +129 -0
- code_mower/templates/calibration-corpus.json +129 -0
- code_mower/templates/code-mower.example.yml +423 -0
- code_mower/templates/context-packs.example.json +150 -0
- code_mower/templates/lane_prompts/base-audit.md +22 -0
- code_mower/templates/lane_prompts/calibration-policy.md +21 -0
- code_mower/templates/lane_prompts/context-driven-quality.md +21 -0
- code_mower/templates/lane_prompts/docs-design.md +12 -0
- code_mower/templates/lane_prompts/generic-programming.md +21 -0
- code_mower/templates/lane_prompts/operability.md +22 -0
- code_mower/templates/lane_prompts/package-runtime.md +12 -0
- code_mower/templates/lane_prompts/security-threat-model.md +22 -0
- code_mower/templates/product-support/code_mower +216 -0
- code_mower/templates/product-support/code_mower_standalone_pin.env +7 -0
- code_mower/templates/product-support/code_mower_standalone_shadow.sh +151 -0
- code_mower/templates/product-support/run_claude_audit_pr.sh +32 -0
- code_mower/templates/product-support/run_codex_audit_pr.sh +32 -0
- code_mower/templates/product-support/safe_gh_comment.py +96 -0
- code_mower/templates/providers.yml +454 -0
- code_mower/templates/reviewer-spend.example.json +28 -0
- code_mower/templates/reviewer-value-report.example.md +20 -0
- code_mower/templates/workflows/private-standalone-shadow.yml.j2 +106 -0
- code_mower/templates/workflows/review-clear-stale.yml.j2 +83 -0
- code_mower/trailer_comment_labeler.py +207 -0
- code_mower/versioning.py +32 -0
- code_mower-0.5.0b5.dist-info/METADATA +302 -0
- code_mower-0.5.0b5.dist-info/RECORD +185 -0
- code_mower-0.5.0b5.dist-info/WHEEL +5 -0
- code_mower-0.5.0b5.dist-info/entry_points.txt +2 -0
- code_mower-0.5.0b5.dist-info/licenses/LICENSE +202 -0
- code_mower-0.5.0b5.dist-info/licenses/NOTICE +10 -0
- 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",)
|