code-mower 0.5.0b41__tar.gz → 0.5.0b43__tar.gz

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 (298) hide show
  1. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/PKG-INFO +6 -6
  2. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/README.md +5 -5
  3. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/pyproject.toml +1 -1
  4. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/__init__.py +1 -1
  5. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/audit_labeler_lib.py +129 -1
  6. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/claude_audit_pr.py +222 -4
  7. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/cli.py +1 -1
  8. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/cloud.py +7 -0
  9. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/cloud_client/operations.py +8 -0
  10. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/code_mower_telemetry.py +5 -1
  11. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/codex_audit_pr.py +28 -4
  12. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/init.py +91 -1
  13. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/lane_configs/claude.py +1 -1
  14. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/lane_configs/codex.py +1 -1
  15. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/package_manifest.py +2 -0
  16. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/provider_runners/__init__.py +6 -0
  17. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/provider_runners/pr_worktree.py +36 -1
  18. {code_mower-0.5.0b41 → code_mower-0.5.0b43/src/code_mower}/templates/workflows/builder-provenance.yml.j2 +1 -1
  19. code_mower-0.5.0b43/src/code_mower/templates/workflows/code-mower-gate-health.yml.j2 +321 -0
  20. {code_mower-0.5.0b41 → code_mower-0.5.0b43/src/code_mower}/templates/workflows/code-mower-gate.yml.j2 +36 -4
  21. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/workflows/self-hosted-local-audit.yml.j2 +77 -5
  22. {code_mower-0.5.0b41 → code_mower-0.5.0b43/src/code_mower}/templates/workflows/trailer-comment-labeler.yml.j2 +2 -0
  23. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/trailer_comment_labeler.py +39 -3
  24. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower.egg-info/PKG-INFO +6 -6
  25. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower.egg-info/SOURCES.txt +3 -0
  26. code_mower-0.5.0b43/templates/workflows/code-mower-gate-health.yml.j2 +321 -0
  27. {code_mower-0.5.0b41/src/code_mower → code_mower-0.5.0b43}/templates/workflows/code-mower-gate.yml.j2 +36 -4
  28. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/workflows/self-hosted-local-audit.yml.j2 +77 -5
  29. {code_mower-0.5.0b41/src/code_mower → code_mower-0.5.0b43}/templates/workflows/trailer-comment-labeler.yml.j2 +2 -0
  30. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_audit_comment_posture.py +15 -0
  31. code_mower-0.5.0b43/tests/test_claude_audit_pr.py +175 -0
  32. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_provider_runners_pr_worktree.py +58 -0
  33. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_release_hygiene.py +461 -38
  34. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_reviewer_metrics_core.py +47 -0
  35. code_mower-0.5.0b43/tests/test_trailer_comment_labeler.py +151 -0
  36. code_mower-0.5.0b41/tests/test_trailer_comment_labeler.py +0 -66
  37. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/CODE_OF_CONDUCT.md +0 -0
  38. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/LICENSE +0 -0
  39. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/MANIFEST.in +0 -0
  40. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/NOTICE +0 -0
  41. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/SUPPORT.md +0 -0
  42. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/requirements/requirements.txt +0 -0
  43. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/setup.cfg +0 -0
  44. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/adapters/__init__.py +0 -0
  45. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/adapters/_base.py +0 -0
  46. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/adapters/cursor_bugbot.py +0 -0
  47. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/adapters/gitar.py +0 -0
  48. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/adapters/greptile.py +0 -0
  49. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/adapters/qodo.py +0 -0
  50. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/antigravity_cli_audit_pr.py +0 -0
  51. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/audit_handoff_log.py +0 -0
  52. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/audit_progress.py +0 -0
  53. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/blind_review_artifacts.py +0 -0
  54. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/blind_review_coordinator.py +0 -0
  55. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/bootstrap.py +0 -0
  56. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/builder_experiment.py +0 -0
  57. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/builder_runs.py +0 -0
  58. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/calibration/__init__.py +0 -0
  59. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/calibration/arms.py +0 -0
  60. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/calibration/auto_discovery.py +0 -0
  61. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/calibration/commands.py +0 -0
  62. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/calibration/context_inputs.py +0 -0
  63. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/calibration/corpus.py +0 -0
  64. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/calibration/effect_report.py +0 -0
  65. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/calibration/evidence.py +0 -0
  66. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/calibration/evidence_report.py +0 -0
  67. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/calibration/identity.py +0 -0
  68. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/calibration/metrics.py +0 -0
  69. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/calibration/overlap.py +0 -0
  70. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/calibration/planning.py +0 -0
  71. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/calibration/policy.py +0 -0
  72. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/calibration/results.py +0 -0
  73. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/calibration/run_results.py +0 -0
  74. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/calibration/run_status.py +0 -0
  75. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/calibration/runner.py +0 -0
  76. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/calibration/truth.py +0 -0
  77. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/calibration/value_report.py +0 -0
  78. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/checks.py +0 -0
  79. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/claude_cli_bounce.py +0 -0
  80. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/claude_cli_environment.py +0 -0
  81. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/clear_stale.py +0 -0
  82. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/cloud_client/__init__.py +0 -0
  83. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/cloud_client/bundle.py +0 -0
  84. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/cloud_client/doctor.py +0 -0
  85. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/cloud_client/dogfood.py +0 -0
  86. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/cloud_client/endpoints.py +0 -0
  87. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/cloud_client/errors.py +0 -0
  88. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/cloud_client/events.py +0 -0
  89. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/cloud_client/export.py +0 -0
  90. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/cloud_client/git_metadata.py +0 -0
  91. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/cloud_client/manifest.py +0 -0
  92. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/cloud_client/reports.py +0 -0
  93. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/cloud_client/setup.py +0 -0
  94. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/cloud_client/upload.py +0 -0
  95. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/code_mower_calibration.py +0 -0
  96. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/code_mower_context_packs.py +0 -0
  97. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/code_mower_merge.py +0 -0
  98. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/coderabbit_cli_audit_pr.py +0 -0
  99. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/codex_audit_env_preflight.py +0 -0
  100. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/codex_audit_schema_smoke.py +0 -0
  101. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/codex_audit_verdict.schema.json +0 -0
  102. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/config.py +0 -0
  103. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor.py +0 -0
  104. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/__init__.py +0 -0
  105. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/cloud.py +0 -0
  106. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/common.py +0 -0
  107. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/github.py +0 -0
  108. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/github_actions.py +0 -0
  109. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/github_actions_cost.py +0 -0
  110. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/github_actions_cost_summary.py +0 -0
  111. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/github_actions_failure_annotations.py +0 -0
  112. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/github_actions_failure_models.py +0 -0
  113. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/github_actions_failure_scan.py +0 -0
  114. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/github_actions_failure_selection.py +0 -0
  115. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/github_actions_failures.py +0 -0
  116. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/github_actions_permissions.py +0 -0
  117. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/github_api.py +0 -0
  118. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/github_branch.py +0 -0
  119. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/github_config.py +0 -0
  120. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/github_provider.py +0 -0
  121. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/github_repo.py +0 -0
  122. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/groups.py +0 -0
  123. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/models.py +0 -0
  124. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/output.py +0 -0
  125. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/presets.py +0 -0
  126. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/privacy.py +0 -0
  127. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/provider_api_model.py +0 -0
  128. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/provider_api_model_openai.py +0 -0
  129. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/provider_api_model_profiles.py +0 -0
  130. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/provider_env.py +0 -0
  131. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/provider_env_required.py +0 -0
  132. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/provider_env_tokens.py +0 -0
  133. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/provider_local_cli.py +0 -0
  134. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/provider_local_cli_commands.py +0 -0
  135. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/provider_local_cli_probe_config.py +0 -0
  136. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/provider_probe.py +0 -0
  137. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/provider_probe_auth.py +0 -0
  138. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/provider_probe_evaluation.py +0 -0
  139. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/provider_probe_json.py +0 -0
  140. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/provider_probe_remediation.py +0 -0
  141. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/provider_review_hygiene.py +0 -0
  142. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/providers.py +0 -0
  143. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/registry.py +0 -0
  144. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/runner.py +0 -0
  145. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/runtime.py +0 -0
  146. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/doctor_checks/runtime_github_auth.py +0 -0
  147. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/gemini_cli_audit_pr.py +0 -0
  148. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/grok_build_audit_pr.py +0 -0
  149. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/hermes_cli_audit_pr.py +0 -0
  150. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/lane_configs/__init__.py +0 -0
  151. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/lane_configs/aider.py +0 -0
  152. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/lane_configs/antigravity_cli.py +0 -0
  153. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/lane_configs/devin.py +0 -0
  154. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/lane_configs/gemini_cli.py +0 -0
  155. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/lane_configs/grok_build.py +0 -0
  156. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/lane_configs/hermes_cli.py +0 -0
  157. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/lane_configs/local_llm.py +0 -0
  158. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/local_llm_audit_pr.py +0 -0
  159. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/local_llm_bakeoff.py +0 -0
  160. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/local_llm_calibration.py +0 -0
  161. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/local_llm_profiles.py +0 -0
  162. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/migration.py +0 -0
  163. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/migration_install.py +0 -0
  164. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/migration_mirror.py +0 -0
  165. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/migration_readiness.py +0 -0
  166. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/migration_rehearsal.py +0 -0
  167. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/next_steps.py +0 -0
  168. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/package.py +0 -0
  169. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/package_content.py +0 -0
  170. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/package_paths.py +0 -0
  171. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/package_rendering.py +0 -0
  172. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/package_static.py +0 -0
  173. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/plan_context.py +0 -0
  174. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/prompts.py +0 -0
  175. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/provider_registry.py +0 -0
  176. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/provider_runners/comments.py +0 -0
  177. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/provider_runners/git.py +0 -0
  178. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/provider_runners/github_auth.py +0 -0
  179. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/provider_runners/github_pr.py +0 -0
  180. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/provider_runners/process.py +0 -0
  181. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/provider_runners/repo_paths.py +0 -0
  182. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/provider_runners/text_schema.py +0 -0
  183. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/provider_runners/verdict_artifacts.py +0 -0
  184. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/provider_runners/workspace.py +0 -0
  185. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/providers/__init__.py +0 -0
  186. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/providers/local_cli.py +0 -0
  187. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/providers/provenance.py +0 -0
  188. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/release_readiness.py +0 -0
  189. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/reviewer_metrics.py +0 -0
  190. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/reviewer_spend.py +0 -0
  191. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/saas_reviewer_labeler.py +0 -0
  192. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/secrets.py +0 -0
  193. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/builder-experiment.example.json +0 -0
  194. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/calibration-corpus.example.json +0 -0
  195. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/calibration-corpus.json +0 -0
  196. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/code-mower.example.yml +0 -0
  197. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/context-packs.example.json +0 -0
  198. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/lane_prompts/base-audit.md +0 -0
  199. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/lane_prompts/calibration-policy.md +0 -0
  200. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/lane_prompts/context-driven-quality.md +0 -0
  201. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/lane_prompts/docs-design.md +0 -0
  202. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/lane_prompts/generic-programming.md +0 -0
  203. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/lane_prompts/operability.md +0 -0
  204. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/lane_prompts/package-runtime.md +0 -0
  205. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/lane_prompts/plan-conformance.md +0 -0
  206. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/lane_prompts/security-threat-model.md +0 -0
  207. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/product-support/code_mower +0 -0
  208. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/product-support/code_mower_standalone_pin.env +0 -0
  209. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/product-support/code_mower_standalone_shadow.sh +0 -0
  210. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/product-support/run_claude_audit_pr.sh +0 -0
  211. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/product-support/run_codex_audit_pr.sh +0 -0
  212. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/product-support/safe_gh_comment.py +0 -0
  213. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/product-support/status_report.py +0 -0
  214. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/providers.yml +0 -0
  215. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/reviewer-spend.example.json +0 -0
  216. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/reviewer-value-report.example.md +0 -0
  217. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/workflows/audit-label-cleanup.yml.j2 +0 -0
  218. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/workflows/hosted-bridge.yml.j2 +0 -0
  219. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/workflows/local-cli-audit.yml.j2 +0 -0
  220. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/workflows/needs-owner-notify.yml.j2 +0 -0
  221. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/workflows/private-standalone-shadow.yml.j2 +0 -0
  222. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/workflows/review-clear-stale.yml.j2 +0 -0
  223. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/workflows/saas-reviewer-labeler.yml.j2 +0 -0
  224. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/templates/workflows/weekly-status.yml.j2 +0 -0
  225. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/versioning.py +0 -0
  226. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower/work_orders.py +0 -0
  227. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower.egg-info/dependency_links.txt +0 -0
  228. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower.egg-info/entry_points.txt +0 -0
  229. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower.egg-info/requires.txt +0 -0
  230. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/src/code_mower.egg-info/top_level.txt +0 -0
  231. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/builder-experiment.example.json +0 -0
  232. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/calibration-corpus.example.json +0 -0
  233. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/calibration-corpus.json +0 -0
  234. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/code-mower.yml.j2 +0 -0
  235. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/context-packs.example.json +0 -0
  236. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/cursor/BUGBOT.md +0 -0
  237. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/providers/acp_bridge.yml +0 -0
  238. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/providers/aider.yml +0 -0
  239. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/providers/antigravity_cli.yml +0 -0
  240. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/providers/claude_audit.yml +0 -0
  241. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/providers/claude_review.yml +0 -0
  242. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/providers/coderabbit_cli.yml +0 -0
  243. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/providers/codex.yml +0 -0
  244. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/providers/cursor_bugbot.yml +0 -0
  245. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/providers/devin.yml +0 -0
  246. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/providers/gemini_cli.yml +0 -0
  247. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/providers/gitar.yml +0 -0
  248. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/providers/greptile.yml +0 -0
  249. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/providers/hermes_cli.yml +0 -0
  250. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/providers/local_llm.yml +0 -0
  251. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/providers/qodo.yml +0 -0
  252. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/providers.yml +0 -0
  253. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/reviewer-spend.example.json +0 -0
  254. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/reviewer-value-report.example.md +0 -0
  255. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/workflows/audit-label-cleanup.yml.j2 +0 -0
  256. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/workflows/blind-review-artifacts-dry-run.yml.j2 +0 -0
  257. {code_mower-0.5.0b41/src/code_mower → code_mower-0.5.0b43}/templates/workflows/builder-provenance.yml.j2 +0 -0
  258. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/workflows/hosted-bridge.yml.j2 +0 -0
  259. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/workflows/local-cli-audit.yml.j2 +0 -0
  260. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/workflows/private-standalone-shadow.yml.j2 +0 -0
  261. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/workflows/review-clear-stale.yml.j2 +0 -0
  262. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/templates/workflows/saas-reviewer-labeler.yml.j2 +0 -0
  263. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_builder_identity.py +0 -0
  264. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_builder_runs.py +0 -0
  265. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_calibration_effect_report.py +0 -0
  266. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_calibration_truth.py +0 -0
  267. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_checks.py +0 -0
  268. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_claude_cli_bounce.py +0 -0
  269. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_clear_stale.py +0 -0
  270. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_cloud_client.py +0 -0
  271. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_cloud_git_metadata.py +0 -0
  272. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_cloud_manifest.py +0 -0
  273. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_cloud_reports.py +0 -0
  274. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_coderabbit_cli_audit_pr.py +0 -0
  275. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_codex_audit_env_preflight.py +0 -0
  276. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_doctor_actions_cost_summary.py +0 -0
  277. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_doctor_actions_failure_selection.py +0 -0
  278. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_doctor_boundaries.py +0 -0
  279. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_doctor_github_checks.py +0 -0
  280. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_doctor_output.py +0 -0
  281. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_doctor_privacy.py +0 -0
  282. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_doctor_provider_env_required.py +0 -0
  283. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_doctor_provider_env_tokens.py +0 -0
  284. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_doctor_provider_probe.py +0 -0
  285. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_doctor_provider_review_hygiene.py +0 -0
  286. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_doctor_registry.py +0 -0
  287. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_local_llm_github_helpers.py +0 -0
  288. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_plan_context.py +0 -0
  289. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_provider_local_cli.py +0 -0
  290. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_provider_runners_comments.py +0 -0
  291. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_provider_runners_git.py +0 -0
  292. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_provider_runners_github_pr.py +0 -0
  293. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_provider_runners_process.py +0 -0
  294. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_provider_runners_repo_paths.py +0 -0
  295. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_provider_runners_text_schema.py +0 -0
  296. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_provider_runners_workspace.py +0 -0
  297. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_reviewer_spend.py +0 -0
  298. {code_mower-0.5.0b41 → code_mower-0.5.0b43}/tests/test_work_orders.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-mower
3
- Version: 0.5.0b41
3
+ Version: 0.5.0b43
4
4
  Summary: Multi-reviewer AI code audit orchestration
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.11
@@ -103,11 +103,11 @@ Code Mower currently targets Python 3.11+; Python 3.12 is recommended.
103
103
 
104
104
  ```bash
105
105
  python3.12 --version
106
- pipx install --python python3.12 code-mower==0.5.0b41
106
+ pipx install --python python3.12 code-mower==0.5.0b43
107
107
  code-mower --version
108
108
  ```
109
109
 
110
- `0.5.0b41` is a beta release. Until Code Mower publishes a stable `1.0`
110
+ `0.5.0b43` is a beta release. Until Code Mower publishes a stable `1.0`
111
111
  line, use the explicit beta version above or allow prereleases with:
112
112
 
113
113
  ```bash
@@ -155,7 +155,7 @@ First-time command map: [docs/launch-command-surface.md](docs/launch-command-sur
155
155
 
156
156
  The current PyPI beta has been rehearsed end-to-end from a clean install:
157
157
  [First-User Install Rehearsal](docs/first-user-install-rehearsal.md) records
158
- the latest 10/10 public-package readiness proof for `code-mower==0.5.0b41`.
158
+ the latest 10/10 public-package readiness proof for `code-mower==0.5.0b43`.
159
159
 
160
160
  ## Optional: Plan Before Coding
161
161
 
@@ -310,9 +310,9 @@ builder provenance, start with
310
310
 
311
311
  ## Installation Status
312
312
 
313
- The current public beta is `v0.5.0-beta.41` from
313
+ The current public beta is `v0.5.0-beta.43` from
314
314
  [codemower-ai/code-mower](https://github.com/codemower-ai/code-mower), published
315
- as `code-mower==0.5.0b41` on [PyPI](https://pypi.org/project/code-mower/).
315
+ as `code-mower==0.5.0b43` on [PyPI](https://pypi.org/project/code-mower/).
316
316
  GitHub releases remain the auditable source for tags, build artifacts, and
317
317
  release notes.
318
318
 
@@ -88,11 +88,11 @@ Code Mower currently targets Python 3.11+; Python 3.12 is recommended.
88
88
 
89
89
  ```bash
90
90
  python3.12 --version
91
- pipx install --python python3.12 code-mower==0.5.0b41
91
+ pipx install --python python3.12 code-mower==0.5.0b43
92
92
  code-mower --version
93
93
  ```
94
94
 
95
- `0.5.0b41` is a beta release. Until Code Mower publishes a stable `1.0`
95
+ `0.5.0b43` is a beta release. Until Code Mower publishes a stable `1.0`
96
96
  line, use the explicit beta version above or allow prereleases with:
97
97
 
98
98
  ```bash
@@ -140,7 +140,7 @@ First-time command map: [docs/launch-command-surface.md](docs/launch-command-sur
140
140
 
141
141
  The current PyPI beta has been rehearsed end-to-end from a clean install:
142
142
  [First-User Install Rehearsal](docs/first-user-install-rehearsal.md) records
143
- the latest 10/10 public-package readiness proof for `code-mower==0.5.0b41`.
143
+ the latest 10/10 public-package readiness proof for `code-mower==0.5.0b43`.
144
144
 
145
145
  ## Optional: Plan Before Coding
146
146
 
@@ -295,9 +295,9 @@ builder provenance, start with
295
295
 
296
296
  ## Installation Status
297
297
 
298
- The current public beta is `v0.5.0-beta.41` from
298
+ The current public beta is `v0.5.0-beta.43` from
299
299
  [codemower-ai/code-mower](https://github.com/codemower-ai/code-mower), published
300
- as `code-mower==0.5.0b41` on [PyPI](https://pypi.org/project/code-mower/).
300
+ as `code-mower==0.5.0b43` on [PyPI](https://pypi.org/project/code-mower/).
301
301
  GitHub releases remain the auditable source for tags, build artifacts, and
302
302
  release notes.
303
303
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code-mower"
7
- version = "0.5.0b41"
7
+ version = "0.5.0b43"
8
8
  description = "Multi-reviewer AI code audit orchestration"
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
@@ -1,3 +1,3 @@
1
1
  """Code Mower package."""
2
2
 
3
- __version__ = "0.5.0b41"
3
+ __version__ = "0.5.0b43"
@@ -17,12 +17,16 @@ import urllib.error
17
17
  import urllib.request
18
18
  from dataclasses import dataclass
19
19
  from pathlib import Path
20
- from typing import Any, Dict, Mapping, Optional, Pattern, Sequence
20
+ from typing import Any, Callable, Dict, Mapping, Optional, Pattern, Sequence
21
21
  from urllib.parse import quote
22
22
  import re
23
23
 
24
24
  MIN_ABBREVIATED_SHA_LENGTH = 7
25
25
  AUTHOR_EXCLUSION_ENV = "CODE_MOWER_AUTHOR_EXCLUSION_JSON"
26
+ ACTIONS_RUN_MARKER_RE = re.compile(
27
+ r"<!--\s*CODE_MOWER_AUDIT_RUN:\s*run_id=([0-9]+)\s*-->"
28
+ )
29
+ TRUSTED_ACTIONS_AUDIT_EVENTS = frozenset({"pull_request_target"})
26
30
 
27
31
 
28
32
  @dataclass(frozen=True)
@@ -291,6 +295,130 @@ def github_request_with_fallback(
291
295
  raise RuntimeError(f"no GitHub tokens available for {method} {path}")
292
296
 
293
297
 
298
+ def parse_csv_set(raw: str) -> frozenset[str]:
299
+ return frozenset(item.strip() for item in raw.split(",") if item.strip())
300
+
301
+
302
+ def parse_audit_run_id(body: str) -> str:
303
+ match = ACTIONS_RUN_MARKER_RE.search(body)
304
+ return match.group(1) if match else ""
305
+
306
+
307
+ def workflow_path_matches(path: str, trusted_workflows: Sequence[str]) -> bool:
308
+ if not path:
309
+ return False
310
+ return any(path == item or path.endswith("/" + item) for item in trusted_workflows)
311
+
312
+
313
+ def _pr_item_matches_head(
314
+ item: Mapping[str, Any],
315
+ *,
316
+ issue_number: int,
317
+ head_sha: str,
318
+ fallback_head_sha: str = "",
319
+ ) -> bool:
320
+ if str(item.get("number") or "") != str(issue_number):
321
+ return False
322
+ pr_head = str(((item.get("head") or {}).get("sha")) or "")
323
+ if pr_head:
324
+ return sha_matches(pr_head, head_sha)
325
+ return bool(fallback_head_sha) and sha_matches(fallback_head_sha, head_sha)
326
+
327
+
328
+ def fetch_actions_run(
329
+ repo: str,
330
+ run_id: str,
331
+ *,
332
+ tokens: Sequence[GitHubToken],
333
+ ) -> dict[str, Any]:
334
+ response = github_request_with_fallback(
335
+ "GET",
336
+ f"/repos/{repo}/actions/runs/{run_id}",
337
+ tokens=tokens,
338
+ )
339
+ return response if isinstance(response, dict) else {}
340
+
341
+
342
+ def fetch_pull_requests_for_commit(
343
+ repo: str,
344
+ head_sha: str,
345
+ *,
346
+ tokens: Sequence[GitHubToken],
347
+ ) -> list[dict[str, Any]]:
348
+ response = github_request_with_fallback(
349
+ "GET",
350
+ f"/repos/{repo}/commits/{head_sha}/pulls?per_page=100",
351
+ tokens=tokens,
352
+ )
353
+ return response if isinstance(response, list) else []
354
+
355
+
356
+ def github_actions_comment_attested(
357
+ *,
358
+ repo: str,
359
+ body: str,
360
+ issue_number: int,
361
+ head_sha: str,
362
+ workflow_paths: Sequence[str],
363
+ tokens: Sequence[GitHubToken],
364
+ actions_run_lookup: Optional[Callable[[str], Mapping[str, Any]]] = None,
365
+ commit_pull_requests_lookup: Optional[Callable[[str], Sequence[Mapping[str, Any]]]] = None,
366
+ ) -> bool:
367
+ """Return whether a github-actions[bot] audit comment is tied to this PR head."""
368
+ trusted_workflows = tuple(parse_csv_set(",".join(workflow_paths)))
369
+ token_list = tuple(token for token in tokens if token.value)
370
+ run_id = parse_audit_run_id(body)
371
+ if not trusted_workflows or not token_list or not run_id or not head_sha:
372
+ return False
373
+ try:
374
+ if actions_run_lookup is None:
375
+ run = fetch_actions_run(repo, run_id, tokens=token_list)
376
+ else:
377
+ run = dict(actions_run_lookup(run_id))
378
+ except Exception:
379
+ return False
380
+
381
+ path = str(run.get("path") or "")
382
+ if not workflow_path_matches(path, trusted_workflows):
383
+ return False
384
+ if str(run.get("event") or "") not in TRUSTED_ACTIONS_AUDIT_EVENTS:
385
+ return False
386
+
387
+ run_head_sha = str(run.get("head_sha") or "")
388
+ pull_requests = run.get("pull_requests")
389
+ if isinstance(pull_requests, list) and pull_requests:
390
+ return any(
391
+ isinstance(item, Mapping)
392
+ and _pr_item_matches_head(
393
+ item,
394
+ issue_number=issue_number,
395
+ head_sha=head_sha,
396
+ fallback_head_sha=run_head_sha,
397
+ )
398
+ for item in pull_requests
399
+ )
400
+ if not run_head_sha or not sha_matches(run_head_sha, head_sha):
401
+ return False
402
+
403
+ try:
404
+ if commit_pull_requests_lookup is None:
405
+ associated_prs = fetch_pull_requests_for_commit(repo, run_head_sha, tokens=token_list)
406
+ else:
407
+ associated_prs = commit_pull_requests_lookup(run_head_sha)
408
+ except Exception:
409
+ return False
410
+ return any(
411
+ isinstance(item, Mapping)
412
+ and _pr_item_matches_head(
413
+ item,
414
+ issue_number=issue_number,
415
+ head_sha=head_sha,
416
+ fallback_head_sha=run_head_sha,
417
+ )
418
+ for item in associated_prs
419
+ )
420
+
421
+
294
422
  def fetch_pull_request(
295
423
  repo: str,
296
424
  issue_number: int,
@@ -36,6 +36,7 @@ if __package__ in {None, "", "tools"}:
36
36
  fetch_pull_request,
37
37
  fetch_base_ref_sha as _shared_fetch_base_ref_sha,
38
38
  fetch_pr_head_sha as _shared_fetch_pr_head_sha,
39
+ fetch_pr_head_sha_unless_local_matches as _shared_fetch_pr_head_sha_unless_local_matches,
39
40
  limit_comment_body,
40
41
  one_line as _one_line,
41
42
  parse_repo_paths as _parse_repo_paths,
@@ -61,6 +62,7 @@ if __package__ in {None, "", "tools"}:
61
62
  fetch_pull_request,
62
63
  fetch_base_ref_sha as _shared_fetch_base_ref_sha,
63
64
  fetch_pr_head_sha as _shared_fetch_pr_head_sha,
65
+ fetch_pr_head_sha_unless_local_matches as _shared_fetch_pr_head_sha_unless_local_matches,
64
66
  limit_comment_body,
65
67
  one_line as _one_line,
66
68
  parse_repo_paths as _parse_repo_paths,
@@ -84,6 +86,7 @@ else: # pragma: no cover - exercised after package extraction.
84
86
  fetch_base_ref_sha as _shared_fetch_base_ref_sha,
85
87
  fetch_pull_request,
86
88
  fetch_pr_head_sha as _shared_fetch_pr_head_sha,
89
+ fetch_pr_head_sha_unless_local_matches as _shared_fetch_pr_head_sha_unless_local_matches,
87
90
  limit_comment_body,
88
91
  one_line as _one_line,
89
92
  parse_repo_paths as _parse_repo_paths,
@@ -109,10 +112,16 @@ MAX_SUMMARY_CHARS = 4_000
109
112
  MAX_FINDING_TITLE_CHARS = 300
110
113
  MAX_FINDING_FILE_CHARS = 500
111
114
  MAX_FINDING_DETAIL_CHARS = 4_000
115
+ MAX_CLAUDE_AUDIT_ATTEMPTS = 2
116
+ MIN_BLOCKED_VERDICT_PROSE_CHARS = 80
117
+ SCHEMA_PLACEHOLDER_TEXT = frozenset({"test", "example", "placeholder"})
118
+ CLAUDE_RAW_OUTPUT_SCHEMA = "code_mower.claudeAuditRawOutput.v1"
119
+ BLOCKER_SEVERITIES = frozenset({"P0", "P1", "P2"})
112
120
 
113
121
  STALE_TRAILER = "<!-- CLAUDE_AUDIT_STATE: needs-claude-audit -->"
114
122
  DONE_TRAILER = "<!-- CLAUDE_AUDIT_STATE: claude-audit-done -->"
115
123
  BLOCKED_TRAILER = "<!-- CLAUDE_AUDIT_STATE: claude-audit-blocked -->"
124
+ AUDIT_RUN_TRAILER_PREFIX = "<!-- CODE_MOWER_AUDIT_RUN:"
116
125
 
117
126
 
118
127
  CLAUDE_VERDICT_SCHEMA: Dict[str, Any] = {
@@ -177,6 +186,8 @@ class ClaudeVerdict:
177
186
  p2_count: int = 0
178
187
  p3_count: int = 0
179
188
  mismatch_note: str = ""
189
+ summary: str = ""
190
+ findings: Tuple[Dict[str, Any], ...] = field(default_factory=tuple)
180
191
 
181
192
  @property
182
193
  def blocker_count(self) -> int:
@@ -203,6 +214,7 @@ class ClaudeAuditResult:
203
214
  class DiffContext:
204
215
  stat: str
205
216
  diff: str
217
+ changed_files: Tuple[str, ...]
206
218
  was_truncated: bool
207
219
  requested_max_bytes: int
208
220
  hard_limit_bytes: int
@@ -333,7 +345,7 @@ def parse_structured_claude_verdict(data: Any) -> ClaudeVerdict:
333
345
 
334
346
  p_counts[int(severity[1])] += 1
335
347
  if len(rendered_findings) < MAX_RENDERED_FINDINGS:
336
- rendered_findings.append(raw_finding)
348
+ rendered_findings.append(dict(raw_finding))
337
349
 
338
350
  blocker_count = p_counts[0] + p_counts[1] + p_counts[2]
339
351
  if blocker_count > 0:
@@ -358,9 +370,139 @@ def parse_structured_claude_verdict(data: Any) -> ClaudeVerdict:
358
370
  p2_count=p_counts[2],
359
371
  p3_count=p_counts[3],
360
372
  mismatch_note=mismatch_note,
373
+ summary=summary,
374
+ findings=tuple(dict(finding) for finding in raw_findings),
361
375
  )
362
376
 
363
377
 
378
+ def _placeholder_equal(value: object) -> bool:
379
+ return str(value or "").strip().lower() in SCHEMA_PLACEHOLDER_TEXT
380
+
381
+
382
+ def _normalize_diff_path(value: object) -> str:
383
+ path = str(value or "").strip().replace("\\", "/")
384
+ while path.startswith("./"):
385
+ path = path[2:]
386
+ if path.startswith(("a/", "b/")):
387
+ path = path[2:]
388
+ return path
389
+
390
+
391
+ def _diff_file_matches(file_path: str, allowed_files: set[str]) -> bool:
392
+ if file_path in allowed_files:
393
+ return True
394
+ suffix_matches = [
395
+ path
396
+ for path in allowed_files
397
+ if path.endswith(f"/{file_path}") or file_path.endswith(f"/{path}")
398
+ ]
399
+ return len(suffix_matches) == 1
400
+
401
+
402
+ def _claude_verdict_guardrail_reason(
403
+ parsed: ClaudeVerdict,
404
+ changed_files: Tuple[str, ...],
405
+ ) -> Optional[str]:
406
+ if parsed.verdict == "UNKNOWN":
407
+ return None
408
+
409
+ if _placeholder_equal(parsed.summary):
410
+ return "structured verdict summary matched a schema placeholder value"
411
+
412
+ allowed_files = {_normalize_diff_path(path) for path in changed_files}
413
+ allowed_files.discard("")
414
+ for idx, finding in enumerate(parsed.findings):
415
+ where = f"findings[{idx}]"
416
+ if _placeholder_equal(finding.get("title")):
417
+ return f"{where}.title matched a schema placeholder value"
418
+ if _placeholder_equal(finding.get("detail")):
419
+ return f"{where}.detail matched a schema placeholder value"
420
+
421
+ file_path = _normalize_diff_path(finding.get("file"))
422
+ severity = str(finding.get("severity") or "")
423
+ if (
424
+ severity in BLOCKER_SEVERITIES
425
+ and (not file_path or not _diff_file_matches(file_path, allowed_files))
426
+ ):
427
+ return f"{where}.file {file_path!r} is not present in the PR diff"
428
+
429
+ if (
430
+ parsed.verdict == "BLOCKED"
431
+ and len(parsed.prose.strip()) < MIN_BLOCKED_VERDICT_PROSE_CHARS
432
+ ):
433
+ return "structured blocked verdict body is too short to be credible"
434
+
435
+ return None
436
+
437
+
438
+ def _apply_claude_verdict_guardrails(
439
+ parsed: ClaudeVerdict,
440
+ changed_files: Tuple[str, ...],
441
+ ) -> Tuple[ClaudeVerdict, Optional[str]]:
442
+ reason = _claude_verdict_guardrail_reason(parsed, changed_files)
443
+ if reason is None:
444
+ return parsed, None
445
+ return _unknown_structured_verdict(reason), reason
446
+
447
+
448
+ def _write_claude_raw_output_sidecar(
449
+ artifact_path: Optional[Path],
450
+ attempts: List[Dict[str, Any]],
451
+ ) -> None:
452
+ if artifact_path is None:
453
+ return
454
+ sidecar_path = artifact_path.with_suffix(".claude-raw-output.json")
455
+ payload = {"schema": CLAUDE_RAW_OUTPUT_SCHEMA, "attempts": attempts}
456
+ try:
457
+ text = json.dumps(payload, indent=2, sort_keys=True) + "\n"
458
+ sidecar_path.write_text(text, encoding="utf-8")
459
+ except OSError as exc:
460
+ print(
461
+ f"warning: failed to write Claude raw output sidecar {sidecar_path}: {exc}",
462
+ file=sys.stderr,
463
+ )
464
+
465
+
466
+ def _guardrail_retry_prompt(
467
+ prompt: str,
468
+ reason: str,
469
+ changed_files: Tuple[str, ...],
470
+ ) -> str:
471
+ files = "\n".join(f"- {path}" for path in changed_files[:50])
472
+ if len(changed_files) > 50:
473
+ files += f"\n- ... {len(changed_files) - 50} additional file(s) omitted"
474
+ return (
475
+ f"{prompt.rstrip()}\n\n"
476
+ "Trusted wrapper retry instruction:\n"
477
+ f"The previous structured verdict was rejected: {reason}. "
478
+ "Return a fresh JSON verdict only. Do not use placeholder values such "
479
+ "as 'test', 'example', or 'placeholder'. P0/P1/P2 findings must cite a "
480
+ "file from the PR diff, using the repository-relative path or a unique "
481
+ "suffix. PR diff files:\n"
482
+ f"{files}\n"
483
+ )
484
+
485
+
486
+ def _record_posted_comment_url(
487
+ artifact_path: Optional[Path],
488
+ posted_comment_url: Optional[str],
489
+ ) -> None:
490
+ if artifact_path is None or not posted_comment_url:
491
+ return
492
+ try:
493
+ payload = json.loads(artifact_path.read_text(encoding="utf-8"))
494
+ if not isinstance(payload, dict):
495
+ return
496
+ payload["posted_comment_url"] = posted_comment_url
497
+ text = json.dumps(payload, indent=2, sort_keys=True) + "\n"
498
+ artifact_path.write_text(text, encoding="utf-8")
499
+ except (OSError, json.JSONDecodeError) as exc:
500
+ print(
501
+ f"warning: failed to update audit verdict artifact {artifact_path}: {exc}",
502
+ file=sys.stderr,
503
+ )
504
+
505
+
364
506
  def _claude_env() -> Dict[str, str]:
365
507
  preserve_auth_overrides = env_flag(os.environ.get("CODE_MOWER_CLAUDE_KEEP_AUTH_ENV"))
366
508
  env, _removed = clean_claude_cli_env(
@@ -387,7 +529,17 @@ def _fetch_base_sha_for_diff(local_repo: Path, base_ref: str) -> str:
387
529
  return _shared_fetch_base_ref_sha(local_repo, base_ref)
388
530
 
389
531
 
390
- def _fetch_pr_head_sha_for_diff(local_repo: Path, pr_number: int) -> str:
532
+ def _fetch_pr_head_sha_for_diff(
533
+ local_repo: Path,
534
+ pr_number: int,
535
+ expected_head_sha: str | None = None,
536
+ ) -> str:
537
+ if expected_head_sha:
538
+ return _shared_fetch_pr_head_sha_unless_local_matches(
539
+ local_repo,
540
+ pr_number,
541
+ expected_head_sha=expected_head_sha,
542
+ )
391
543
  return _shared_fetch_pr_head_sha(local_repo, pr_number)
392
544
 
393
545
 
@@ -482,11 +634,22 @@ def _build_diff_context(
482
634
  )
483
635
 
484
636
  fetched_base_ref = _fetch_base_sha_for_diff(local_repo, base_ref)
485
- fetched_head_ref = _fetch_pr_head_sha_for_diff(local_repo, pr_number)
637
+ fetched_head_ref = _fetch_pr_head_sha_for_diff(
638
+ local_repo,
639
+ pr_number,
640
+ expected_head_sha=expected_head_sha,
641
+ )
486
642
  if fetched_head_ref.lower() != expected_head_sha.lower():
487
643
  raise FetchedHeadMismatch(expected_head_sha, fetched_head_ref)
488
644
  diff_range = f"{fetched_base_ref}...{fetched_head_ref}"
489
645
  stat = _run_git(local_repo, ["diff", "--stat", "--find-renames", diff_range])
646
+ changed_files_text = _run_git(
647
+ local_repo,
648
+ ["-c", "core.quotePath=false", "diff", "--name-only", "--find-renames", diff_range],
649
+ )
650
+ changed_files = tuple(
651
+ line.strip() for line in changed_files_text.splitlines() if line.strip()
652
+ )
490
653
  included_diff, full_diff_bytes, was_truncated = _run_git_limited(
491
654
  local_repo,
492
655
  ["diff", "--find-renames", "--unified=80", diff_range],
@@ -500,6 +663,7 @@ def _build_diff_context(
500
663
  return DiffContext(
501
664
  stat=stat,
502
665
  diff=included_diff,
666
+ changed_files=changed_files,
503
667
  was_truncated=was_truncated,
504
668
  requested_max_bytes=max_diff_bytes,
505
669
  hard_limit_bytes=hard_limit,
@@ -705,10 +869,13 @@ def format_comment(
705
869
  stale_end_sha: Optional[str] = None,
706
870
  is_unknown: bool = False,
707
871
  merge_authority: bool = True,
872
+ actions_run_id: Optional[str] = None,
708
873
  ) -> str:
709
874
  posture = "merge-authority lane" if merge_authority else "informational only"
710
875
  header = f"## Claude audit ({posture})\n\n"
711
876
  header += f"Head SHA: `{head_sha}`\n"
877
+ if actions_run_id:
878
+ header += f"{AUDIT_RUN_TRAILER_PREFIX} run_id={actions_run_id} -->\n"
712
879
  if is_stale:
713
880
  body = (
714
881
  header
@@ -848,6 +1015,10 @@ def audit_pr(config: ClaudeAuditConfig, repo: str, pr_number: int) -> ClaudeAudi
848
1015
  )
849
1016
  posted = post_pr_comment(repo, pr_number, comment_body, token=config.github_token)
850
1017
  result.posted_comment_url = posted.get("html_url")
1018
+ _record_posted_comment_url(
1019
+ result.verdict_artifact_path,
1020
+ result.posted_comment_url,
1021
+ )
851
1022
  print(
852
1023
  f"posted {repo}#{pr_number} verdict=STALE "
853
1024
  f"url={result.posted_comment_url}",
@@ -906,8 +1077,46 @@ def audit_pr(config: ClaudeAuditConfig, repo: str, pr_number: int) -> ClaudeAudi
906
1077
  ),
907
1078
  )
908
1079
 
1080
+ raw_output_attempts: List[Dict[str, Any]] = []
1081
+ parsed = _unknown_structured_verdict("Claude audit did not run")
1082
+ claude_stdout = ""
1083
+ claude_stderr = ""
1084
+ attempt_prompt = prompt
909
1085
  t0 = time.time()
910
- parsed, claude_stdout, claude_stderr = run_claude_audit(config, prompt)
1086
+ for attempt in range(1, MAX_CLAUDE_AUDIT_ATTEMPTS + 1):
1087
+ parsed_candidate, attempt_stdout, attempt_stderr = run_claude_audit(
1088
+ config,
1089
+ attempt_prompt,
1090
+ )
1091
+ claude_stdout = attempt_stdout
1092
+ claude_stderr = attempt_stderr
1093
+ parsed, guardrail_reason = _apply_claude_verdict_guardrails(
1094
+ parsed_candidate,
1095
+ diff_context.changed_files,
1096
+ )
1097
+ raw_output_attempts.append(
1098
+ {
1099
+ "attempt": attempt,
1100
+ "stdout": attempt_stdout,
1101
+ "stderr": attempt_stderr,
1102
+ "verdict": parsed_candidate.verdict,
1103
+ "guardrail_rejection": guardrail_reason,
1104
+ }
1105
+ )
1106
+ if guardrail_reason and attempt < MAX_CLAUDE_AUDIT_ATTEMPTS:
1107
+ print(
1108
+ " structured-verdict guardrail rejected Claude output: "
1109
+ f"{guardrail_reason}; retrying once",
1110
+ file=sys.stderr,
1111
+ flush=True,
1112
+ )
1113
+ attempt_prompt = _guardrail_retry_prompt(
1114
+ prompt,
1115
+ guardrail_reason,
1116
+ diff_context.changed_files,
1117
+ )
1118
+ continue
1119
+ break
911
1120
  dt = time.time() - t0
912
1121
  print(f" claude audit completed in {dt:.0f}s", file=sys.stderr, flush=True)
913
1122
  if parsed.mismatch_note:
@@ -916,6 +1125,7 @@ def audit_pr(config: ClaudeAuditConfig, repo: str, pr_number: int) -> ClaudeAudi
916
1125
  pr_meta_after = fetch_pull_request(repo, pr_number, token=config.github_token)
917
1126
  head_sha_end = pr_meta_after["head"]["sha"]
918
1127
  is_stale = head_sha_start != head_sha_end
1128
+ actions_run_id = os.environ.get("GITHUB_RUN_ID") or None
919
1129
 
920
1130
  if is_stale:
921
1131
  comment_body = format_comment(
@@ -924,6 +1134,7 @@ def audit_pr(config: ClaudeAuditConfig, repo: str, pr_number: int) -> ClaudeAudi
924
1134
  is_stale=True,
925
1135
  stale_end_sha=head_sha_end,
926
1136
  merge_authority=config.merge_authority,
1137
+ actions_run_id=actions_run_id,
927
1138
  )
928
1139
  result_verdict = "STALE"
929
1140
  trailer = STALE_TRAILER
@@ -933,6 +1144,7 @@ def audit_pr(config: ClaudeAuditConfig, repo: str, pr_number: int) -> ClaudeAudi
933
1144
  head_sha_start,
934
1145
  is_unknown=True,
935
1146
  merge_authority=config.merge_authority,
1147
+ actions_run_id=actions_run_id,
936
1148
  )
937
1149
  result_verdict = "UNKNOWN"
938
1150
  trailer = STALE_TRAILER
@@ -941,6 +1153,7 @@ def audit_pr(config: ClaudeAuditConfig, repo: str, pr_number: int) -> ClaudeAudi
941
1153
  parsed,
942
1154
  head_sha_start,
943
1155
  merge_authority=config.merge_authority,
1156
+ actions_run_id=actions_run_id,
944
1157
  )
945
1158
  result_verdict = parsed.verdict
946
1159
  trailer = BLOCKED_TRAILER if parsed.verdict == "BLOCKED" else DONE_TRAILER
@@ -976,8 +1189,13 @@ def audit_pr(config: ClaudeAuditConfig, repo: str, pr_number: int) -> ClaudeAudi
976
1189
  file=sys.stderr,
977
1190
  flush=True,
978
1191
  )
1192
+ _write_claude_raw_output_sidecar(artifact_path, raw_output_attempts)
979
1193
  posted = post_pr_comment(repo, pr_number, comment_body, token=config.github_token)
980
1194
  result.posted_comment_url = posted.get("html_url")
1195
+ _record_posted_comment_url(
1196
+ result.verdict_artifact_path,
1197
+ result.posted_comment_url,
1198
+ )
981
1199
  print(
982
1200
  f"posted {repo}#{pr_number} verdict={result_verdict} "
983
1201
  f"url={result.posted_comment_url}",
@@ -14,7 +14,7 @@ from pathlib import Path
14
14
  if __package__ in {None, ""}:
15
15
  raise SystemExit(
16
16
  "code_mower.cli is a packaged entrypoint. Install Code Mower with "
17
- "`pipx install code-mower==0.5.0b41`, or run source checkouts with "
17
+ "`pipx install code-mower==0.5.0b43`, or run source checkouts with "
18
18
  "`PYTHONPATH=src python -m code_mower.cli`."
19
19
  )
20
20
 
@@ -418,6 +418,12 @@ def main(argv: list[str] | None = None) -> int:
418
418
  default=MAX_EVENT_COUNT,
419
419
  help=f"number of verdict artifacts to include; max {MAX_EVENT_COUNT}",
420
420
  )
421
+ reviewer_runs.add_argument(
422
+ "--offset",
423
+ type=int,
424
+ default=0,
425
+ help="skip this many newest matching verdict artifacts before applying --limit",
426
+ )
421
427
  reviewer_runs.add_argument(
422
428
  "--include-git-ref",
423
429
  action="store_true",
@@ -674,6 +680,7 @@ def main(argv: list[str] | None = None) -> int:
674
680
  team_id=args.team_id,
675
681
  install_id=args.install_id,
676
682
  limit=args.limit,
683
+ offset=args.offset,
677
684
  endpoint=args.endpoint,
678
685
  token_env=args.token_env,
679
686
  yes=args.yes,