code-mower 0.5.0b45__tar.gz → 0.5.0b47__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 (304) hide show
  1. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/PKG-INFO +6 -6
  2. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/README.md +5 -5
  3. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/pyproject.toml +1 -1
  4. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/__init__.py +1 -1
  5. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/audit_labeler_lib.py +46 -6
  6. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/claude_audit_pr.py +175 -4
  7. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/cli.py +1 -1
  8. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/code_mower_telemetry.py +2 -0
  9. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/codex_audit_pr.py +56 -13
  10. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/__init__.py +2 -0
  11. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/github_actions_failures.py +26 -0
  12. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/runner.py +2 -0
  13. code_mower-0.5.0b47/src/code_mower/doctor_checks/runtime.py +173 -0
  14. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/gate_health.py +399 -20
  15. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/init.py +51 -2
  16. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/package_content.py +3 -4
  17. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/package_manifest.py +1 -0
  18. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/provider_runners/__init__.py +8 -1
  19. code_mower-0.5.0b47/src/code_mower/provider_runners/comments.py +71 -0
  20. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/provider_runners/github_pr.py +15 -0
  21. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/provider_runners/verdict_artifacts.py +12 -4
  22. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/product-support/safe_gh_comment.py +21 -1
  23. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/workflows/builder-provenance.yml.j2 +1 -1
  24. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/workflows/code-mower-gate-health.yml.j2 +7 -0
  25. {code_mower-0.5.0b45 → code_mower-0.5.0b47/src/code_mower}/templates/workflows/code-mower-gate.yml.j2 +112 -11
  26. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/workflows/needs-owner-notify.yml.j2 +3 -3
  27. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/workflows/review-clear-stale.yml.j2 +3 -4
  28. {code_mower-0.5.0b45 → code_mower-0.5.0b47/src/code_mower}/templates/workflows/self-hosted-local-audit.yml.j2 +6 -2
  29. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/workflows/trailer-comment-labeler.yml.j2 +15 -2
  30. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/trailer_comment_labeler.py +2 -1
  31. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower.egg-info/PKG-INFO +6 -6
  32. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower.egg-info/SOURCES.txt +1 -0
  33. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/workflows/code-mower-gate-health.yml.j2 +7 -0
  34. {code_mower-0.5.0b45/src/code_mower → code_mower-0.5.0b47}/templates/workflows/code-mower-gate.yml.j2 +112 -11
  35. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/workflows/review-clear-stale.yml.j2 +3 -4
  36. {code_mower-0.5.0b45/src/code_mower → code_mower-0.5.0b47}/templates/workflows/self-hosted-local-audit.yml.j2 +6 -2
  37. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/workflows/trailer-comment-labeler.yml.j2 +15 -2
  38. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_audit_comment_posture.py +31 -0
  39. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_builder_identity.py +21 -0
  40. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_claude_audit_pr.py +109 -0
  41. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_codex_audit_pr.py +57 -0
  42. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_doctor_github_checks.py +30 -0
  43. code_mower-0.5.0b47/tests/test_doctor_runtime.py +60 -0
  44. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_gate_health.py +462 -3
  45. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_release_hygiene.py +592 -52
  46. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_reviewer_metrics_core.py +60 -0
  47. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_trailer_comment_labeler.py +75 -5
  48. code_mower-0.5.0b45/src/code_mower/doctor_checks/runtime.py +0 -89
  49. code_mower-0.5.0b45/src/code_mower/provider_runners/comments.py +0 -31
  50. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/CODE_OF_CONDUCT.md +0 -0
  51. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/LICENSE +0 -0
  52. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/MANIFEST.in +0 -0
  53. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/NOTICE +0 -0
  54. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/SUPPORT.md +0 -0
  55. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/requirements/requirements.txt +0 -0
  56. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/setup.cfg +0 -0
  57. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/adapters/__init__.py +0 -0
  58. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/adapters/_base.py +0 -0
  59. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/adapters/cursor_bugbot.py +0 -0
  60. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/adapters/gitar.py +0 -0
  61. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/adapters/greptile.py +0 -0
  62. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/adapters/qodo.py +0 -0
  63. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/antigravity_cli_audit_pr.py +0 -0
  64. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/audit_handoff_log.py +0 -0
  65. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/audit_progress.py +0 -0
  66. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/blind_review_artifacts.py +0 -0
  67. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/blind_review_coordinator.py +0 -0
  68. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/bootstrap.py +0 -0
  69. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/builder_experiment.py +0 -0
  70. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/builder_runs.py +0 -0
  71. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/calibration/__init__.py +0 -0
  72. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/calibration/arms.py +0 -0
  73. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/calibration/auto_discovery.py +0 -0
  74. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/calibration/commands.py +0 -0
  75. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/calibration/context_inputs.py +0 -0
  76. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/calibration/corpus.py +0 -0
  77. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/calibration/effect_report.py +0 -0
  78. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/calibration/evidence.py +0 -0
  79. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/calibration/evidence_report.py +0 -0
  80. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/calibration/identity.py +0 -0
  81. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/calibration/metrics.py +0 -0
  82. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/calibration/overlap.py +0 -0
  83. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/calibration/planning.py +0 -0
  84. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/calibration/policy.py +0 -0
  85. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/calibration/results.py +0 -0
  86. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/calibration/run_results.py +0 -0
  87. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/calibration/run_status.py +0 -0
  88. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/calibration/runner.py +0 -0
  89. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/calibration/truth.py +0 -0
  90. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/calibration/value_report.py +0 -0
  91. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/checks.py +0 -0
  92. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/claude_cli_bounce.py +0 -0
  93. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/claude_cli_environment.py +0 -0
  94. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/clear_stale.py +0 -0
  95. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/cloud.py +0 -0
  96. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/cloud_client/__init__.py +0 -0
  97. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/cloud_client/bundle.py +0 -0
  98. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/cloud_client/doctor.py +0 -0
  99. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/cloud_client/dogfood.py +0 -0
  100. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/cloud_client/endpoints.py +0 -0
  101. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/cloud_client/errors.py +0 -0
  102. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/cloud_client/events.py +0 -0
  103. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/cloud_client/export.py +0 -0
  104. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/cloud_client/git_metadata.py +0 -0
  105. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/cloud_client/manifest.py +0 -0
  106. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/cloud_client/operations.py +0 -0
  107. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/cloud_client/reports.py +0 -0
  108. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/cloud_client/setup.py +0 -0
  109. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/cloud_client/upload.py +0 -0
  110. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/code_mower_calibration.py +0 -0
  111. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/code_mower_context_packs.py +0 -0
  112. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/code_mower_merge.py +0 -0
  113. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/coderabbit_cli_audit_pr.py +0 -0
  114. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/codex_audit_env_preflight.py +0 -0
  115. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/codex_audit_schema_smoke.py +0 -0
  116. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/codex_audit_verdict.schema.json +0 -0
  117. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/config.py +0 -0
  118. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor.py +0 -0
  119. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/cloud.py +0 -0
  120. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/common.py +0 -0
  121. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/github.py +0 -0
  122. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/github_actions.py +0 -0
  123. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/github_actions_cost.py +0 -0
  124. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/github_actions_cost_summary.py +0 -0
  125. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/github_actions_failure_annotations.py +0 -0
  126. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/github_actions_failure_models.py +0 -0
  127. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/github_actions_failure_scan.py +0 -0
  128. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/github_actions_failure_selection.py +0 -0
  129. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/github_actions_permissions.py +0 -0
  130. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/github_api.py +0 -0
  131. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/github_branch.py +0 -0
  132. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/github_config.py +0 -0
  133. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/github_provider.py +0 -0
  134. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/github_repo.py +0 -0
  135. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/groups.py +0 -0
  136. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/models.py +0 -0
  137. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/output.py +0 -0
  138. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/presets.py +0 -0
  139. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/privacy.py +0 -0
  140. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/provider_api_model.py +0 -0
  141. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/provider_api_model_openai.py +0 -0
  142. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/provider_api_model_profiles.py +0 -0
  143. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/provider_env.py +0 -0
  144. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/provider_env_required.py +0 -0
  145. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/provider_env_tokens.py +0 -0
  146. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/provider_local_cli.py +0 -0
  147. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/provider_local_cli_commands.py +0 -0
  148. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/provider_local_cli_probe_config.py +0 -0
  149. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/provider_probe.py +0 -0
  150. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/provider_probe_auth.py +0 -0
  151. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/provider_probe_evaluation.py +0 -0
  152. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/provider_probe_json.py +0 -0
  153. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/provider_probe_remediation.py +0 -0
  154. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/provider_review_hygiene.py +0 -0
  155. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/providers.py +0 -0
  156. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/registry.py +0 -0
  157. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/doctor_checks/runtime_github_auth.py +0 -0
  158. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/gemini_cli_audit_pr.py +0 -0
  159. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/grok_build_audit_pr.py +0 -0
  160. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/hermes_cli_audit_pr.py +0 -0
  161. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/lane_configs/__init__.py +0 -0
  162. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/lane_configs/aider.py +0 -0
  163. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/lane_configs/antigravity_cli.py +0 -0
  164. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/lane_configs/claude.py +0 -0
  165. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/lane_configs/codex.py +0 -0
  166. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/lane_configs/devin.py +0 -0
  167. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/lane_configs/gemini_cli.py +0 -0
  168. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/lane_configs/grok_build.py +0 -0
  169. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/lane_configs/hermes_cli.py +0 -0
  170. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/lane_configs/local_llm.py +0 -0
  171. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/local_llm_audit_pr.py +0 -0
  172. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/local_llm_bakeoff.py +0 -0
  173. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/local_llm_calibration.py +0 -0
  174. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/local_llm_profiles.py +0 -0
  175. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/migration.py +0 -0
  176. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/migration_install.py +0 -0
  177. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/migration_mirror.py +0 -0
  178. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/migration_readiness.py +0 -0
  179. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/migration_rehearsal.py +0 -0
  180. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/next_steps.py +0 -0
  181. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/package.py +0 -0
  182. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/package_paths.py +0 -0
  183. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/package_rendering.py +0 -0
  184. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/package_static.py +0 -0
  185. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/plan_context.py +0 -0
  186. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/prompts.py +0 -0
  187. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/provider_registry.py +0 -0
  188. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/provider_runners/git.py +0 -0
  189. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/provider_runners/github_auth.py +0 -0
  190. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/provider_runners/pr_worktree.py +0 -0
  191. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/provider_runners/process.py +0 -0
  192. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/provider_runners/repo_paths.py +0 -0
  193. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/provider_runners/text_schema.py +0 -0
  194. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/provider_runners/workspace.py +0 -0
  195. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/providers/__init__.py +0 -0
  196. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/providers/local_cli.py +0 -0
  197. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/providers/provenance.py +0 -0
  198. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/release_readiness.py +0 -0
  199. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/reviewer_metrics.py +0 -0
  200. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/reviewer_spend.py +0 -0
  201. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/saas_reviewer_labeler.py +0 -0
  202. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/secrets.py +0 -0
  203. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/builder-experiment.example.json +0 -0
  204. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/calibration-corpus.example.json +0 -0
  205. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/calibration-corpus.json +0 -0
  206. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/code-mower.example.yml +0 -0
  207. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/context-packs.example.json +0 -0
  208. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/lane_prompts/base-audit.md +0 -0
  209. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/lane_prompts/calibration-policy.md +0 -0
  210. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/lane_prompts/context-driven-quality.md +0 -0
  211. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/lane_prompts/docs-design.md +0 -0
  212. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/lane_prompts/generic-programming.md +0 -0
  213. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/lane_prompts/operability.md +0 -0
  214. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/lane_prompts/package-runtime.md +0 -0
  215. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/lane_prompts/plan-conformance.md +0 -0
  216. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/lane_prompts/security-threat-model.md +0 -0
  217. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/product-support/code_mower +0 -0
  218. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/product-support/code_mower_standalone_pin.env +0 -0
  219. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/product-support/code_mower_standalone_shadow.sh +0 -0
  220. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/product-support/run_claude_audit_pr.sh +0 -0
  221. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/product-support/run_codex_audit_pr.sh +0 -0
  222. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/product-support/status_report.py +0 -0
  223. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/providers.yml +0 -0
  224. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/reviewer-spend.example.json +0 -0
  225. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/reviewer-value-report.example.md +0 -0
  226. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/workflows/audit-label-cleanup.yml.j2 +0 -0
  227. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/workflows/hosted-bridge.yml.j2 +0 -0
  228. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/workflows/local-cli-audit.yml.j2 +0 -0
  229. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/workflows/private-standalone-shadow.yml.j2 +0 -0
  230. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/workflows/saas-reviewer-labeler.yml.j2 +0 -0
  231. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/templates/workflows/weekly-status.yml.j2 +0 -0
  232. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/versioning.py +0 -0
  233. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower/work_orders.py +0 -0
  234. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower.egg-info/dependency_links.txt +0 -0
  235. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower.egg-info/entry_points.txt +0 -0
  236. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower.egg-info/requires.txt +0 -0
  237. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/src/code_mower.egg-info/top_level.txt +0 -0
  238. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/builder-experiment.example.json +0 -0
  239. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/calibration-corpus.example.json +0 -0
  240. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/calibration-corpus.json +0 -0
  241. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/code-mower.yml.j2 +0 -0
  242. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/context-packs.example.json +0 -0
  243. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/cursor/BUGBOT.md +0 -0
  244. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/providers/acp_bridge.yml +0 -0
  245. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/providers/aider.yml +0 -0
  246. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/providers/antigravity_cli.yml +0 -0
  247. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/providers/claude_audit.yml +0 -0
  248. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/providers/claude_review.yml +0 -0
  249. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/providers/coderabbit_cli.yml +0 -0
  250. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/providers/codex.yml +0 -0
  251. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/providers/cursor_bugbot.yml +0 -0
  252. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/providers/devin.yml +0 -0
  253. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/providers/gemini_cli.yml +0 -0
  254. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/providers/gitar.yml +0 -0
  255. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/providers/greptile.yml +0 -0
  256. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/providers/hermes_cli.yml +0 -0
  257. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/providers/local_llm.yml +0 -0
  258. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/providers/qodo.yml +0 -0
  259. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/providers.yml +0 -0
  260. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/reviewer-spend.example.json +0 -0
  261. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/reviewer-value-report.example.md +0 -0
  262. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/workflows/audit-label-cleanup.yml.j2 +0 -0
  263. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/workflows/blind-review-artifacts-dry-run.yml.j2 +0 -0
  264. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/workflows/builder-provenance.yml.j2 +0 -0
  265. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/workflows/hosted-bridge.yml.j2 +0 -0
  266. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/workflows/local-cli-audit.yml.j2 +0 -0
  267. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/workflows/private-standalone-shadow.yml.j2 +0 -0
  268. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/templates/workflows/saas-reviewer-labeler.yml.j2 +0 -0
  269. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_builder_runs.py +0 -0
  270. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_calibration_effect_report.py +0 -0
  271. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_calibration_truth.py +0 -0
  272. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_checks.py +0 -0
  273. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_claude_cli_bounce.py +0 -0
  274. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_clear_stale.py +0 -0
  275. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_cloud_client.py +0 -0
  276. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_cloud_git_metadata.py +0 -0
  277. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_cloud_manifest.py +0 -0
  278. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_cloud_reports.py +0 -0
  279. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_coderabbit_cli_audit_pr.py +0 -0
  280. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_codex_audit_env_preflight.py +0 -0
  281. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_doctor_actions_cost_summary.py +0 -0
  282. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_doctor_actions_failure_selection.py +0 -0
  283. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_doctor_boundaries.py +0 -0
  284. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_doctor_output.py +0 -0
  285. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_doctor_privacy.py +0 -0
  286. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_doctor_provider_env_required.py +0 -0
  287. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_doctor_provider_env_tokens.py +0 -0
  288. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_doctor_provider_probe.py +0 -0
  289. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_doctor_provider_review_hygiene.py +0 -0
  290. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_doctor_registry.py +0 -0
  291. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_local_llm_github_helpers.py +0 -0
  292. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_plan_context.py +0 -0
  293. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_prompts.py +0 -0
  294. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_provider_local_cli.py +0 -0
  295. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_provider_runners_comments.py +0 -0
  296. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_provider_runners_git.py +0 -0
  297. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_provider_runners_github_pr.py +0 -0
  298. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_provider_runners_pr_worktree.py +0 -0
  299. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_provider_runners_process.py +0 -0
  300. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_provider_runners_repo_paths.py +0 -0
  301. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_provider_runners_text_schema.py +0 -0
  302. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_provider_runners_workspace.py +0 -0
  303. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/tests/test_reviewer_spend.py +0 -0
  304. {code_mower-0.5.0b45 → code_mower-0.5.0b47}/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.0b45
3
+ Version: 0.5.0b47
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.0b45
106
+ pipx install --python python3.12 code-mower==0.5.0b47
107
107
  code-mower --version
108
108
  ```
109
109
 
110
- `0.5.0b45` is a beta release. Until Code Mower publishes a stable `1.0`
110
+ `0.5.0b47` 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.0b45`.
158
+ the latest 10/10 public-package readiness proof for `code-mower==0.5.0b47`.
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.45` from
313
+ The current public beta is `v0.5.0-beta.47` from
314
314
  [codemower-ai/code-mower](https://github.com/codemower-ai/code-mower), published
315
- as `code-mower==0.5.0b45` on [PyPI](https://pypi.org/project/code-mower/).
315
+ as `code-mower==0.5.0b47` 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.0b45
91
+ pipx install --python python3.12 code-mower==0.5.0b47
92
92
  code-mower --version
93
93
  ```
94
94
 
95
- `0.5.0b45` is a beta release. Until Code Mower publishes a stable `1.0`
95
+ `0.5.0b47` 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.0b45`.
143
+ the latest 10/10 public-package readiness proof for `code-mower==0.5.0b47`.
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.45` from
298
+ The current public beta is `v0.5.0-beta.47` from
299
299
  [codemower-ai/code-mower](https://github.com/codemower-ai/code-mower), published
300
- as `code-mower==0.5.0b45` on [PyPI](https://pypi.org/project/code-mower/).
300
+ as `code-mower==0.5.0b47` 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.0b45"
7
+ version = "0.5.0b47"
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.0b45"
3
+ __version__ = "0.5.0b47"
@@ -10,6 +10,7 @@ has proven the shared library on main.
10
10
  from __future__ import annotations
11
11
 
12
12
  import functools
13
+ import hashlib
13
14
  import json
14
15
  import os
15
16
  import sys
@@ -24,7 +25,9 @@ import re
24
25
  MIN_ABBREVIATED_SHA_LENGTH = 7
25
26
  AUTHOR_EXCLUSION_ENV = "CODE_MOWER_AUTHOR_EXCLUSION_JSON"
26
27
  ACTIONS_RUN_MARKER_RE = re.compile(
27
- r"<!--\s*CODE_MOWER_AUDIT_RUN:\s*run_id=([0-9]+)\s*-->"
28
+ r"<!--\s*CODE_MOWER_AUDIT_RUN:\s*run_id=([0-9]+)"
29
+ r"(?:\s+comment_id=([0-9]+))?"
30
+ r"(?:\s+body_sha256=([0-9a-f]{64}))?\s*-->"
28
31
  )
29
32
  TRUSTED_ACTIONS_AUDIT_EVENTS = frozenset({"pull_request_target"})
30
33
 
@@ -44,6 +47,13 @@ class GitHubToken:
44
47
  value: str
45
48
 
46
49
 
50
+ @dataclass(frozen=True)
51
+ class ActionsRunMarker:
52
+ run_id: str
53
+ comment_id: str
54
+ body_sha256: str
55
+
56
+
47
57
  @dataclass(frozen=True)
48
58
  class LaneConfig:
49
59
  name: str
@@ -300,8 +310,32 @@ def parse_csv_set(raw: str) -> frozenset[str]:
300
310
 
301
311
 
302
312
  def parse_audit_run_id(body: str) -> str:
313
+ marker = parse_audit_run_marker(body)
314
+ return marker.run_id if marker else ""
315
+
316
+
317
+ def parse_audit_run_marker(body: str) -> Optional[ActionsRunMarker]:
318
+ match = ACTIONS_RUN_MARKER_RE.search(body)
319
+ if not match:
320
+ return None
321
+ return ActionsRunMarker(
322
+ run_id=match.group(1),
323
+ comment_id=match.group(2) or "",
324
+ body_sha256=match.group(3) or "",
325
+ )
326
+
327
+
328
+ def audit_run_marker_body_digest_matches(body: str) -> bool:
303
329
  match = ACTIONS_RUN_MARKER_RE.search(body)
304
- return match.group(1) if match else ""
330
+ if not match or not match.group(2) or not match.group(3):
331
+ return False
332
+ canonical_marker = (
333
+ "<!-- CODE_MOWER_AUDIT_RUN: "
334
+ f"run_id={match.group(1)} comment_id={match.group(2)} -->"
335
+ )
336
+ canonical_body = body[: match.start()] + canonical_marker + body[match.end() :]
337
+ digest = hashlib.sha256(canonical_body.encode("utf-8")).hexdigest()
338
+ return digest == match.group(3)
305
339
 
306
340
 
307
341
  def workflow_path_matches(path: str, trusted_workflows: Sequence[str]) -> bool:
@@ -357,6 +391,7 @@ def github_actions_comment_attested(
357
391
  *,
358
392
  repo: str,
359
393
  body: str,
394
+ comment_id: int | str | None = None,
360
395
  issue_number: int,
361
396
  head_sha: str,
362
397
  workflow_paths: Sequence[str],
@@ -367,14 +402,19 @@ def github_actions_comment_attested(
367
402
  """Return whether a github-actions[bot] audit comment is tied to this PR head."""
368
403
  trusted_workflows = tuple(parse_csv_set(",".join(workflow_paths)))
369
404
  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:
405
+ marker = parse_audit_run_marker(body)
406
+ comment_id_text = str(comment_id or "").strip()
407
+ if not trusted_workflows or not token_list or marker is None or not head_sha:
408
+ return False
409
+ if not comment_id_text or marker.comment_id != comment_id_text:
410
+ return False
411
+ if not audit_run_marker_body_digest_matches(body):
372
412
  return False
373
413
  try:
374
414
  if actions_run_lookup is None:
375
- run = fetch_actions_run(repo, run_id, tokens=token_list)
415
+ run = fetch_actions_run(repo, marker.run_id, tokens=token_list)
376
416
  else:
377
- run = dict(actions_run_lookup(run_id))
417
+ run = dict(actions_run_lookup(marker.run_id))
378
418
  except Exception:
379
419
  return False
380
420
 
@@ -21,6 +21,7 @@ import tempfile
21
21
  import time
22
22
  import urllib.error
23
23
  from dataclasses import dataclass, field, replace
24
+ from datetime import datetime, timezone
24
25
  from pathlib import Path
25
26
  from typing import Any, Dict, List, Optional, Tuple
26
27
 
@@ -33,7 +34,9 @@ if __package__ in {None, "", "tools"}:
33
34
  from tools.claude_cli_environment import clean_claude_cli_env, env_flag
34
35
  from tools.provider_runners import (
35
36
  audit_runtime_quarantine_reason as _audit_runtime_quarantine_reason,
37
+ bind_actions_run_comment_id,
36
38
  clip_text as _clip_text,
39
+ edit_pr_comment,
37
40
  fetch_pull_request,
38
41
  fetch_base_ref_sha as _shared_fetch_base_ref_sha,
39
42
  fetch_pr_head_sha as _shared_fetch_pr_head_sha,
@@ -61,7 +64,9 @@ if __package__ in {None, "", "tools"}:
61
64
  from claude_cli_environment import clean_claude_cli_env, env_flag # type: ignore
62
65
  from provider_runners import ( # type: ignore
63
66
  audit_runtime_quarantine_reason as _audit_runtime_quarantine_reason,
67
+ bind_actions_run_comment_id,
64
68
  clip_text as _clip_text,
69
+ edit_pr_comment,
65
70
  fetch_pull_request,
66
71
  fetch_base_ref_sha as _shared_fetch_base_ref_sha,
67
72
  fetch_pr_head_sha as _shared_fetch_pr_head_sha,
@@ -87,7 +92,9 @@ else: # pragma: no cover - exercised after package extraction.
87
92
  from .provider_runners import (
88
93
  FetchedHeadMismatch,
89
94
  audit_runtime_quarantine_reason as _audit_runtime_quarantine_reason,
95
+ bind_actions_run_comment_id,
90
96
  clip_text as _clip_text,
97
+ edit_pr_comment,
91
98
  fetch_base_ref_sha as _shared_fetch_base_ref_sha,
92
99
  fetch_pull_request,
93
100
  fetch_pr_head_sha as _shared_fetch_pr_head_sha,
@@ -123,11 +130,32 @@ MIN_BLOCKED_VERDICT_PROSE_CHARS = 80
123
130
  SCHEMA_PLACEHOLDER_TEXT = frozenset({"test", "example", "placeholder"})
124
131
  CLAUDE_RAW_OUTPUT_SCHEMA = "code_mower.claudeAuditRawOutput.v1"
125
132
  BLOCKER_SEVERITIES = frozenset({"P0", "P1", "P2"})
133
+ DEFAULT_CLAUDE_CLI_FAILURE_DIR = (
134
+ Path.home() / ".cache" / "code-mower-audits" / "cli-failures"
135
+ )
126
136
 
127
137
  STALE_TRAILER = "<!-- CLAUDE_AUDIT_STATE: needs-claude-audit -->"
128
138
  DONE_TRAILER = "<!-- CLAUDE_AUDIT_STATE: claude-audit-done -->"
129
139
  BLOCKED_TRAILER = "<!-- CLAUDE_AUDIT_STATE: claude-audit-blocked -->"
130
140
  AUDIT_RUN_TRAILER_PREFIX = "<!-- CODE_MOWER_AUDIT_RUN:"
141
+ STALE_REQUEUE_MARKER = "<!-- CODE_MOWER_AUDIT_REQUEUE: kind=stale -->"
142
+ UNKNOWN_REQUEUE_MARKER = "<!-- CODE_MOWER_AUDIT_REQUEUE: kind=unknown -->"
143
+
144
+
145
+ def _post_audit_comment(
146
+ repo: str,
147
+ pr_number: int,
148
+ body: str,
149
+ *,
150
+ token: str,
151
+ actions_run_id: Optional[str],
152
+ ) -> tuple[dict[str, Any], str]:
153
+ posted = post_pr_comment(repo, pr_number, body, token=token)
154
+ if not actions_run_id:
155
+ return posted, body
156
+ bound_body = bind_actions_run_comment_id(body, posted.get("id"))
157
+ edited = edit_pr_comment(repo, posted["id"], bound_body, token=token)
158
+ return edited if isinstance(edited, dict) else posted, bound_body
131
159
 
132
160
 
133
161
  CLAUDE_VERDICT_SCHEMA: Dict[str, Any] = {
@@ -492,6 +520,104 @@ def _write_claude_raw_output_sidecar(
492
520
  )
493
521
 
494
522
 
523
+ def _claude_status_payload(stdout: str, stderr: str) -> Dict[str, Any]:
524
+ candidates = [stdout.strip(), stderr.strip()]
525
+ candidates.extend(
526
+ line.strip()
527
+ for text in (stdout, stderr)
528
+ for line in reversed(text.splitlines())
529
+ )
530
+ for candidate in candidates:
531
+ if not candidate:
532
+ continue
533
+ try:
534
+ payload = json.loads(candidate)
535
+ except json.JSONDecodeError:
536
+ continue
537
+ if isinstance(payload, dict):
538
+ return payload
539
+ return {}
540
+
541
+
542
+ def _claude_payload_text(payload: Dict[str, Any], key: str, limit: int = 500) -> str:
543
+ value = payload.get(key)
544
+ if value is None:
545
+ return ""
546
+ if isinstance(value, (dict, list)):
547
+ text = json.dumps(value, sort_keys=True)
548
+ else:
549
+ text = str(value)
550
+ return _one_line(text, limit)
551
+
552
+
553
+ def _claude_cli_failure_reason(stdout: str, stderr: str) -> str:
554
+ payload = _claude_status_payload(stdout, stderr)
555
+ if not payload:
556
+ return ""
557
+ result = _claude_payload_text(payload, "result", 160)
558
+ terminal_reason = _claude_payload_text(payload, "terminal_reason", 160)
559
+ status = _claude_payload_text(payload, "api_error_status", 40)
560
+ reason = result.split(" · ", 1)[0].strip() or terminal_reason
561
+ if not reason and status:
562
+ reason = f"API error {status}"
563
+ return f"Claude CLI: {reason}" if reason else ""
564
+
565
+
566
+ def _dump_claude_cli_failure(
567
+ *,
568
+ repo: str,
569
+ pr_number: int,
570
+ head_sha: str,
571
+ stdout: str,
572
+ stderr: str,
573
+ reason: str,
574
+ base_dir: Optional[Path] = None,
575
+ ) -> Optional[Path]:
576
+ try:
577
+ base_dir = base_dir or DEFAULT_CLAUDE_CLI_FAILURE_DIR
578
+ base_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
579
+ try:
580
+ os.chmod(base_dir, 0o700)
581
+ except OSError:
582
+ pass
583
+ owner_name = repo.replace("/", "_")
584
+ short_sha = (head_sha or "unknown")[:12]
585
+ ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S_%fZ")
586
+ path = base_dir / f"{owner_name}_pr{pr_number}_{short_sha}_{ts}_claude.log"
587
+ payload = _claude_status_payload(stdout, stderr)
588
+ field_lines = ""
589
+ for key in ("result", "terminal_reason", "api_error_status"):
590
+ value = _claude_payload_text(payload, key)
591
+ if value:
592
+ field_lines += f"# claude_{key}: {value}\n"
593
+ body = (
594
+ "# Claude CLI raw output - structured verdict was unusable\n"
595
+ f"# repo: {repo}\n"
596
+ f"# pr: {pr_number}\n"
597
+ f"# head_sha: {head_sha}\n"
598
+ f"# captured_at: {ts}\n"
599
+ f"# diagnostic_reason: {reason}\n"
600
+ f"{field_lines}"
601
+ f"# stdout_bytes: {len(stdout.encode('utf-8'))}\n"
602
+ f"# stderr_bytes: {len(stderr.encode('utf-8'))}\n"
603
+ "\n===== STDOUT =====\n"
604
+ + (stdout if stdout else "<empty>\n")
605
+ + "\n===== STDERR =====\n"
606
+ + (stderr if stderr else "<empty>\n")
607
+ )
608
+ fd = os.open(
609
+ str(path),
610
+ os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0),
611
+ 0o600,
612
+ )
613
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
614
+ handle.write(body)
615
+ return path
616
+ except OSError as exc:
617
+ print(f" warning: failed to dump Claude CLI output: {exc}", file=sys.stderr)
618
+ return None
619
+
620
+
495
621
  def _guardrail_retry_prompt(
496
622
  prompt: str,
497
623
  reason: str,
@@ -899,6 +1025,7 @@ def format_comment(
899
1025
  is_unknown: bool = False,
900
1026
  merge_authority: bool = True,
901
1027
  actions_run_id: Optional[str] = None,
1028
+ unknown_reason: str = "",
902
1029
  ) -> str:
903
1030
  posture = "merge-authority lane" if merge_authority else "informational only"
904
1031
  header = f"## Claude audit ({posture})\n\n"
@@ -911,16 +1038,23 @@ def format_comment(
911
1038
  + f"\nHead SHA changed during review (`{head_sha[:8]}` -> "
912
1039
  + f"`{(stale_end_sha or '?')[:8]}`). Skipping this verdict and "
913
1040
  + "requeuing for re-review of the new head.\n\n"
1041
+ + STALE_REQUEUE_MARKER
1042
+ + "\n"
914
1043
  + STALE_TRAILER
915
1044
  + "\n"
916
1045
  )
917
1046
  return limit_comment_body(body, STALE_TRAILER, provider_name="Claude")
918
1047
  if is_unknown:
1048
+ reason_line = f"\n\n{unknown_reason}." if unknown_reason else ""
919
1049
  body = (
920
1050
  header
921
1051
  + "\nCould not validate a Claude structured verdict artifact. "
922
1052
  + "The CLI may have failed, exceeded budget, or emitted no "
923
- + "schema-valid structured output. Requeuing for re-review.\n\n"
1053
+ + "schema-valid structured output. Requeuing for re-review."
1054
+ + reason_line
1055
+ + "\n\n"
1056
+ + UNKNOWN_REQUEUE_MARKER
1057
+ + "\n"
924
1058
  + STALE_TRAILER
925
1059
  + "\n"
926
1060
  )
@@ -996,6 +1130,7 @@ def audit_pr(config: ClaudeAuditConfig, repo: str, pr_number: int) -> ClaudeAudi
996
1130
  config.max_diff_hard_limit_bytes,
997
1131
  )
998
1132
  except FetchedHeadMismatch as exc:
1133
+ actions_run_id = os.environ.get("GITHUB_RUN_ID") or None
999
1134
  print(
1000
1135
  f" force-push race: fetched head {exc.actual_sha[:8]} does not "
1001
1136
  f"match recorded head {exc.expected_sha[:8]}; emitting STALE",
@@ -1012,6 +1147,7 @@ def audit_pr(config: ClaudeAuditConfig, repo: str, pr_number: int) -> ClaudeAudi
1012
1147
  is_stale=True,
1013
1148
  stale_end_sha=exc.actual_sha,
1014
1149
  merge_authority=config.merge_authority,
1150
+ actions_run_id=actions_run_id,
1015
1151
  )
1016
1152
  result = ClaudeAuditResult(
1017
1153
  repo=repo,
@@ -1066,7 +1202,14 @@ def audit_pr(config: ClaudeAuditConfig, repo: str, pr_number: int) -> ClaudeAudi
1066
1202
  quarantine_reason,
1067
1203
  )
1068
1204
  result.comment_body = comment_body
1069
- posted = post_pr_comment(repo, pr_number, comment_body, token=config.github_token)
1205
+ posted, comment_body = _post_audit_comment(
1206
+ repo,
1207
+ pr_number,
1208
+ comment_body,
1209
+ token=config.github_token,
1210
+ actions_run_id=actions_run_id,
1211
+ )
1212
+ result.comment_body = comment_body
1070
1213
  result.posted_comment_url = posted.get("html_url")
1071
1214
  _record_posted_comment_url(
1072
1215
  result.verdict_artifact_path,
@@ -1181,6 +1324,7 @@ def audit_pr(config: ClaudeAuditConfig, repo: str, pr_number: int) -> ClaudeAudi
1181
1324
  head_sha_end = pr_meta_after["head"]["sha"]
1182
1325
  is_stale = head_sha_start != head_sha_end
1183
1326
  actions_run_id = os.environ.get("GITHUB_RUN_ID") or None
1327
+ claude_cli_reason = _claude_cli_failure_reason(claude_stdout, claude_stderr)
1184
1328
 
1185
1329
  if is_stale:
1186
1330
  comment_body = format_comment(
@@ -1200,6 +1344,7 @@ def audit_pr(config: ClaudeAuditConfig, repo: str, pr_number: int) -> ClaudeAudi
1200
1344
  is_unknown=True,
1201
1345
  merge_authority=config.merge_authority,
1202
1346
  actions_run_id=actions_run_id,
1347
+ unknown_reason=claude_cli_reason,
1203
1348
  )
1204
1349
  result_verdict = "UNKNOWN"
1205
1350
  trailer = STALE_TRAILER
@@ -1227,6 +1372,21 @@ def audit_pr(config: ClaudeAuditConfig, repo: str, pr_number: int) -> ClaudeAudi
1227
1372
  )
1228
1373
 
1229
1374
  if not config.dry_run:
1375
+ if result_verdict == "UNKNOWN":
1376
+ dump_path = _dump_claude_cli_failure(
1377
+ repo=repo,
1378
+ pr_number=pr_number,
1379
+ head_sha=head_sha_start,
1380
+ stdout=claude_stdout,
1381
+ stderr=claude_stderr,
1382
+ reason=parsed.prose,
1383
+ )
1384
+ if dump_path is not None:
1385
+ print(
1386
+ f" captured Claude CLI output for diagnosis: {dump_path}",
1387
+ file=sys.stderr,
1388
+ flush=True,
1389
+ )
1230
1390
  quarantine_reason = _audit_runtime_quarantine_reason(
1231
1391
  comment_body=comment_body,
1232
1392
  fixture_reason=final_fixture_reason if parsed.verdict == "UNKNOWN" else None,
@@ -1269,7 +1429,14 @@ def audit_pr(config: ClaudeAuditConfig, repo: str, pr_number: int) -> ClaudeAudi
1269
1429
  quarantine_reason,
1270
1430
  )
1271
1431
  result.comment_body = comment_body
1272
- posted = post_pr_comment(repo, pr_number, comment_body, token=config.github_token)
1432
+ posted, comment_body = _post_audit_comment(
1433
+ repo,
1434
+ pr_number,
1435
+ comment_body,
1436
+ token=config.github_token,
1437
+ actions_run_id=actions_run_id,
1438
+ )
1439
+ result.comment_body = comment_body
1273
1440
  result.posted_comment_url = posted.get("html_url")
1274
1441
  _record_posted_comment_url(
1275
1442
  result.verdict_artifact_path,
@@ -1534,7 +1701,11 @@ def main(argv: Optional[List[str]] = None) -> int:
1534
1701
  print(f"spend metadata appended to {args.spend_path}", file=sys.stderr)
1535
1702
  except (OSError, ValueError) as exc:
1536
1703
  print(f"warning: failed to append spend metadata: {exc}", file=sys.stderr)
1537
- return 2 if result.verdict in {"STALE", "UNKNOWN"} else 0
1704
+ return _audit_exit_code(result.verdict)
1705
+
1706
+
1707
+ def _audit_exit_code(verdict: str) -> int:
1708
+ return 2 if verdict == "UNKNOWN" else 0
1538
1709
 
1539
1710
 
1540
1711
  if __name__ == "__main__":
@@ -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.0b45`, or run source checkouts with "
17
+ "`pipx install code-mower==0.5.0b47`, or run source checkouts with "
18
18
  "`PYTHONPATH=src python -m code_mower.cli`."
19
19
  )
20
20
 
@@ -229,6 +229,8 @@ def reviewer_run_event_from_verdict_artifact(
229
229
  duration_seconds = _artifact_duration_seconds(payload)
230
230
  if duration_seconds is not None:
231
231
  metrics["duration_seconds_total"] = duration_seconds
232
+ metrics["duration_seconds"] = duration_seconds
233
+ metrics["wall_seconds"] = duration_seconds
232
234
  dimensions["duration_source"] = "verdict_artifact"
233
235
  return {
234
236
  "schema": BENCHMARK_EVENT_SCHEMA,
@@ -57,7 +57,7 @@ daemon analogous to local_llm_audit_bridge.py):
57
57
  Exit codes (CLI mode):
58
58
  0 comment posted (or dry-run printed)
59
59
  1 generic error (config, network, Codex CLI failure)
60
- 2 stale head SHA detected mid-review (caller may requeue)
60
+ 2 UNKNOWN verdict emitted (caller should requeue and investigate)
61
61
 
62
62
  Trailer protocol (mirrors Devin/local LLM):
63
63
  <!-- CODEX_AUDIT_STATE: codex-audit-done --> (verdict was PASS)
@@ -90,7 +90,9 @@ if __package__ in {None, "", "tools"}:
90
90
  from tools.audit_progress import AuditProgress, run_subprocess_with_progress
91
91
  from tools.provider_runners import (
92
92
  audit_runtime_quarantine_reason as _audit_runtime_quarantine_reason,
93
+ bind_actions_run_comment_id,
93
94
  clip_text as _clip_text,
95
+ edit_pr_comment,
94
96
  fetch_pull_request,
95
97
  fetch_base_ref as _shared_fetch_base_ref,
96
98
  fetch_pr_head as _shared_fetch_pr_head,
@@ -115,7 +117,9 @@ if __package__ in {None, "", "tools"}:
115
117
  from audit_progress import AuditProgress, run_subprocess_with_progress # type: ignore
116
118
  from provider_runners import ( # type: ignore
117
119
  audit_runtime_quarantine_reason as _audit_runtime_quarantine_reason,
120
+ bind_actions_run_comment_id,
118
121
  clip_text as _clip_text,
122
+ edit_pr_comment,
119
123
  fetch_pull_request,
120
124
  fetch_base_ref as _shared_fetch_base_ref,
121
125
  fetch_pr_head as _shared_fetch_pr_head,
@@ -140,8 +144,10 @@ else: # pragma: no cover - exercised after package extraction.
140
144
  from .audit_progress import AuditProgress, run_subprocess_with_progress
141
145
  from .provider_runners import (
142
146
  audit_runtime_quarantine_reason as _audit_runtime_quarantine_reason,
147
+ bind_actions_run_comment_id,
143
148
  clip_text as _clip_text,
144
149
  create_temp_worktree as _shared_create_temp_worktree,
150
+ edit_pr_comment,
145
151
  fetch_base_ref as _shared_fetch_base_ref,
146
152
  fetch_pull_request,
147
153
  fetch_pr_head as _shared_fetch_pr_head,
@@ -319,6 +325,24 @@ STALE_TRAILER = "<!-- CODEX_AUDIT_STATE: needs-codex-audit -->"
319
325
  DONE_TRAILER = "<!-- CODEX_AUDIT_STATE: codex-audit-done -->"
320
326
  BLOCKED_TRAILER = "<!-- CODEX_AUDIT_STATE: codex-audit-blocked -->"
321
327
  AUDIT_RUN_TRAILER_PREFIX = "<!-- CODE_MOWER_AUDIT_RUN:"
328
+ STALE_REQUEUE_MARKER = "<!-- CODE_MOWER_AUDIT_REQUEUE: kind=stale -->"
329
+ UNKNOWN_REQUEUE_MARKER = "<!-- CODE_MOWER_AUDIT_REQUEUE: kind=unknown -->"
330
+
331
+
332
+ def _post_audit_comment(
333
+ repo: str,
334
+ pr_number: int,
335
+ body: str,
336
+ *,
337
+ token: str,
338
+ actions_run_id: Optional[str],
339
+ ) -> tuple[dict[str, Any], str]:
340
+ posted = post_pr_comment(repo, pr_number, body, token=token)
341
+ if not actions_run_id:
342
+ return posted, body
343
+ bound_body = bind_actions_run_comment_id(body, posted.get("id"))
344
+ edited = edit_pr_comment(repo, posted["id"], bound_body, token=token)
345
+ return edited if isinstance(edited, dict) else posted, bound_body
322
346
 
323
347
 
324
348
  def _quarantine_is_test_only(reason: Optional[str]) -> bool:
@@ -1242,6 +1266,8 @@ def format_comment(
1242
1266
  + f"\nHead SHA changed during review (`{head_sha[:8]}` → "
1243
1267
  + f"`{(stale_end_sha or '?')[:8]}`). Skipping this verdict and "
1244
1268
  + "requeuing for re-review of the new head.\n\n"
1269
+ + STALE_REQUEUE_MARKER
1270
+ + "\n"
1245
1271
  + STALE_TRAILER
1246
1272
  + "\n"
1247
1273
  )
@@ -1254,6 +1280,8 @@ def format_comment(
1254
1280
  + "the structured-output pass may have failed, or the verdict "
1255
1281
  + "format may have drifted. "
1256
1282
  + "Requeuing for re-review.\n\n"
1283
+ + UNKNOWN_REQUEUE_MARKER
1284
+ + "\n"
1257
1285
  + STALE_TRAILER
1258
1286
  + "\n"
1259
1287
  )
@@ -1389,6 +1417,7 @@ def audit_pr(config: AuditConfig, repo: str, pr_number: int) -> AuditResult:
1389
1417
  pr_meta_after = fetch_pull_request(repo, pr_number, token=config.github_token)
1390
1418
  head_sha_after = pr_meta_after["head"]["sha"]
1391
1419
  if head_sha_after != head_sha_start:
1420
+ actions_run_id = os.environ.get("GITHUB_RUN_ID") or None
1392
1421
  print(f" force-push race: head moved from {head_sha_start[:8]} "
1393
1422
  f"to {head_sha_after[:8]} during fetch; emitting STALE",
1394
1423
  file=sys.stderr, flush=True)
@@ -1399,6 +1428,7 @@ def audit_pr(config: AuditConfig, repo: str, pr_number: int) -> AuditResult:
1399
1428
  is_stale=True,
1400
1429
  stale_end_sha=head_sha_after,
1401
1430
  merge_authority=config.merge_authority,
1431
+ actions_run_id=actions_run_id,
1402
1432
  )
1403
1433
  result = AuditResult(
1404
1434
  repo=repo, pr_number=pr_number,
@@ -1458,8 +1488,14 @@ def audit_pr(config: AuditConfig, repo: str, pr_number: int) -> AuditResult:
1458
1488
  quarantine_reason,
1459
1489
  )
1460
1490
  result.comment_body = comment_body
1461
- posted = post_pr_comment(repo, pr_number, comment_body,
1462
- token=config.github_token)
1491
+ posted, comment_body = _post_audit_comment(
1492
+ repo,
1493
+ pr_number,
1494
+ comment_body,
1495
+ token=config.github_token,
1496
+ actions_run_id=actions_run_id,
1497
+ )
1498
+ result.comment_body = comment_body
1463
1499
  result.posted_comment_url = posted.get("html_url")
1464
1500
  config.progress.emit(
1465
1501
  "audit",
@@ -1657,8 +1693,14 @@ def audit_pr(config: AuditConfig, repo: str, pr_number: int) -> AuditResult:
1657
1693
  quarantine_reason,
1658
1694
  )
1659
1695
  result.comment_body = comment_body
1660
- posted = post_pr_comment(repo, pr_number, comment_body,
1661
- token=config.github_token)
1696
+ posted, comment_body = _post_audit_comment(
1697
+ repo,
1698
+ pr_number,
1699
+ comment_body,
1700
+ token=config.github_token,
1701
+ actions_run_id=actions_run_id,
1702
+ )
1703
+ result.comment_body = comment_body
1662
1704
  result.posted_comment_url = posted.get("html_url")
1663
1705
  print(f"posted {repo}#{pr_number} verdict={result_verdict} "
1664
1706
  f"url={result.posted_comment_url}", file=sys.stderr)
@@ -2016,14 +2058,15 @@ def main(argv: Optional[List[str]] = None) -> int:
2016
2058
  except (OSError, ValueError) as exc:
2017
2059
  print(f"warning: failed to append spend metadata: {exc}", file=sys.stderr)
2018
2060
 
2019
- # Codex round 7 of #234 — P2: both STALE and UNKNOWN result in a
2020
- # `needs-codex-audit` requeue comment. Automation using the exit
2021
- # code to decide whether to retry must see exit 2 for BOTH cases;
2022
- # otherwise UNKNOWN looks like a successful audit even though the
2023
- # comment requests re-review.
2024
- if result.verdict in ("STALE", "UNKNOWN"):
2025
- return 2
2026
- return 0
2061
+ return _audit_exit_code(result.verdict)
2062
+
2063
+
2064
+ def _audit_exit_code(verdict: str) -> int:
2065
+ # UNKNOWN still means the lane produced no trustworthy verdict and should
2066
+ # fail loudly. STALE means a newer head superseded this run after the wrapper
2067
+ # posted a requeue note, so returning success avoids alarm-grade noise while
2068
+ # the newer head's audit becomes authoritative.
2069
+ return 2 if verdict == "UNKNOWN" else 0
2027
2070
 
2028
2071
 
2029
2072
  if __name__ == "__main__":