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,12 @@
1
+ # Package Runtime Lens
2
+
3
+ Use this lens when a PR changes packaging, bootstrap, doctor, provider catalog, CLI dispatch, or extracted-package imports.
4
+
5
+ Focus on:
6
+
7
+ - Repo execution and extracted package execution both work.
8
+ - Runtime choices are explicit: Python version, CLI command, environment variables, token requirements, and optional spend boundaries.
9
+ - Package manifests include every file needed at runtime, including templates, lane configs, adapters, and prompt lenses.
10
+ - Doctor checks reveal missing local CLIs or configuration instead of allowing silent fallback to random system tooling.
11
+
12
+ Block if the reference repo passes but the generated package would import, dispatch, or locate templates incorrectly.
@@ -0,0 +1,22 @@
1
+ # Security Threat Model Lens
2
+
3
+ Use this lens for authentication, authorization, billing, entitlements, debug upload, secrets, storage, network APIs, and any change that crosses a trust boundary.
4
+
5
+ Stance:
6
+
7
+ - Reason from attacker capability, asset value, and trust boundaries before judging code shape.
8
+ - Prefer least privilege, explicit authorization, scoped tokens, auditable decisions, and fail-closed behavior.
9
+ - Treat logs, diagnostics, previews, artifacts, and exported benchmark bundles as possible disclosure surfaces.
10
+ - Consider STRIDE-style threats: spoofing, tampering, repudiation, information disclosure, denial of service, and elevation of privilege.
11
+
12
+ Review focus:
13
+
14
+ - Flag missing server-side authorization, quota, entitlement, ownership, or replay checks.
15
+ - Flag token, secret, account-state, debug payload, or customer-data exposure in logs, artifacts, comments, client state, or shareable reports.
16
+ - Flag client-side-only controls for protected actions.
17
+ - Flag unsafe defaults, broad permissions, ambiguous identity binding, or failure paths that grant access.
18
+ - Flag security-sensitive changes whose tests or diagnostics do not exercise the threat boundary.
19
+
20
+ Block only when the PR creates a plausible security, privacy, billing, entitlement, or data-exposure risk. Do not block on generic hardening suggestions without a concrete exploit path or sensitive asset.
21
+
22
+ When useful, phrase findings with: asset, actor, trust boundary, threat, control, failure mode, exploit path, and expected server-side invariant.
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
5
+ repo_root="$(CDPATH= cd -- "${script_dir}/.." && pwd -P)"
6
+ standalone_shadow="${script_dir}/code_mower_standalone_shadow.sh"
7
+ install_lock_acquired=0
8
+ install_lock_dir=""
9
+
10
+ python_is_supported() {
11
+ "$1" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)' >/dev/null 2>&1
12
+ }
13
+
14
+ resolve_python_candidate() {
15
+ case "$1" in
16
+ */*)
17
+ if [ -x "$1" ]; then
18
+ printf '%s\n' "$1"
19
+ fi
20
+ return 0
21
+ ;;
22
+ *) command -v "$1" 2>/dev/null || true ;;
23
+ esac
24
+ }
25
+
26
+ find_bootstrap_python() {
27
+ local candidate resolved
28
+ if [ -n "${CODE_MOWER_BOOTSTRAP_PYTHON:-}" ]; then
29
+ printf '%s\n' "${CODE_MOWER_BOOTSTRAP_PYTHON}"
30
+ return
31
+ fi
32
+ if [ -n "${CODE_MOWER_PYTHON:-}" ]; then
33
+ printf '%s\n' "${CODE_MOWER_PYTHON}"
34
+ return
35
+ fi
36
+
37
+ for candidate in ${CODE_MOWER_BOOTSTRAP_PYTHON_CANDIDATES:-python3.13 python3.12 python3.11 /opt/homebrew/bin/python3 /usr/local/bin/python3 /opt/homebrew/bin/python3.13 /opt/homebrew/bin/python3.12 /opt/homebrew/bin/python3.11 /usr/local/bin/python3.13 /usr/local/bin/python3.12 /usr/local/bin/python3.11 python3 python}; do
38
+ resolved="$(resolve_python_candidate "${candidate}")"
39
+ if [ -n "${resolved}" ] && python_is_supported "${resolved}"; then
40
+ printf '%s\n' "${resolved}"
41
+ return
42
+ fi
43
+ done
44
+
45
+ resolved="$(command -v python3 2>/dev/null || command -v python 2>/dev/null || true)"
46
+ printf '%s\n' "${resolved:-python3}"
47
+ }
48
+
49
+ bootstrap_python="$(find_bootstrap_python)"
50
+
51
+ if [ "${CODE_MOWER_USE_LOCAL:-}" = "1" ] || [ "${CODE_MOWER_USE_STANDALONE:-1}" = "0" ]; then
52
+ unset CODE_MOWER_STANDALONE_COMMAND
53
+ unset CODE_MOWER_STANDALONE_PATH
54
+ fi
55
+
56
+ if [ -z "${CODE_MOWER_STANDALONE_COMMAND:-}" ] \
57
+ && [ -z "${CODE_MOWER_STANDALONE_PATH:-}" ] \
58
+ && [ -z "${CODE_MOWER_STANDALONE_DELEGATING:-}" ] \
59
+ && [ "${CODE_MOWER_USE_LOCAL:-}" != "1" ] \
60
+ && [ "${CODE_MOWER_USE_STANDALONE:-1}" != "0" ]; then
61
+ if [ -x "${standalone_shadow}" ]; then
62
+ export CODE_MOWER_STANDALONE_DELEGATING=1
63
+ exec "${standalone_shadow}" "$@"
64
+ fi
65
+ echo "warning: standalone delegation expected but ${standalone_shadow} is missing or not executable; running repo-local mirror" >&2
66
+ fi
67
+
68
+ release_install_lock() {
69
+ if [ "${install_lock_acquired}" = "1" ] && [ -n "${install_lock_dir}" ]; then
70
+ rm -rf "${install_lock_dir}"
71
+ fi
72
+ install_lock_acquired=0
73
+ install_lock_dir=""
74
+ }
75
+ trap release_install_lock EXIT
76
+
77
+ acquire_install_lock() {
78
+ local lock_age lock_mtime lock_pid now parent_dir stale_seconds started timeout_seconds
79
+ install_lock_dir="${venv_dir}.install.lock"
80
+ parent_dir="$(dirname -- "${install_lock_dir}")"
81
+ stale_seconds="${CODE_MOWER_STANDALONE_LOCK_STALE_SECONDS:-30}"
82
+ timeout_seconds="${CODE_MOWER_STANDALONE_LOCK_TIMEOUT_SECONDS:-120}"
83
+ started="$(date +%s)"
84
+ mkdir -p "${parent_dir}"
85
+ while ! mkdir "${install_lock_dir}" 2>/dev/null; do
86
+ now="$(date +%s)"
87
+ lock_pid=""
88
+ if [ -f "${install_lock_dir}/pid" ]; then
89
+ lock_pid="$(cat "${install_lock_dir}/pid" 2>/dev/null || true)"
90
+ fi
91
+ lock_mtime="$(stat -c %Y "${install_lock_dir}" 2>/dev/null || stat -f %m "${install_lock_dir}" 2>/dev/null || echo "${now}")"
92
+ case "${lock_mtime}" in
93
+ ''|*[!0-9]*) lock_mtime="${now}" ;;
94
+ esac
95
+ lock_age=$((now - lock_mtime))
96
+ if [ -z "${lock_pid}" ] && [ "${lock_age}" -ge "${stale_seconds}" ]; then
97
+ rm -rf "${install_lock_dir}" 2>/dev/null || true
98
+ continue
99
+ fi
100
+ if [ -n "${lock_pid}" ] && ! kill -0 "${lock_pid}" 2>/dev/null; then
101
+ rm -rf "${install_lock_dir}" 2>/dev/null || true
102
+ continue
103
+ fi
104
+ if [ $((now - started)) -ge "${timeout_seconds}" ]; then
105
+ echo "error: timed out waiting for standalone install lock: ${install_lock_dir}" >&2
106
+ exit 1
107
+ fi
108
+ sleep 0.2
109
+ done
110
+ install_lock_acquired=1
111
+ printf '%s\n' "$$" > "${install_lock_dir}/pid"
112
+ }
113
+
114
+ check_standalone_python() {
115
+ if [ "${CODE_MOWER_SKIP_STANDALONE_PYTHON_CHECK:-}" = "1" ]; then
116
+ return
117
+ fi
118
+ if ! "${bootstrap_python}" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)' >/dev/null 2>&1; then
119
+ echo "error: Code Mower standalone install requires Python >= 3.11; ${bootstrap_python} is unsupported. Set CODE_MOWER_BOOTSTRAP_PYTHON=/path/to/python3.11+ or install python3.11/python3.12." >&2
120
+ exit 1
121
+ fi
122
+ }
123
+
124
+ if [ -n "${CODE_MOWER_STANDALONE_COMMAND:-}" ]; then
125
+ exec "${CODE_MOWER_STANDALONE_COMMAND}" "$@"
126
+ fi
127
+
128
+ if [ -n "${CODE_MOWER_STANDALONE_PATH:-}" ]; then
129
+ standalone_path="${CODE_MOWER_STANDALONE_PATH}"
130
+ if [ ! -f "${standalone_path}/pyproject.toml" ]; then
131
+ echo "error: CODE_MOWER_STANDALONE_PATH does not look like a Code Mower package: ${standalone_path}" >&2
132
+ exit 1
133
+ fi
134
+ standalone_path="$(CDPATH= cd -- "${standalone_path}" && pwd -P)"
135
+
136
+ venv_dir="${CODE_MOWER_STANDALONE_VENV:-${repo_root}/.code-mower-standalone-venv}"
137
+ case "${venv_dir}" in
138
+ /*) ;;
139
+ *) venv_dir="${repo_root}/${venv_dir}" ;;
140
+ esac
141
+ allowed_standalone_venv="${repo_root}/.code-mower-standalone-venv"
142
+ allowed_managed_venv_root="${repo_root}/.code-mower/standalone-venvs"
143
+ venv_parent="$(dirname -- "${venv_dir}")"
144
+ venv_name="$(basename -- "${venv_dir}")"
145
+ if [ -d "${venv_parent}" ]; then
146
+ venv_parent="$(CDPATH= cd -- "${venv_parent}" && pwd -P)"
147
+ venv_dir="${venv_parent}/${venv_name}"
148
+ fi
149
+ marker="${venv_dir}/.code-mower-standalone-path"
150
+ installed_path=""
151
+ if [ -f "${marker}" ]; then
152
+ installed_path="$(cat "${marker}")"
153
+ fi
154
+ if [ ! -x "${venv_dir}/bin/code-mower" ] || [ "${installed_path}" != "${standalone_path}" ] || [ "${CODE_MOWER_STANDALONE_REINSTALL:-}" = "1" ]; then
155
+ acquire_install_lock
156
+ installed_path=""
157
+ if [ -f "${marker}" ]; then
158
+ installed_path="$(cat "${marker}")"
159
+ fi
160
+ if [ ! -x "${venv_dir}/bin/code-mower" ] || [ "${installed_path}" != "${standalone_path}" ] || [ "${CODE_MOWER_STANDALONE_REINSTALL:-}" = "1" ]; then
161
+ if [ -d "${venv_dir}" ] && [ "${CODE_MOWER_STANDALONE_REINSTALL:-}" = "1" ]; then
162
+ case "${venv_dir}" in
163
+ "${allowed_standalone_venv}"|"${allowed_standalone_venv}"/*|"${allowed_managed_venv_root}"/*) rm -rf "${venv_dir}" ;;
164
+ *)
165
+ if [ ! -x "${venv_dir}/bin/python" ]; then
166
+ echo "error: refusing to recreate unsafe custom standalone venv without an existing Python: ${venv_dir}" >&2
167
+ echo "Choose a venv under ${allowed_standalone_venv} or ${allowed_managed_venv_root}, or remove/recreate the custom venv yourself." >&2
168
+ exit 1
169
+ fi
170
+ ;;
171
+ esac
172
+ fi
173
+ if [ ! -x "${venv_dir}/bin/python" ]; then
174
+ check_standalone_python
175
+ "${bootstrap_python}" -m venv "${venv_dir}"
176
+ fi
177
+ "${venv_dir}/bin/python" -m pip install -e "${standalone_path}" 1>&2
178
+ printf '%s\n' "${standalone_path}" > "${marker}"
179
+ fi
180
+ release_install_lock
181
+ fi
182
+ release_install_lock
183
+ exec "${venv_dir}/bin/code-mower" "$@"
184
+ fi
185
+
186
+ if [ ! -f "${script_dir}/code_mower_bootstrap.py" ]; then
187
+ echo "error: repo-local Code Mower mirror is unavailable. Use the pinned standalone path via tools/code_mower_standalone_shadow.sh, or unset CODE_MOWER_USE_LOCAL/CODE_MOWER_USE_STANDALONE=0." >&2
188
+ exit 1
189
+ fi
190
+
191
+ case "${1:-}" in
192
+ bootstrap)
193
+ shift
194
+ exec "${bootstrap_python}" "${script_dir}/code_mower_bootstrap.py" "$@"
195
+ ;;
196
+ trailer-comment-labeler)
197
+ shift
198
+ python_bin="$("${bootstrap_python}" "${script_dir}/code_mower_bootstrap.py" --repo-root "${repo_root}" --print-python)"
199
+ exec "${python_bin}" "${script_dir}/trailer_comment_labeler.py" "$@"
200
+ ;;
201
+ saas-reviewer-labeler)
202
+ shift
203
+ python_bin="$("${bootstrap_python}" "${script_dir}/code_mower_bootstrap.py" --repo-root "${repo_root}" --print-python)"
204
+ exec "${python_bin}" "${script_dir}/saas_reviewer_labeler.py" "$@"
205
+ ;;
206
+ clear-stale)
207
+ if [ -f "${script_dir}/clear_stale.py" ]; then
208
+ shift
209
+ python_bin="$("${bootstrap_python}" "${script_dir}/code_mower_bootstrap.py" --repo-root "${repo_root}" --print-python)"
210
+ exec "${python_bin}" "${script_dir}/clear_stale.py" "$@"
211
+ fi
212
+ ;;
213
+ esac
214
+
215
+ python_bin="$("${bootstrap_python}" "${script_dir}/code_mower_bootstrap.py" --repo-root "${repo_root}" --print-python)"
216
+ exec "${python_bin}" "${script_dir}/code_mower_cli.py" "$@"
@@ -0,0 +1,7 @@
1
+ # Pinned standalone Code Mower source for product-repo wrapper mode.
2
+ #
3
+ # Replace OWNER/code-mower.git and the ref before copying into a live
4
+ # repository, or set CODE_MOWER_STANDALONE_REPO_URL / CODE_MOWER_STANDALONE_REF
5
+ # in the workflow environment.
6
+ CODE_MOWER_STANDALONE_REPO_URL="https://github.com/OWNER/code-mower.git"
7
+ CODE_MOWER_STANDALONE_REF="<pin-a-reviewed-code-mower-commit-or-tag>"
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
5
+ repo_root="$(CDPATH= cd -- "${script_dir}/.." && pwd -P)"
6
+ pin_file="${CODE_MOWER_STANDALONE_PIN_FILE:-${script_dir}/code_mower_standalone_pin.env}"
7
+ override_repo_url="${CODE_MOWER_STANDALONE_REPO_URL:-}"
8
+ override_ref="${CODE_MOWER_STANDALONE_REF:-}"
9
+ override_source_dir="${CODE_MOWER_STANDALONE_SOURCE_DIR:-}"
10
+ pin_file_missing=0
11
+
12
+ if [ -f "${pin_file}" ]; then
13
+ # shellcheck disable=SC1090
14
+ . "${pin_file}"
15
+ else
16
+ pin_file_missing=1
17
+ fi
18
+ if [ -n "${override_repo_url}" ]; then
19
+ CODE_MOWER_STANDALONE_REPO_URL="${override_repo_url}"
20
+ fi
21
+ if [ -n "${override_ref}" ]; then
22
+ CODE_MOWER_STANDALONE_REF="${override_ref}"
23
+ fi
24
+ if [ -n "${override_source_dir}" ]; then
25
+ CODE_MOWER_STANDALONE_SOURCE_DIR="${override_source_dir}"
26
+ fi
27
+
28
+ repo_url="${CODE_MOWER_STANDALONE_REPO_URL:-https://github.com/OWNER/code-mower.git}"
29
+ source_dir="${CODE_MOWER_STANDALONE_SOURCE_DIR:-}"
30
+ checkout_lock_acquired=0
31
+ checkout_lock_dir=""
32
+
33
+ release_checkout_lock() {
34
+ if [ "${checkout_lock_acquired}" = "1" ] && [ -n "${checkout_lock_dir}" ]; then
35
+ rm -rf "${checkout_lock_dir}"
36
+ fi
37
+ checkout_lock_acquired=0
38
+ checkout_lock_dir=""
39
+ }
40
+ trap release_checkout_lock EXIT
41
+
42
+ acquire_checkout_lock() {
43
+ local lock_age lock_mtime lock_pid now parent_dir stale_seconds started timeout_seconds
44
+ checkout_lock_dir="${source_dir}.checkout.lock"
45
+ parent_dir="$(dirname -- "${checkout_lock_dir}")"
46
+ stale_seconds="${CODE_MOWER_STANDALONE_CHECKOUT_LOCK_STALE_SECONDS:-30}"
47
+ timeout_seconds="${CODE_MOWER_STANDALONE_CHECKOUT_LOCK_TIMEOUT_SECONDS:-7200}"
48
+ started="$(date +%s)"
49
+ mkdir -p "${parent_dir}"
50
+ while ! mkdir "${checkout_lock_dir}" 2>/dev/null; do
51
+ now="$(date +%s)"
52
+ lock_pid=""
53
+ if [ -f "${checkout_lock_dir}/pid" ]; then
54
+ lock_pid="$(cat "${checkout_lock_dir}/pid" 2>/dev/null || true)"
55
+ fi
56
+ lock_mtime="$(stat -c %Y "${checkout_lock_dir}" 2>/dev/null || stat -f %m "${checkout_lock_dir}" 2>/dev/null || echo "${now}")"
57
+ case "${lock_mtime}" in
58
+ ''|*[!0-9]*) lock_mtime="${now}" ;;
59
+ esac
60
+ lock_age=$((now - lock_mtime))
61
+ if [ -z "${lock_pid}" ] && [ "${lock_age}" -ge "${stale_seconds}" ]; then
62
+ rm -rf "${checkout_lock_dir}" 2>/dev/null || true
63
+ continue
64
+ fi
65
+ if [ -n "${lock_pid}" ] && ! kill -0 "${lock_pid}" 2>/dev/null; then
66
+ rm -rf "${checkout_lock_dir}" 2>/dev/null || true
67
+ continue
68
+ fi
69
+ if [ $((now - started)) -ge "${timeout_seconds}" ]; then
70
+ echo "error: timed out waiting for standalone checkout lock: ${checkout_lock_dir}" >&2
71
+ exit 1
72
+ fi
73
+ sleep 0.2
74
+ done
75
+ checkout_lock_acquired=1
76
+ printf '%s\n' "$$" > "${checkout_lock_dir}/pid"
77
+ }
78
+
79
+ hash_ref_name() {
80
+ if command -v sha256sum >/dev/null 2>&1; then
81
+ printf '%s' "$1" | sha256sum | cut -d ' ' -f1
82
+ return
83
+ fi
84
+ if command -v shasum >/dev/null 2>&1; then
85
+ printf '%s' "$1" | shasum -a 256 | cut -d ' ' -f1
86
+ return
87
+ fi
88
+ if command -v openssl >/dev/null 2>&1; then
89
+ printf '%s' "$1" | openssl dgst -sha256 -r | cut -d ' ' -f1
90
+ return
91
+ fi
92
+ printf '%s' "$1" | cksum | cut -d ' ' -f1
93
+ }
94
+
95
+ if [ -n "${CODE_MOWER_STANDALONE_COMMAND:-}" ]; then
96
+ exec "${script_dir}/code_mower" "$@"
97
+ fi
98
+
99
+ if [ -z "${CODE_MOWER_STANDALONE_PATH:-}" ]; then
100
+ if [ -z "${CODE_MOWER_STANDALONE_REF:-}" ]; then
101
+ if [ "${pin_file_missing}" = "1" ]; then
102
+ echo "error: missing Code Mower standalone pin file: ${pin_file}" >&2
103
+ echo "Restore tools/code_mower_standalone_pin.env or set CODE_MOWER_STANDALONE_REF explicitly." >&2
104
+ else
105
+ echo "error: CODE_MOWER_STANDALONE_REF is required for standalone shadow mode." >&2
106
+ echo "Add CODE_MOWER_STANDALONE_REF to ${pin_file} or set it explicitly." >&2
107
+ fi
108
+ exit 1
109
+ fi
110
+ ref="${CODE_MOWER_STANDALONE_REF}"
111
+ if [ "${repo_url}" = "https://github.com/OWNER/code-mower.git" ] || [ "${ref}" = "<pin-a-reviewed-code-mower-commit-or-tag>" ]; then
112
+ echo "error: replace the placeholder Code Mower standalone repository URL and ref before using standalone shadow mode." >&2
113
+ echo "Update ${pin_file}, or set CODE_MOWER_STANDALONE_REPO_URL and CODE_MOWER_STANDALONE_REF explicitly." >&2
114
+ exit 1
115
+ fi
116
+ if [ -z "${source_dir}" ]; then
117
+ ref_slug="$(printf '%s' "${ref}" | tr -c 'A-Za-z0-9._-' '_' | cut -c1-48)"
118
+ ref_hash="$(hash_ref_name "${ref}" | cut -c1-12)"
119
+ source_dir="${repo_root}/.code-mower/standalone/code-mower-${ref_slug}-${ref_hash}"
120
+ if [ -z "${CODE_MOWER_STANDALONE_VENV:-}" ]; then
121
+ export CODE_MOWER_STANDALONE_VENV="${repo_root}/.code-mower/standalone-venvs/code-mower-${ref_slug}-${ref_hash}"
122
+ fi
123
+ fi
124
+ acquire_checkout_lock
125
+ if [ ! -d "${source_dir}/.git" ]; then
126
+ mkdir -p "$(dirname -- "${source_dir}")"
127
+ git clone --quiet "${repo_url}" "${source_dir}"
128
+ fi
129
+ repo_url_changed=""
130
+ existing_origin="$(git -C "${source_dir}" remote get-url origin 2>/dev/null || true)"
131
+ if [ "${existing_origin}" != "${repo_url}" ]; then
132
+ git -C "${source_dir}" remote set-url origin "${repo_url}"
133
+ repo_url_changed=1
134
+ fi
135
+ previous_head="$(git -C "${source_dir}" rev-parse --verify HEAD 2>/dev/null || true)"
136
+ if [ -n "${repo_url_changed}" ] || ! git -C "${source_dir}" cat-file -e "${ref}^{commit}" 2>/dev/null; then
137
+ git -C "${source_dir}" fetch --quiet --tags origin "${ref}" || git -C "${source_dir}" fetch --quiet --tags origin
138
+ fi
139
+ git -C "${source_dir}" checkout --quiet --detach "${ref}"
140
+ checked_out_head="$(git -C "${source_dir}" rev-parse --verify HEAD)"
141
+ if [ -n "${previous_head}" ] && [ "${previous_head}" != "${checked_out_head}" ] && [ -z "${CODE_MOWER_STANDALONE_REINSTALL:-}" ]; then
142
+ export CODE_MOWER_STANDALONE_REINSTALL=1
143
+ fi
144
+ export CODE_MOWER_STANDALONE_PATH="${source_dir}"
145
+ fi
146
+
147
+ set +e
148
+ "${script_dir}/code_mower" "$@"
149
+ rc=$?
150
+ set -e
151
+ exit "${rc}"
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
5
+ code_mower="${script_dir}/code_mower"
6
+ unset CODE_MOWER_USE_LOCAL
7
+ unset CODE_MOWER_USE_STANDALONE
8
+
9
+ token_from_stdin=""
10
+ filtered_args=()
11
+ for arg in "$@"; do
12
+ case "${arg}" in
13
+ --token-from-stdin|--read-token-from-stdin)
14
+ token_from_stdin=1
15
+ ;;
16
+ *)
17
+ filtered_args+=("${arg}")
18
+ ;;
19
+ esac
20
+ done
21
+
22
+ if [ -n "${token_from_stdin}" ]; then
23
+ exec env -u GITHUB_TOKEN -u GH_TOKEN "${code_mower}" claude-audit --read-token-from-stdin "${filtered_args[@]}"
24
+ fi
25
+
26
+ env_token="${GITHUB_TOKEN:-${GH_TOKEN:-}}"
27
+ if [ -n "${env_token}" ]; then
28
+ printf '%s\n' "${env_token}" | env -u GITHUB_TOKEN -u GH_TOKEN "${code_mower}" claude-audit --read-token-from-stdin "${filtered_args[@]}"
29
+ exit $?
30
+ fi
31
+
32
+ exec env -u GITHUB_TOKEN -u GH_TOKEN "${code_mower}" claude-audit "${filtered_args[@]}"
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ script_dir="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
5
+ code_mower="${script_dir}/code_mower"
6
+ unset CODE_MOWER_USE_LOCAL
7
+ unset CODE_MOWER_USE_STANDALONE
8
+
9
+ token_from_stdin=""
10
+ filtered_args=()
11
+ for arg in "$@"; do
12
+ case "${arg}" in
13
+ --token-from-stdin|--read-token-from-stdin)
14
+ token_from_stdin=1
15
+ ;;
16
+ *)
17
+ filtered_args+=("${arg}")
18
+ ;;
19
+ esac
20
+ done
21
+
22
+ if [ -n "${token_from_stdin}" ]; then
23
+ exec env -u GITHUB_TOKEN -u GH_TOKEN "${code_mower}" codex-audit --read-token-from-stdin "${filtered_args[@]}"
24
+ fi
25
+
26
+ env_token="${GITHUB_TOKEN:-${GH_TOKEN:-}}"
27
+ if [ -n "${env_token}" ]; then
28
+ printf '%s\n' "${env_token}" | env -u GITHUB_TOKEN -u GH_TOKEN "${code_mower}" codex-audit --read-token-from-stdin "${filtered_args[@]}"
29
+ exit $?
30
+ fi
31
+
32
+ exec env -u GITHUB_TOKEN -u GH_TOKEN "${code_mower}" codex-audit "${filtered_args[@]}"
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env python3
2
+ """Post or edit GitHub issue/PR comments without shell-interpreting Markdown.
3
+
4
+ Never pass Markdown through `gh ... --body "..."`: backticks and `$()` inside
5
+ the body are shell syntax before GitHub ever sees them. This helper reads the
6
+ body from a file or stdin, serializes it as JSON, and invokes `gh api` with
7
+ argument-list subprocess calls.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import subprocess
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence
17
+
18
+ if TYPE_CHECKING:
19
+ RunFn = Callable[..., subprocess.CompletedProcess[str]]
20
+ else:
21
+ # Keep the generated helper importable under older ambient Python 3.x
22
+ # interpreters that cannot evaluate subprocess.CompletedProcess[str].
23
+ RunFn = Callable[..., Any]
24
+
25
+
26
+ def _read_body(*, body_file: Optional[Path], stdin_text: Optional[str] = None) -> str:
27
+ if body_file is not None:
28
+ return body_file.read_text(encoding="utf-8")
29
+ if stdin_text is not None:
30
+ return stdin_text
31
+ return sys.stdin.read()
32
+
33
+
34
+ def _repo_from_gh(run: RunFn = subprocess.run) -> str:
35
+ result = run(
36
+ ["gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"],
37
+ check=True,
38
+ text=True,
39
+ capture_output=True,
40
+ )
41
+ return result.stdout.strip()
42
+
43
+
44
+ def _run_gh_api(args: Sequence[str], payload: "dict[str, str]", run: RunFn = subprocess.run) -> None:
45
+ run(
46
+ ["gh", "api", *args, "--input", "-"],
47
+ input=json.dumps(payload),
48
+ check=True,
49
+ text=True,
50
+ capture_output=True,
51
+ )
52
+
53
+
54
+ def post_comment(repo: str, issue_or_pr: int, body: str, run: RunFn = subprocess.run) -> None:
55
+ _run_gh_api(
56
+ ["-X", "POST", f"repos/{repo}/issues/{issue_or_pr}/comments"],
57
+ {"body": body},
58
+ run,
59
+ )
60
+
61
+
62
+ def edit_comment(repo: str, comment_id: int, body: str, run: RunFn = subprocess.run) -> None:
63
+ _run_gh_api(
64
+ ["-X", "PATCH", f"repos/{repo}/issues/comments/{comment_id}"],
65
+ {"body": body},
66
+ run,
67
+ )
68
+
69
+
70
+ def _parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
71
+ parser = argparse.ArgumentParser(description=__doc__)
72
+ target = parser.add_mutually_exclusive_group(required=True)
73
+ target.add_argument("--issue", "--pr", dest="issue_or_pr", type=int)
74
+ target.add_argument("--edit-comment-id", type=int)
75
+ parser.add_argument("--repo", help="owner/repo. Defaults to `gh repo view`.")
76
+ parser.add_argument("--body-file", type=Path, help="Markdown file to post/edit. Defaults to stdin.")
77
+ return parser.parse_args(argv)
78
+
79
+
80
+ def main(argv: Optional[Sequence[str]] = None) -> int:
81
+ args = _parse_args(argv)
82
+ try:
83
+ repo = args.repo or _repo_from_gh()
84
+ body = _read_body(body_file=args.body_file)
85
+ if args.edit_comment_id is not None:
86
+ edit_comment(repo, args.edit_comment_id, body)
87
+ else:
88
+ post_comment(repo, args.issue_or_pr, body)
89
+ except (OSError, subprocess.CalledProcessError) as exc:
90
+ print(f"error: {exc}", file=sys.stderr)
91
+ return 1
92
+ return 0
93
+
94
+
95
+ if __name__ == "__main__":
96
+ raise SystemExit(main())