code-mower 0.5.0b46__tar.gz → 0.5.0b48__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 (307) hide show
  1. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/PKG-INFO +6 -6
  2. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/README.md +5 -5
  3. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/pyproject.toml +1 -1
  4. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/__init__.py +1 -1
  5. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/audit_labeler_lib.py +236 -6
  6. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/calibration/__init__.py +2 -0
  7. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/calibration/effect_report.py +10 -2
  8. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/calibration/evidence_report.py +5 -0
  9. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/calibration/policy.py +2 -0
  10. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/calibration/run_status.py +4 -1
  11. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/calibration/value_report.py +7 -5
  12. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/claude_audit_pr.py +197 -5
  13. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/cli.py +1 -1
  14. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/code_mower_calibration.py +5 -0
  15. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/code_mower_telemetry.py +2 -0
  16. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/codex_audit_pr.py +80 -14
  17. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/__init__.py +2 -0
  18. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/github_actions_failures.py +26 -0
  19. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/runner.py +2 -0
  20. code_mower-0.5.0b48/src/code_mower/doctor_checks/runtime.py +173 -0
  21. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/gate_health.py +401 -21
  22. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/init.py +36 -2
  23. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/package_content.py +3 -4
  24. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/package_manifest.py +1 -0
  25. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/provider_runners/__init__.py +8 -1
  26. code_mower-0.5.0b48/src/code_mower/provider_runners/comments.py +71 -0
  27. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/provider_runners/github_pr.py +15 -0
  28. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/provider_runners/verdict_artifacts.py +12 -4
  29. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/reviewer_metrics.py +3 -0
  30. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/product-support/safe_gh_comment.py +21 -1
  31. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/reviewer-value-report.example.md +3 -3
  32. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/workflows/builder-provenance.yml.j2 +1 -1
  33. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/workflows/code-mower-gate-health.yml.j2 +7 -0
  34. code_mower-0.5.0b48/src/code_mower/templates/workflows/code-mower-gate.yml.j2 +612 -0
  35. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/workflows/needs-owner-notify.yml.j2 +3 -3
  36. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/workflows/review-clear-stale.yml.j2 +2 -3
  37. {code_mower-0.5.0b46 → code_mower-0.5.0b48/src/code_mower}/templates/workflows/self-hosted-local-audit.yml.j2 +46 -55
  38. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/workflows/trailer-comment-labeler.yml.j2 +15 -2
  39. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/trailer_comment_labeler.py +107 -1
  40. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower.egg-info/PKG-INFO +6 -6
  41. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower.egg-info/SOURCES.txt +1 -0
  42. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/reviewer-value-report.example.md +3 -3
  43. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/workflows/code-mower-gate-health.yml.j2 +7 -0
  44. code_mower-0.5.0b48/templates/workflows/code-mower-gate.yml.j2 +612 -0
  45. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/workflows/review-clear-stale.yml.j2 +2 -3
  46. {code_mower-0.5.0b46/src/code_mower → code_mower-0.5.0b48}/templates/workflows/self-hosted-local-audit.yml.j2 +46 -55
  47. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/workflows/trailer-comment-labeler.yml.j2 +15 -2
  48. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_audit_comment_posture.py +31 -0
  49. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_builder_identity.py +21 -0
  50. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_calibration_effect_report.py +33 -0
  51. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_calibration_truth.py +41 -0
  52. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_claude_audit_pr.py +127 -0
  53. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_codex_audit_pr.py +75 -0
  54. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_doctor_github_checks.py +30 -0
  55. code_mower-0.5.0b48/tests/test_doctor_runtime.py +60 -0
  56. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_gate_health.py +478 -3
  57. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_release_hygiene.py +754 -54
  58. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_reviewer_metrics_core.py +64 -0
  59. code_mower-0.5.0b48/tests/test_trailer_comment_labeler.py +339 -0
  60. code_mower-0.5.0b46/src/code_mower/doctor_checks/runtime.py +0 -89
  61. code_mower-0.5.0b46/src/code_mower/provider_runners/comments.py +0 -31
  62. code_mower-0.5.0b46/src/code_mower/templates/workflows/code-mower-gate.yml.j2 +0 -269
  63. code_mower-0.5.0b46/templates/workflows/code-mower-gate.yml.j2 +0 -269
  64. code_mower-0.5.0b46/tests/test_trailer_comment_labeler.py +0 -151
  65. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/CODE_OF_CONDUCT.md +0 -0
  66. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/LICENSE +0 -0
  67. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/MANIFEST.in +0 -0
  68. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/NOTICE +0 -0
  69. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/SUPPORT.md +0 -0
  70. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/requirements/requirements.txt +0 -0
  71. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/setup.cfg +0 -0
  72. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/adapters/__init__.py +0 -0
  73. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/adapters/_base.py +0 -0
  74. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/adapters/cursor_bugbot.py +0 -0
  75. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/adapters/gitar.py +0 -0
  76. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/adapters/greptile.py +0 -0
  77. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/adapters/qodo.py +0 -0
  78. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/antigravity_cli_audit_pr.py +0 -0
  79. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/audit_handoff_log.py +0 -0
  80. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/audit_progress.py +0 -0
  81. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/blind_review_artifacts.py +0 -0
  82. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/blind_review_coordinator.py +0 -0
  83. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/bootstrap.py +0 -0
  84. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/builder_experiment.py +0 -0
  85. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/builder_runs.py +0 -0
  86. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/calibration/arms.py +0 -0
  87. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/calibration/auto_discovery.py +0 -0
  88. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/calibration/commands.py +0 -0
  89. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/calibration/context_inputs.py +0 -0
  90. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/calibration/corpus.py +0 -0
  91. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/calibration/evidence.py +0 -0
  92. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/calibration/identity.py +0 -0
  93. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/calibration/metrics.py +0 -0
  94. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/calibration/overlap.py +0 -0
  95. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/calibration/planning.py +0 -0
  96. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/calibration/results.py +0 -0
  97. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/calibration/run_results.py +0 -0
  98. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/calibration/runner.py +0 -0
  99. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/calibration/truth.py +0 -0
  100. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/checks.py +0 -0
  101. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/claude_cli_bounce.py +0 -0
  102. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/claude_cli_environment.py +0 -0
  103. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/clear_stale.py +0 -0
  104. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/cloud.py +0 -0
  105. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/cloud_client/__init__.py +0 -0
  106. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/cloud_client/bundle.py +0 -0
  107. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/cloud_client/doctor.py +0 -0
  108. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/cloud_client/dogfood.py +0 -0
  109. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/cloud_client/endpoints.py +0 -0
  110. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/cloud_client/errors.py +0 -0
  111. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/cloud_client/events.py +0 -0
  112. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/cloud_client/export.py +0 -0
  113. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/cloud_client/git_metadata.py +0 -0
  114. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/cloud_client/manifest.py +0 -0
  115. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/cloud_client/operations.py +0 -0
  116. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/cloud_client/reports.py +0 -0
  117. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/cloud_client/setup.py +0 -0
  118. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/cloud_client/upload.py +0 -0
  119. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/code_mower_context_packs.py +0 -0
  120. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/code_mower_merge.py +0 -0
  121. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/coderabbit_cli_audit_pr.py +0 -0
  122. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/codex_audit_env_preflight.py +0 -0
  123. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/codex_audit_schema_smoke.py +0 -0
  124. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/codex_audit_verdict.schema.json +0 -0
  125. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/config.py +0 -0
  126. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor.py +0 -0
  127. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/cloud.py +0 -0
  128. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/common.py +0 -0
  129. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/github.py +0 -0
  130. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/github_actions.py +0 -0
  131. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/github_actions_cost.py +0 -0
  132. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/github_actions_cost_summary.py +0 -0
  133. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/github_actions_failure_annotations.py +0 -0
  134. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/github_actions_failure_models.py +0 -0
  135. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/github_actions_failure_scan.py +0 -0
  136. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/github_actions_failure_selection.py +0 -0
  137. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/github_actions_permissions.py +0 -0
  138. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/github_api.py +0 -0
  139. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/github_branch.py +0 -0
  140. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/github_config.py +0 -0
  141. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/github_provider.py +0 -0
  142. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/github_repo.py +0 -0
  143. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/groups.py +0 -0
  144. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/models.py +0 -0
  145. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/output.py +0 -0
  146. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/presets.py +0 -0
  147. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/privacy.py +0 -0
  148. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/provider_api_model.py +0 -0
  149. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/provider_api_model_openai.py +0 -0
  150. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/provider_api_model_profiles.py +0 -0
  151. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/provider_env.py +0 -0
  152. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/provider_env_required.py +0 -0
  153. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/provider_env_tokens.py +0 -0
  154. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/provider_local_cli.py +0 -0
  155. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/provider_local_cli_commands.py +0 -0
  156. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/provider_local_cli_probe_config.py +0 -0
  157. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/provider_probe.py +0 -0
  158. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/provider_probe_auth.py +0 -0
  159. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/provider_probe_evaluation.py +0 -0
  160. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/provider_probe_json.py +0 -0
  161. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/provider_probe_remediation.py +0 -0
  162. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/provider_review_hygiene.py +0 -0
  163. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/providers.py +0 -0
  164. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/registry.py +0 -0
  165. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/doctor_checks/runtime_github_auth.py +0 -0
  166. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/gemini_cli_audit_pr.py +0 -0
  167. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/grok_build_audit_pr.py +0 -0
  168. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/hermes_cli_audit_pr.py +0 -0
  169. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/lane_configs/__init__.py +0 -0
  170. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/lane_configs/aider.py +0 -0
  171. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/lane_configs/antigravity_cli.py +0 -0
  172. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/lane_configs/claude.py +0 -0
  173. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/lane_configs/codex.py +0 -0
  174. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/lane_configs/devin.py +0 -0
  175. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/lane_configs/gemini_cli.py +0 -0
  176. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/lane_configs/grok_build.py +0 -0
  177. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/lane_configs/hermes_cli.py +0 -0
  178. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/lane_configs/local_llm.py +0 -0
  179. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/local_llm_audit_pr.py +0 -0
  180. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/local_llm_bakeoff.py +0 -0
  181. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/local_llm_calibration.py +0 -0
  182. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/local_llm_profiles.py +0 -0
  183. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/migration.py +0 -0
  184. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/migration_install.py +0 -0
  185. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/migration_mirror.py +0 -0
  186. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/migration_readiness.py +0 -0
  187. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/migration_rehearsal.py +0 -0
  188. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/next_steps.py +0 -0
  189. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/package.py +0 -0
  190. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/package_paths.py +0 -0
  191. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/package_rendering.py +0 -0
  192. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/package_static.py +0 -0
  193. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/plan_context.py +0 -0
  194. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/prompts.py +0 -0
  195. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/provider_registry.py +0 -0
  196. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/provider_runners/git.py +0 -0
  197. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/provider_runners/github_auth.py +0 -0
  198. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/provider_runners/pr_worktree.py +0 -0
  199. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/provider_runners/process.py +0 -0
  200. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/provider_runners/repo_paths.py +0 -0
  201. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/provider_runners/text_schema.py +0 -0
  202. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/provider_runners/workspace.py +0 -0
  203. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/providers/__init__.py +0 -0
  204. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/providers/local_cli.py +0 -0
  205. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/providers/provenance.py +0 -0
  206. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/release_readiness.py +0 -0
  207. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/reviewer_spend.py +0 -0
  208. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/saas_reviewer_labeler.py +0 -0
  209. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/secrets.py +0 -0
  210. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/builder-experiment.example.json +0 -0
  211. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/calibration-corpus.example.json +0 -0
  212. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/calibration-corpus.json +0 -0
  213. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/code-mower.example.yml +0 -0
  214. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/context-packs.example.json +0 -0
  215. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/lane_prompts/base-audit.md +0 -0
  216. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/lane_prompts/calibration-policy.md +0 -0
  217. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/lane_prompts/context-driven-quality.md +0 -0
  218. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/lane_prompts/docs-design.md +0 -0
  219. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/lane_prompts/generic-programming.md +0 -0
  220. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/lane_prompts/operability.md +0 -0
  221. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/lane_prompts/package-runtime.md +0 -0
  222. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/lane_prompts/plan-conformance.md +0 -0
  223. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/lane_prompts/security-threat-model.md +0 -0
  224. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/product-support/code_mower +0 -0
  225. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/product-support/code_mower_standalone_pin.env +0 -0
  226. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/product-support/code_mower_standalone_shadow.sh +0 -0
  227. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/product-support/run_claude_audit_pr.sh +0 -0
  228. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/product-support/run_codex_audit_pr.sh +0 -0
  229. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/product-support/status_report.py +0 -0
  230. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/providers.yml +0 -0
  231. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/reviewer-spend.example.json +0 -0
  232. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/workflows/audit-label-cleanup.yml.j2 +0 -0
  233. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/workflows/hosted-bridge.yml.j2 +0 -0
  234. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/workflows/local-cli-audit.yml.j2 +0 -0
  235. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/workflows/private-standalone-shadow.yml.j2 +0 -0
  236. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/workflows/saas-reviewer-labeler.yml.j2 +0 -0
  237. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/templates/workflows/weekly-status.yml.j2 +0 -0
  238. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/versioning.py +0 -0
  239. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower/work_orders.py +0 -0
  240. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower.egg-info/dependency_links.txt +0 -0
  241. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower.egg-info/entry_points.txt +0 -0
  242. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower.egg-info/requires.txt +0 -0
  243. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/src/code_mower.egg-info/top_level.txt +0 -0
  244. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/builder-experiment.example.json +0 -0
  245. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/calibration-corpus.example.json +0 -0
  246. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/calibration-corpus.json +0 -0
  247. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/code-mower.yml.j2 +0 -0
  248. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/context-packs.example.json +0 -0
  249. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/cursor/BUGBOT.md +0 -0
  250. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/providers/acp_bridge.yml +0 -0
  251. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/providers/aider.yml +0 -0
  252. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/providers/antigravity_cli.yml +0 -0
  253. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/providers/claude_audit.yml +0 -0
  254. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/providers/claude_review.yml +0 -0
  255. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/providers/coderabbit_cli.yml +0 -0
  256. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/providers/codex.yml +0 -0
  257. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/providers/cursor_bugbot.yml +0 -0
  258. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/providers/devin.yml +0 -0
  259. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/providers/gemini_cli.yml +0 -0
  260. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/providers/gitar.yml +0 -0
  261. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/providers/greptile.yml +0 -0
  262. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/providers/hermes_cli.yml +0 -0
  263. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/providers/local_llm.yml +0 -0
  264. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/providers/qodo.yml +0 -0
  265. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/providers.yml +0 -0
  266. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/reviewer-spend.example.json +0 -0
  267. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/workflows/audit-label-cleanup.yml.j2 +0 -0
  268. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/workflows/blind-review-artifacts-dry-run.yml.j2 +0 -0
  269. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/workflows/builder-provenance.yml.j2 +0 -0
  270. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/workflows/hosted-bridge.yml.j2 +0 -0
  271. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/workflows/local-cli-audit.yml.j2 +0 -0
  272. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/workflows/private-standalone-shadow.yml.j2 +0 -0
  273. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/templates/workflows/saas-reviewer-labeler.yml.j2 +0 -0
  274. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_builder_runs.py +0 -0
  275. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_checks.py +0 -0
  276. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_claude_cli_bounce.py +0 -0
  277. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_clear_stale.py +0 -0
  278. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_cloud_client.py +0 -0
  279. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_cloud_git_metadata.py +0 -0
  280. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_cloud_manifest.py +0 -0
  281. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_cloud_reports.py +0 -0
  282. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_coderabbit_cli_audit_pr.py +0 -0
  283. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_codex_audit_env_preflight.py +0 -0
  284. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_doctor_actions_cost_summary.py +0 -0
  285. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_doctor_actions_failure_selection.py +0 -0
  286. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_doctor_boundaries.py +0 -0
  287. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_doctor_output.py +0 -0
  288. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_doctor_privacy.py +0 -0
  289. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_doctor_provider_env_required.py +0 -0
  290. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_doctor_provider_env_tokens.py +0 -0
  291. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_doctor_provider_probe.py +0 -0
  292. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_doctor_provider_review_hygiene.py +0 -0
  293. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_doctor_registry.py +0 -0
  294. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_local_llm_github_helpers.py +0 -0
  295. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_plan_context.py +0 -0
  296. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_prompts.py +0 -0
  297. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_provider_local_cli.py +0 -0
  298. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_provider_runners_comments.py +0 -0
  299. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_provider_runners_git.py +0 -0
  300. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_provider_runners_github_pr.py +0 -0
  301. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_provider_runners_pr_worktree.py +0 -0
  302. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_provider_runners_process.py +0 -0
  303. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_provider_runners_repo_paths.py +0 -0
  304. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_provider_runners_text_schema.py +0 -0
  305. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_provider_runners_workspace.py +0 -0
  306. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/tests/test_reviewer_spend.py +0 -0
  307. {code_mower-0.5.0b46 → code_mower-0.5.0b48}/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.0b46
3
+ Version: 0.5.0b48
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.0b46
106
+ pipx install --python python3.12 code-mower==0.5.0b48
107
107
  code-mower --version
108
108
  ```
109
109
 
110
- `0.5.0b46` is a beta release. Until Code Mower publishes a stable `1.0`
110
+ `0.5.0b48` 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.0b46`.
158
+ the latest 10/10 public-package readiness proof for `code-mower==0.5.0b48`.
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.46` from
313
+ The current public beta is `v0.5.0-beta.48` from
314
314
  [codemower-ai/code-mower](https://github.com/codemower-ai/code-mower), published
315
- as `code-mower==0.5.0b46` on [PyPI](https://pypi.org/project/code-mower/).
315
+ as `code-mower==0.5.0b48` 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.0b46
91
+ pipx install --python python3.12 code-mower==0.5.0b48
92
92
  code-mower --version
93
93
  ```
94
94
 
95
- `0.5.0b46` is a beta release. Until Code Mower publishes a stable `1.0`
95
+ `0.5.0b48` 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.0b46`.
143
+ the latest 10/10 public-package readiness proof for `code-mower==0.5.0b48`.
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.46` from
298
+ The current public beta is `v0.5.0-beta.48` from
299
299
  [codemower-ai/code-mower](https://github.com/codemower-ai/code-mower), published
300
- as `code-mower==0.5.0b46` on [PyPI](https://pypi.org/project/code-mower/).
300
+ as `code-mower==0.5.0b48` 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.0b46"
7
+ version = "0.5.0b48"
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.0b46"
3
+ __version__ = "0.5.0b48"
@@ -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,9 +25,15 @@ 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"})
33
+ AUDIT_RUN_NON_TERMINAL_STATUSES = frozenset(
34
+ {"queued", "requested", "waiting", "pending", "in_progress"}
35
+ )
36
+ HEAD_SHA_LINE_RE = re.compile(r"Head SHA:\s*`?([0-9a-fA-F]{7,40})`?", re.IGNORECASE)
30
37
 
31
38
 
32
39
  @dataclass(frozen=True)
@@ -44,6 +51,13 @@ class GitHubToken:
44
51
  value: str
45
52
 
46
53
 
54
+ @dataclass(frozen=True)
55
+ class ActionsRunMarker:
56
+ run_id: str
57
+ comment_id: str
58
+ body_sha256: str
59
+
60
+
47
61
  @dataclass(frozen=True)
48
62
  class LaneConfig:
49
63
  name: str
@@ -300,8 +314,32 @@ def parse_csv_set(raw: str) -> frozenset[str]:
300
314
 
301
315
 
302
316
  def parse_audit_run_id(body: str) -> str:
317
+ marker = parse_audit_run_marker(body)
318
+ return marker.run_id if marker else ""
319
+
320
+
321
+ def parse_audit_run_marker(body: str) -> Optional[ActionsRunMarker]:
303
322
  match = ACTIONS_RUN_MARKER_RE.search(body)
304
- return match.group(1) if match else ""
323
+ if not match:
324
+ return None
325
+ return ActionsRunMarker(
326
+ run_id=match.group(1),
327
+ comment_id=match.group(2) or "",
328
+ body_sha256=match.group(3) or "",
329
+ )
330
+
331
+
332
+ def audit_run_marker_body_digest_matches(body: str) -> bool:
333
+ match = ACTIONS_RUN_MARKER_RE.search(body)
334
+ if not match or not match.group(2) or not match.group(3):
335
+ return False
336
+ canonical_marker = (
337
+ "<!-- CODE_MOWER_AUDIT_RUN: "
338
+ f"run_id={match.group(1)} comment_id={match.group(2)} -->"
339
+ )
340
+ canonical_body = body[: match.start()] + canonical_marker + body[match.end() :]
341
+ digest = hashlib.sha256(canonical_body.encode("utf-8")).hexdigest()
342
+ return digest == match.group(3)
305
343
 
306
344
 
307
345
  def workflow_path_matches(path: str, trusted_workflows: Sequence[str]) -> bool:
@@ -310,6 +348,192 @@ def workflow_path_matches(path: str, trusted_workflows: Sequence[str]) -> bool:
310
348
  return any(path == item or path.endswith("/" + item) for item in trusted_workflows)
311
349
 
312
350
 
351
+ def flatten_paginated_items(payload: Any) -> list[dict[str, Any]]:
352
+ items: list[dict[str, Any]] = []
353
+ if isinstance(payload, list):
354
+ for page in payload:
355
+ if isinstance(page, list):
356
+ items.extend(item for item in page if isinstance(item, dict))
357
+ elif isinstance(page, dict):
358
+ items.append(page)
359
+ return items
360
+
361
+
362
+ def audit_comment_head_sha(body: str) -> str:
363
+ match = HEAD_SHA_LINE_RE.search(body)
364
+ return match.group(1) if match else ""
365
+
366
+
367
+ def terminal_audit_trailer_verdict(lane: Mapping[str, Any], body: str) -> str:
368
+ candidates: list[tuple[int, str]] = []
369
+ for label_key, verdict in (("done", "done"), ("blocked", "blocked")):
370
+ label = str(lane.get(label_key) or "")
371
+ if not label:
372
+ continue
373
+ index = body.rfind(": " + label + " -->")
374
+ if index >= 0:
375
+ candidates.append((index, verdict))
376
+ if not candidates:
377
+ return ""
378
+ return max(candidates, key=lambda item: item[0])[1]
379
+
380
+
381
+ def current_audit_trailer_verdict(
382
+ lane: Mapping[str, Any],
383
+ body: str,
384
+ *,
385
+ head_sha: str,
386
+ ) -> str:
387
+ if "Head SHA: `" + head_sha + "`" not in body:
388
+ return ""
389
+ return terminal_audit_trailer_verdict(lane, body)
390
+
391
+
392
+ def latest_current_audit_verdict(
393
+ lane: Mapping[str, Any],
394
+ comments: Sequence[Mapping[str, Any]],
395
+ *,
396
+ head_sha: str,
397
+ trusted_comment_author: Callable[
398
+ [Mapping[str, Any], str, str, object, str | None],
399
+ bool,
400
+ ],
401
+ ) -> str:
402
+ verdicts: list[tuple[tuple[str, str, int, int], str]] = []
403
+ for index, comment in enumerate(comments):
404
+ author = str(((comment.get("user") or {}).get("login")) or "")
405
+ body = str(comment.get("body") or "")
406
+ if not trusted_comment_author(lane, author, body, comment.get("id"), None):
407
+ continue
408
+ verdict = current_audit_trailer_verdict(lane, body, head_sha=head_sha)
409
+ if verdict:
410
+ try:
411
+ comment_id = int(comment.get("id") or 0)
412
+ except (TypeError, ValueError):
413
+ comment_id = 0
414
+ updated = str(
415
+ comment.get("updated_at")
416
+ or comment.get("updatedAt")
417
+ or comment.get("created_at")
418
+ or comment.get("createdAt")
419
+ or ""
420
+ )
421
+ created = str(comment.get("created_at") or comment.get("createdAt") or "")
422
+ verdicts.append(((updated, created, comment_id, index), verdict))
423
+ if verdicts:
424
+ return max(verdicts, key=lambda item: item[0])[1]
425
+ return ""
426
+
427
+
428
+ def attested_non_current_audit_heads(
429
+ lanes: Sequence[Mapping[str, Any]],
430
+ comments: Sequence[Mapping[str, Any]],
431
+ *,
432
+ head_sha: str,
433
+ trusted_comment_author: Callable[
434
+ [Mapping[str, Any], str, str, object, str | None],
435
+ bool,
436
+ ],
437
+ ) -> list[str]:
438
+ heads: list[str] = []
439
+ for comment in comments:
440
+ author = str(((comment.get("user") or {}).get("login")) or "")
441
+ body = str(comment.get("body") or "")
442
+ reviewed = audit_comment_head_sha(body)
443
+ if not reviewed or sha_matches(reviewed, head_sha):
444
+ continue
445
+ for lane in lanes:
446
+ if not terminal_audit_trailer_verdict(lane, body):
447
+ continue
448
+ if not trusted_comment_author(lane, author, body, comment.get("id"), reviewed):
449
+ continue
450
+ heads.append(reviewed)
451
+ break
452
+ return list(dict.fromkeys(heads))
453
+
454
+
455
+ def audit_run_workflow_path(run: Mapping[str, Any]) -> str:
456
+ return str(run.get("path") or run.get("workflow_path") or run.get("workflowPath") or "")
457
+
458
+
459
+ def audit_run_matches_pr_head(
460
+ run: Mapping[str, Any],
461
+ *,
462
+ head_sha: str,
463
+ pr_number: int | str,
464
+ ) -> bool:
465
+ run_head_sha = str(run.get("head_sha") or "")
466
+ pull_requests = [
467
+ item for item in run.get("pull_requests") or [] if isinstance(item, Mapping)
468
+ ]
469
+ for item in pull_requests:
470
+ if not isinstance(item, Mapping):
471
+ continue
472
+ if str(item.get("number") or "") != str(pr_number):
473
+ continue
474
+ pr_head = str(((item.get("head") or {}).get("sha")) or "")
475
+ if pr_head and sha_matches(pr_head, head_sha):
476
+ return True
477
+ if run_head_sha and sha_matches(run_head_sha, head_sha):
478
+ return True
479
+ if pull_requests:
480
+ return False
481
+ if run_head_sha and sha_matches(run_head_sha, head_sha):
482
+ return True
483
+ return False
484
+
485
+
486
+ def _audit_lane_match_tokens(lane: Mapping[str, Any]) -> set[str]:
487
+ tokens = [
488
+ str(lane.get("id") or ""),
489
+ str(lane.get("author_lane") or ""),
490
+ str(lane.get("display_name") or ""),
491
+ ]
492
+ for label_key in ("done", "blocked"):
493
+ label = str(lane.get(label_key) or "")
494
+ for suffix in ("-audit-done", "-audit-blocked", "-done", "-blocked"):
495
+ if label.endswith(suffix):
496
+ tokens.append(label[: -len(suffix)])
497
+ return {token.strip().lower().replace("_", "-") for token in tokens if token.strip()}
498
+
499
+
500
+ def audit_job_matches_lane(job: Mapping[str, Any], lane: Mapping[str, Any]) -> bool:
501
+ name = str(job.get("name") or "").lower().replace("_", "-")
502
+ return any(token in name for token in _audit_lane_match_tokens(lane))
503
+
504
+
505
+ def audit_run_in_flight_for_lane(
506
+ entry: Mapping[str, Any],
507
+ lane: Mapping[str, Any],
508
+ *,
509
+ head_sha: str,
510
+ pr_number: int | str,
511
+ ) -> bool:
512
+ run = entry.get("run") if isinstance(entry.get("run"), Mapping) else entry
513
+ if not isinstance(run, Mapping):
514
+ return False
515
+ if str(run.get("status") or "") not in AUDIT_RUN_NON_TERMINAL_STATUSES:
516
+ return False
517
+ workflows = parse_csv_set(str(lane.get("github_actions_workflows") or ""))
518
+ if not workflow_path_matches(audit_run_workflow_path(run), workflows):
519
+ return False
520
+ if not audit_run_matches_pr_head(run, head_sha=head_sha, pr_number=pr_number):
521
+ return False
522
+ jobs = entry.get("jobs") if isinstance(entry.get("jobs"), list) else []
523
+ non_terminal_jobs = [
524
+ job
525
+ for job in jobs
526
+ if isinstance(job, Mapping)
527
+ and (
528
+ str(job.get("status") or "") in AUDIT_RUN_NON_TERMINAL_STATUSES
529
+ or (str(job.get("status") or "") != "completed" and job.get("conclusion") is None)
530
+ )
531
+ ]
532
+ if non_terminal_jobs:
533
+ return any(audit_job_matches_lane(job, lane) for job in non_terminal_jobs)
534
+ return bool(entry.get("jobs_fetch_failed") or not jobs)
535
+
536
+
313
537
  def _pr_item_matches_head(
314
538
  item: Mapping[str, Any],
315
539
  *,
@@ -357,6 +581,7 @@ def github_actions_comment_attested(
357
581
  *,
358
582
  repo: str,
359
583
  body: str,
584
+ comment_id: int | str | None = None,
360
585
  issue_number: int,
361
586
  head_sha: str,
362
587
  workflow_paths: Sequence[str],
@@ -367,14 +592,19 @@ def github_actions_comment_attested(
367
592
  """Return whether a github-actions[bot] audit comment is tied to this PR head."""
368
593
  trusted_workflows = tuple(parse_csv_set(",".join(workflow_paths)))
369
594
  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:
595
+ marker = parse_audit_run_marker(body)
596
+ comment_id_text = str(comment_id or "").strip()
597
+ if not trusted_workflows or not token_list or marker is None or not head_sha:
598
+ return False
599
+ if not comment_id_text or marker.comment_id != comment_id_text:
600
+ return False
601
+ if not audit_run_marker_body_digest_matches(body):
372
602
  return False
373
603
  try:
374
604
  if actions_run_lookup is None:
375
- run = fetch_actions_run(repo, run_id, tokens=token_list)
605
+ run = fetch_actions_run(repo, marker.run_id, tokens=token_list)
376
606
  else:
377
- run = dict(actions_run_lookup(run_id))
607
+ run = dict(actions_run_lookup(marker.run_id))
378
608
  except Exception:
379
609
  return False
380
610
 
@@ -85,6 +85,7 @@ from .run_status import (
85
85
  RUN_STATUS_CATEGORY_ALIASES,
86
86
  RUN_STATUS_INFRA_ERROR,
87
87
  RUN_STATUS_PASS,
88
+ RUN_STATUS_STALE,
88
89
  RUN_STATUS_UNKNOWN,
89
90
  count_normalized_findings,
90
91
  normalize_run_status_category,
@@ -119,6 +120,7 @@ __all__ = [
119
120
  "RUN_STATUS_CATEGORY_ALIASES",
120
121
  "RUN_STATUS_INFRA_ERROR",
121
122
  "RUN_STATUS_PASS",
123
+ "RUN_STATUS_STALE",
122
124
  "RUN_STATUS_UNKNOWN",
123
125
  "SELECTIVE_USEFUL_RATE",
124
126
  "TRUTH_EXPECTATION_ALIASES",
@@ -12,6 +12,7 @@ from .run_status import (
12
12
  RUN_STATUS_BLOCKED,
13
13
  RUN_STATUS_INFRA_ERROR,
14
14
  RUN_STATUS_PASS,
15
+ RUN_STATUS_STALE,
15
16
  normalize_run_status_category,
16
17
  )
17
18
 
@@ -39,6 +40,7 @@ class CellStats:
39
40
  known_blocked_missed_runs: int = 0
40
41
  audit_input_insufficient_runs: int = 0
41
42
  infra_error_runs: int = 0
43
+ stale_runs: int = 0
42
44
  unknown_blocked_runs: int = 0
43
45
  known_clean_runs: int = 0
44
46
  known_clean_pass_runs: int = 0
@@ -94,6 +96,7 @@ class CellStats:
94
96
  "clean_nonblocking_finding_runs": self.clean_nonblocking_finding_runs,
95
97
  "audit_input_insufficient_runs": self.audit_input_insufficient_runs,
96
98
  "infra_error_runs": self.infra_error_runs,
99
+ "stale_runs": self.stale_runs,
97
100
  "unknown_blocked_runs": self.unknown_blocked_runs,
98
101
  "duration_seconds_total": round(self.duration_seconds_total, 3),
99
102
  "seconds_per_run": _round_or_none(self.seconds_per_run),
@@ -209,8 +212,12 @@ def build_effect_report(
209
212
  cell.audit_input_insufficient_runs += 1
210
213
  elif status_category == RUN_STATUS_INFRA_ERROR:
211
214
  cell.infra_error_runs += 1
215
+ elif status_category == RUN_STATUS_STALE:
216
+ cell.stale_runs += 1
212
217
  else:
213
218
  cell.unknown_blocked_runs += 1
219
+ elif status_category == RUN_STATUS_STALE:
220
+ cell.stale_runs += 1
214
221
  if known_clean:
215
222
  cell.known_clean_runs += 1
216
223
  if status_category == RUN_STATUS_PASS and finding_count == 0:
@@ -489,8 +496,8 @@ def render_effect_report_text(report: Mapping[str, Any]) -> str:
489
496
  "",
490
497
  "## Provider/Lens Cells",
491
498
  "",
492
- "| Provider | Lens | Runs | Blocked caught/missed | Input gaps | Clean pass/false block | Effective catch | Evaluable catch | False-blocker rate | Sec/run |",
493
- "| --- | --- | ---: | --- | ---: | --- | ---: | ---: | ---: | ---: |",
499
+ "| Provider | Lens | Runs | Blocked caught/missed | Input gaps | Stale | Clean pass/false block | Effective catch | Evaluable catch | False-blocker rate | Sec/run |",
500
+ "| --- | --- | ---: | --- | ---: | ---: | --- | ---: | ---: | ---: | ---: |",
494
501
  ]
495
502
  for cell in report.get("cells", []) or []:
496
503
  if not isinstance(cell, Mapping):
@@ -504,6 +511,7 @@ def render_effect_report_text(report: Mapping[str, Any]) -> str:
504
511
  str(cell.get("runs", 0)),
505
512
  f"{cell.get('known_blocked_caught_runs', 0)}/{cell.get('known_blocked_missed_runs', 0)}",
506
513
  str(cell.get("audit_input_insufficient_runs", 0)),
514
+ str(cell.get("stale_runs", 0)),
507
515
  f"{cell.get('known_clean_pass_runs', 0)}/{cell.get('blocking_false_positive_runs', 0)}",
508
516
  _format_rate(cell.get("effective_catch_rate")),
509
517
  _format_rate(cell.get("catch_rate")),
@@ -12,6 +12,7 @@ from .run_status import (
12
12
  RUN_STATUS_BLOCKED,
13
13
  RUN_STATUS_INFRA_ERROR,
14
14
  RUN_STATUS_PASS,
15
+ RUN_STATUS_STALE,
15
16
  RUN_STATUS_UNKNOWN,
16
17
  normalize_run_status_category,
17
18
  )
@@ -77,6 +78,7 @@ def build_reviewer_evidence_report(corpus: Mapping[str, Any]) -> dict[str, Any]:
77
78
  profile_known_blocked_missed_run_keys: dict[str, set[tuple[Any, ...]]] = {}
78
79
  profile_audit_input_insufficient_run_keys: dict[str, set[tuple[Any, ...]]] = {}
79
80
  profile_infra_error_run_keys: dict[str, set[tuple[Any, ...]]] = {}
81
+ profile_stale_run_keys: dict[str, set[tuple[Any, ...]]] = {}
80
82
  profile_run_statuses: dict[str, dict[str, int]] = {}
81
83
  profile_automated_vs_manual: dict[str, dict[str, int]] = {}
82
84
  profile_review_classes: dict[str, set[str]] = {}
@@ -303,6 +305,8 @@ def build_reviewer_evidence_report(corpus: Mapping[str, Any]) -> dict[str, Any]:
303
305
  profile_infra_error_run_keys.setdefault(reviewer, set()).add(
304
306
  reviewer_run_key
305
307
  )
308
+ if status_category == RUN_STATUS_STALE:
309
+ profile_stale_run_keys.setdefault(reviewer, set()).add(reviewer_run_key)
306
310
  if status_category == RUN_STATUS_AUDIT_INPUT_INSUFFICIENT:
307
311
  profile_audit_input_insufficient_run_keys.setdefault(
308
312
  reviewer, set()
@@ -358,6 +362,7 @@ def build_reviewer_evidence_report(corpus: Mapping[str, Any]) -> dict[str, Any]:
358
362
  profile_known_blocked_missed_run_keys.get(reviewer, set())
359
363
  ),
360
364
  "infra_error_runs": len(profile_infra_error_run_keys.get(reviewer, set())),
365
+ "stale_runs": len(profile_stale_run_keys.get(reviewer, set())),
361
366
  "audit_input_insufficient_runs": len(
362
367
  profile_audit_input_insufficient_run_keys.get(reviewer, set())
363
368
  ),
@@ -32,6 +32,7 @@ def build_lane_policy_report(metrics: Mapping[str, Any]) -> dict[str, Any]:
32
32
  false_positive_runs = int(stats.get("blocking_false_positive_runs") or 0)
33
33
  known_blocked_missed_runs = int(stats.get("known_blocked_missed_runs") or 0)
34
34
  infra_error_runs = int(stats.get("infra_error_runs") or 0)
35
+ stale_runs = int(stats.get("stale_runs") or 0)
35
36
  audit_input_insufficient_runs = int(
36
37
  stats.get("audit_input_insufficient_runs") or 0
37
38
  )
@@ -158,6 +159,7 @@ def build_lane_policy_report(metrics: Mapping[str, Any]) -> dict[str, Any]:
158
159
  ),
159
160
  "known_blocked_missed_runs": known_blocked_missed_runs,
160
161
  "infra_error_runs": infra_error_runs,
162
+ "stale_runs": stale_runs,
161
163
  "audit_input_insufficient_runs": audit_input_insufficient_runs,
162
164
  "observed_pr_count": observed_pr_count,
163
165
  "reasons": reasons
@@ -9,6 +9,7 @@ RUN_STATUS_PASS = "pass"
9
9
  RUN_STATUS_BLOCKED = "blocked"
10
10
  RUN_STATUS_AUDIT_INPUT_INSUFFICIENT = "audit_input_insufficient"
11
11
  RUN_STATUS_INFRA_ERROR = "infra_error"
12
+ RUN_STATUS_STALE = "stale"
12
13
  RUN_STATUS_UNKNOWN = "unknown"
13
14
 
14
15
  RUN_STATUS_CATEGORY_ALIASES = {
@@ -36,7 +37,9 @@ RUN_STATUS_CATEGORY_ALIASES = {
36
37
  "rate_limit": RUN_STATUS_INFRA_ERROR,
37
38
  "rate_limited": RUN_STATUS_INFRA_ERROR,
38
39
  "setup_error": RUN_STATUS_INFRA_ERROR,
39
- "stale": RUN_STATUS_INFRA_ERROR,
40
+ "stale": RUN_STATUS_STALE,
41
+ "superseded": RUN_STATUS_STALE,
42
+ "superseded_head": RUN_STATUS_STALE,
40
43
  "timeout": RUN_STATUS_INFRA_ERROR,
41
44
  "timed_out": RUN_STATUS_INFRA_ERROR,
42
45
  }
@@ -56,8 +56,8 @@ def render_value_report_text(report: Mapping[str, Any]) -> str:
56
56
  f"Run dispositions: {report.get('run_disposition_count', 0)}",
57
57
  f"Reviewer runs: {report.get('reviewer_run_count', 0)}",
58
58
  "",
59
- "| Reviewer | Runs | Useful | Negative | Useful rate | Known-clean pass | Known-blocked caught/missed | Infra errors | Input gaps | Auto/manual M/MB/FB | Cost | Sec/run | Cost/useful | Policy | Recommended role |",
60
- "| --- | ---: | ---: | ---: | ---: | ---: | --- | ---: | ---: | --- | ---: | ---: | ---: | --- | --- |",
59
+ "| Reviewer | Runs | Useful | Negative | Useful rate | Known-clean pass | Known-blocked caught/missed | Infra errors | Stale | Input gaps | Auto/manual M/MB/FB | Cost | Sec/run | Cost/useful | Policy | Recommended role |",
60
+ "| --- | ---: | ---: | ---: | ---: | ---: | --- | ---: | ---: | ---: | --- | ---: | ---: | ---: | --- | --- |",
61
61
  ]
62
62
  if isinstance(profiles, Mapping) and profiles:
63
63
  for profile_id, stats in sorted(profiles.items()):
@@ -89,6 +89,7 @@ def render_value_report_text(report: Mapping[str, Any]) -> str:
89
89
  str(stats.get("known_clean_pass_runs", 0)),
90
90
  caught_missed,
91
91
  str(stats.get("infra_error_runs", 0)),
92
+ str(stats.get("stale_runs", 0)),
92
93
  str(stats.get("audit_input_insufficient_runs", 0)),
93
94
  auto_manual,
94
95
  str(stats.get("cost_usd", 0)),
@@ -109,7 +110,7 @@ def render_value_report_text(report: Mapping[str, Any]) -> str:
109
110
  + " |"
110
111
  )
111
112
  else:
112
- lines.append("| none | 0 | 0 | 0 | | 0 | 0/0 | 0 | 0 | 0/0/0 | 0 | | | | |")
113
+ lines.append("| none | 0 | 0 | 0 | | 0 | 0/0 | 0 | 0 | 0 | 0/0/0 | 0 | | | | |")
113
114
 
114
115
  recommendations = metrics.get("recommendations", []) if isinstance(metrics, Mapping) else []
115
116
  lines.extend(["", "## Recommendations"])
@@ -188,6 +189,7 @@ def render_value_report_html(report: Mapping[str, Any]) -> str:
188
189
  f"<td>{esc(stats.get('negative_findings', 0))}</td>"
189
190
  f"<td>{esc(stats.get('useful_rate', ''))}</td>"
190
191
  f"<td>{esc(caught_missed)}</td>"
192
+ f"<td>{esc(stats.get('stale_runs', 0))}</td>"
191
193
  f"<td>{esc(auto_manual)}</td>"
192
194
  f"<td>{esc(stats.get('cost_usd', 0))}</td>"
193
195
  f"<td>{esc(stats.get('seconds_per_run', ''))}</td>"
@@ -195,7 +197,7 @@ def render_value_report_html(report: Mapping[str, Any]) -> str:
195
197
  "</tr>"
196
198
  )
197
199
  else:
198
- rows.append("<tr><td colspan='10'>No reviewer rows yet.</td></tr>")
200
+ rows.append("<tr><td colspan='11'>No reviewer rows yet.</td></tr>")
199
201
 
200
202
  recommendation_items = (
201
203
  "\n".join(f"<li>{esc(item)}</li>" for item in recommendations)
@@ -242,7 +244,7 @@ def render_value_report_html(report: Mapping[str, Any]) -> str:
242
244
  " </section>\n"
243
245
  " <section class='card'>\n"
244
246
  " <h2>Reviewer / lens signal</h2>\n"
245
- " <table><thead><tr><th>Reviewer</th><th>Runs</th><th>Useful</th><th>Negative</th><th>Useful rate</th><th>Blocked caught/missed</th><th>Auto/manual</th><th>Cost</th><th>Sec/run</th><th>Role</th></tr></thead>\n"
247
+ " <table><thead><tr><th>Reviewer</th><th>Runs</th><th>Useful</th><th>Negative</th><th>Useful rate</th><th>Blocked caught/missed</th><th>Stale</th><th>Auto/manual</th><th>Cost</th><th>Sec/run</th><th>Role</th></tr></thead>\n"
246
248
  f" <tbody>{''.join(rows)}</tbody></table>\n"
247
249
  " </section>\n"
248
250
  " <section class='card'>\n"