claude-dev-env 1.95.0 → 2.0.1

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 (241) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +20 -20
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +15 -12
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +11 -10
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +25 -19
  6. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
  7. package/_shared/advisor/scripts/tier_model_ids.py +18 -18
  8. package/_shared/pr-loop/CLAUDE.md +1 -0
  9. package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
  10. package/_shared/pr-loop/scripts/README.md +1 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
  13. package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
  14. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
  15. package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
  16. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
  17. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
  23. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
  24. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
  25. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
  26. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
  27. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
  28. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
  29. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
  30. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
  31. package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
  32. package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
  33. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
  34. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
  35. package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
  36. package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
  37. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
  38. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
  39. package/_shared/pr-loop/worker-spawn.md +186 -0
  40. package/agents/code-verifier.md +1 -1
  41. package/bin/ever-shipped-skills.mjs +3 -0
  42. package/bin/expand_home_directory_tokens.mjs +1 -1
  43. package/bin/install.mjs +5 -2
  44. package/hooks/advisory/refactor_guard.py +3 -4
  45. package/hooks/blocking/CLAUDE.md +7 -1
  46. package/hooks/blocking/block_main_commit.py +2 -2
  47. package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
  48. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
  49. package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
  50. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
  52. package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
  53. package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
  54. package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
  55. package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
  56. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
  59. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
  60. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
  61. package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
  62. package/hooks/blocking/code_rules_mock_completeness.py +1 -1
  63. package/hooks/blocking/code_rules_optional_params.py +2 -2
  64. package/hooks/blocking/code_rules_shared.py +1 -1
  65. package/hooks/blocking/code_rules_test_assertions.py +1 -1
  66. package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
  67. package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
  68. package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
  69. package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
  70. package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
  71. package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
  72. package/hooks/blocking/inventory_intent_records/records.py +271 -0
  73. package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
  74. package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
  75. package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
  76. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
  77. package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
  78. package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
  79. package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
  80. package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
  81. package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
  82. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
  83. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
  84. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
  85. package/hooks/blocking/pii_payload_scan.py +138 -42
  86. package/hooks/blocking/pii_prevention_blocker.py +185 -291
  87. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
  88. package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
  89. package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
  90. package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
  91. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
  92. package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
  93. package/hooks/blocking/pr_description_command_parser.py +8 -4
  94. package/hooks/blocking/precommit_code_rules_gate.py +3 -3
  95. package/hooks/blocking/tdd_enforcer.py +97 -608
  96. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
  97. package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
  98. package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
  99. package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
  100. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
  101. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
  102. package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
  103. package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
  104. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
  105. package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
  106. package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
  107. package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
  108. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
  109. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
  110. package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
  111. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
  112. package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
  113. package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
  114. package/hooks/blocking/test_pii_payload_scan.py +168 -0
  115. package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
  116. package/hooks/blocking/test_verifier_verdict_minter.py +55 -158
  117. package/hooks/blocking/tests/conftest.py +10 -0
  118. package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
  119. package/hooks/blocking/tests/test_repository_exemption.py +105 -0
  120. package/hooks/blocking/tests/test_repository_resolution.py +108 -0
  121. package/hooks/diagnostic/hook_log_extractor.py +12 -10
  122. package/hooks/git-hooks/post_commit.py +3 -4
  123. package/hooks/hooks_constants/CLAUDE.md +2 -2
  124. package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
  125. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  126. package/hooks/hooks_constants/local_identity.py +59 -8
  127. package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
  128. package/hooks/hooks_constants/test_local_identity.py +105 -3
  129. package/hooks/pyproject.toml +13 -36
  130. package/hooks/session/plugin_data_dir_cleanup.py +0 -1
  131. package/hooks/validation/mypy_validator.py +2 -2
  132. package/hooks/validators/health_check.py +1 -0
  133. package/hooks/validators/mypy_integration.py +2 -0
  134. package/hooks/validators/ruff_integration.py +3 -0
  135. package/hooks/workflow/auto_formatter.py +5 -4
  136. package/package.json +1 -1
  137. package/scripts/CLAUDE.md +4 -0
  138. package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
  139. package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
  140. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
  141. package/scripts/dev_env_scripts_constants/timing.py +7 -1
  142. package/scripts/grok_headless_runner.py +294 -0
  143. package/scripts/grok_worker_preflight.py +410 -0
  144. package/scripts/invoke_code_review.py +463 -0
  145. package/scripts/resolve_worker_spawn.py +619 -0
  146. package/scripts/spawn_grok_batch.py +672 -0
  147. package/scripts/test_grok_headless_runner.py +626 -0
  148. package/scripts/test_grok_worker_preflight.py +1054 -0
  149. package/scripts/test_invoke_code_review.py +672 -0
  150. package/scripts/test_resolve_worker_spawn.py +1014 -0
  151. package/scripts/test_spawn_grok_batch.py +1017 -0
  152. package/skills/CLAUDE.md +5 -3
  153. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
  154. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
  155. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
  156. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
  157. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
  158. package/skills/autoconverge/CLAUDE.md +3 -3
  159. package/skills/autoconverge/SKILL.md +9 -3
  160. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  161. package/skills/autoconverge/reference/convergence.md +33 -11
  162. package/skills/autoconverge/reference/stop-conditions.md +16 -5
  163. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  164. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
  165. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
  166. package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
  167. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
  168. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  169. package/skills/autoconverge/workflow/converge.mjs +200 -16
  170. package/skills/bugteam/CLAUDE.md +2 -2
  171. package/skills/bugteam/CONSTRAINTS.md +3 -2
  172. package/skills/bugteam/PROMPTS.md +7 -6
  173. package/skills/bugteam/SKILL.md +18 -13
  174. package/skills/bugteam/reference/audit-and-teammates.md +215 -35
  175. package/skills/bugteam/reference/design-rationale.md +1 -1
  176. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
  177. package/skills/bugteam/reference/team-setup.md +8 -2
  178. package/skills/codex-review/CLAUDE.md +46 -0
  179. package/skills/codex-review/SKILL.md +181 -0
  180. package/skills/codex-review/reference/CLAUDE.md +15 -0
  181. package/skills/codex-review/reference/cli-contract.md +253 -0
  182. package/skills/codex-review/reference/loop-integration.md +118 -0
  183. package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
  184. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
  185. package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
  186. package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  187. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
  188. package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  189. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
  190. package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
  191. package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  192. package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  193. package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  194. package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  195. package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
  196. package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  197. package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  198. package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  199. package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
  200. package/skills/codex-review/scripts/run_codex_review.py +415 -0
  201. package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
  202. package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
  203. package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
  204. package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
  205. package/skills/codex-review/test_skill_scaffold.py +192 -0
  206. package/skills/grok-spawn/CLAUDE.md +28 -0
  207. package/skills/grok-spawn/SKILL.md +226 -0
  208. package/skills/grok-spawn/reference/flag-profiles.md +132 -0
  209. package/skills/grok-spawn/reference/worker-briefs.md +152 -0
  210. package/skills/grokify/SKILL.md +9 -1
  211. package/skills/grokify/capability-claims.test.mjs +28 -0
  212. package/skills/grokify/evals/README.md +72 -0
  213. package/skills/grokify/evals/parse-payload.test.mjs +171 -0
  214. package/skills/grokify/evals/run-capability-evals.mjs +545 -0
  215. package/skills/orchestrator/SKILL.md +42 -29
  216. package/skills/orchestrator-refresh/SKILL.md +17 -9
  217. package/skills/pr-converge/SKILL.md +34 -13
  218. package/skills/pr-converge/reference/convergence-gates.md +42 -15
  219. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  220. package/skills/pr-converge/reference/ground-rules.md +1 -1
  221. package/skills/pr-converge/reference/per-tick.md +130 -42
  222. package/skills/pr-converge/reference/state-schema.md +10 -0
  223. package/skills/pr-converge/scripts/CLAUDE.md +2 -0
  224. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
  225. package/skills/pr-converge/scripts/check_convergence.py +605 -29
  226. package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
  227. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
  228. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
  229. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
  230. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
  231. package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
  232. package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
  233. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
  234. package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
  235. package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
  236. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
  237. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
  238. package/skills/pr-converge/test_step5_host_branch.py +106 -0
  239. package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
  240. package/skills/reviewer-gates/SKILL.md +7 -5
  241. package/skills/team-advisor/SKILL.md +7 -7
@@ -2,8 +2,12 @@
2
2
 
3
3
  ::
4
4
 
5
- check_all(all gates passing) -> exit 0, "All pre-conditions met ..."
6
- check_all(one gate failing) -> exit 1, "One or more ... do not mark ready."
5
+ check_all(all gates passing,
6
+ is_bugteam_post_blocked=False,
7
+ ) -> exit 0, "All pre-conditions met ..."
8
+ check_all(one gate failing,
9
+ is_bugteam_post_blocked=False,
10
+ ) -> exit 1, "One or more ... do not mark ready."
7
11
  _get_pr_head_sha raises SystemExit -> exit 2 propagates
8
12
  parse_arguments([... --bugteam-post-blocked]) -> bugteam_post_blocked True
9
13
 
@@ -22,6 +26,7 @@ from types import ModuleType
22
26
  import pytest
23
27
 
24
28
  import _pr_converge_path_setup # noqa: F401
29
+ import check_convergence_availability
25
30
  from pr_converge_skill_constants.constants import EXIT_CODE_GH_ERROR
26
31
 
27
32
  _SCRIPTS_DIRECTORY = Path(__file__).absolute().parent
@@ -45,9 +50,10 @@ EXPECTED_ALL_PASS_STDOUT = (
45
50
  "2. bugbot review body clean: PASS — clean\n"
46
51
  "3. bugteam_clean_at == current_head: PASS — clean\n"
47
52
  "4. copilot_clean_at == current_head: PASS — clean\n"
48
- "5. zero unresolved bot threads: PASS — clean\n"
49
- "6. PR is mergeable: PASS — clean\n"
50
- "7. no pending requested reviews: PASS — clean\n\n"
53
+ "5. codex_clean_at == current_head: PASS — skipped (codex review not required)\n"
54
+ "6. zero unresolved bot threads: PASS — clean\n"
55
+ "7. PR is mergeable: PASS — clean\n"
56
+ "8. no pending requested reviews: PASS — clean\n\n"
51
57
  "All pre-conditions met — PR is ready to mark ready.\n"
52
58
  )
53
59
 
@@ -56,9 +62,10 @@ EXPECTED_BUGBOT_DOWN_STDOUT = (
56
62
  "1. bugbot_clean_at == current_head: PASS — bypassed (bugbot_down)\n"
57
63
  "2. bugteam_clean_at == current_head: PASS — clean\n"
58
64
  "3. copilot_clean_at == current_head: PASS — clean\n"
59
- "4. zero unresolved bot threads: PASS — clean\n"
60
- "5. PR is mergeable: PASS — clean\n"
61
- "6. no pending requested reviews: PASS — clean\n\n"
65
+ "4. codex_clean_at == current_head: PASS — skipped (codex review not required)\n"
66
+ "5. zero unresolved bot threads: PASS — clean\n"
67
+ "6. PR is mergeable: PASS — clean\n"
68
+ "7. no pending requested reviews: PASS — clean\n\n"
62
69
  "All pre-conditions met — PR is ready to mark ready.\n"
63
70
  )
64
71
 
@@ -68,9 +75,10 @@ EXPECTED_COPILOT_DOWN_STDOUT = (
68
75
  "2. bugbot review body clean: PASS — clean\n"
69
76
  "3. bugteam_clean_at == current_head: PASS — clean\n"
70
77
  "4. copilot_clean_at == current_head: PASS — bypassed (copilot_down)\n"
71
- "5. zero unresolved bot threads: PASS — clean\n"
72
- "6. PR is mergeable: PASS — clean\n"
73
- "7. no pending requested reviews: PASS — bypassed (copilot_down)\n\n"
78
+ "5. codex_clean_at == current_head: PASS — skipped (codex review not required)\n"
79
+ "6. zero unresolved bot threads: PASS — clean\n"
80
+ "7. PR is mergeable: PASS — clean\n"
81
+ "8. no pending requested reviews: PASS — bypassed (copilot_down)\n\n"
74
82
  "All pre-conditions met — PR is ready to mark ready.\n"
75
83
  )
76
84
 
@@ -110,8 +118,15 @@ def _refuse_bugteam_clean(**_call_keywords: object) -> tuple[bool, str]:
110
118
  )
111
119
 
112
120
 
121
+ def _null_codex_percent_left() -> float | None:
122
+ return None
123
+
124
+
113
125
  def _patch_gates_clean(monkeypatch: pytest.MonkeyPatch) -> None:
114
126
  monkeypatch.setattr(check_convergence, "_get_pr_head_sha", _clean_head)
127
+ monkeypatch.setattr(
128
+ check_convergence, "_probe_codex_percent_left", _null_codex_percent_left
129
+ )
115
130
  for each_gate_name in _ALL_LEAF_GATE_NAMES:
116
131
  monkeypatch.setattr(check_convergence, each_gate_name, _clean_gate)
117
132
 
@@ -122,7 +137,7 @@ def test_all_pass_stdout_and_exit_code_match_the_pinned_contract(
122
137
  _patch_gates_clean(monkeypatch)
123
138
  exit_code = check_convergence.check_all(
124
139
  owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=False
125
- )
140
+ , is_bugteam_post_blocked=False, is_codex_down=False, live_codex_clean_at=None)
126
141
  assert capsys.readouterr().out == EXPECTED_ALL_PASS_STDOUT
127
142
  assert exit_code == 0
128
143
 
@@ -133,7 +148,7 @@ def test_bugbot_down_bypass_line_matches_the_pinned_contract(
133
148
  _patch_gates_clean(monkeypatch)
134
149
  exit_code = check_convergence.check_all(
135
150
  owner="o", repo="r", number=1, is_bugbot_down=True, is_copilot_down=False
136
- )
151
+ , is_bugteam_post_blocked=False, is_codex_down=False, live_codex_clean_at=None)
137
152
  assert capsys.readouterr().out == EXPECTED_BUGBOT_DOWN_STDOUT
138
153
  assert exit_code == 0
139
154
 
@@ -144,7 +159,7 @@ def test_copilot_down_bypass_lines_match_the_pinned_contract(
144
159
  _patch_gates_clean(monkeypatch)
145
160
  exit_code = check_convergence.check_all(
146
161
  owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=True
147
- )
162
+ , is_bugteam_post_blocked=False, is_codex_down=False, live_codex_clean_at=None)
148
163
  assert capsys.readouterr().out == EXPECTED_COPILOT_DOWN_STDOUT
149
164
  assert exit_code == 0
150
165
 
@@ -160,9 +175,9 @@ def test_a_failing_gate_yields_exit_one_and_the_fail_summary(
160
175
  monkeypatch.setattr(check_convergence, "_get_mergeable", _blocked_mergeable)
161
176
  exit_code = check_convergence.check_all(
162
177
  owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=False
163
- )
178
+ , is_bugteam_post_blocked=False, is_codex_down=False, live_codex_clean_at=None)
164
179
  captured_stdout = capsys.readouterr().out
165
- assert "6. PR is mergeable: FAIL — blocked\n" in captured_stdout
180
+ assert "7. PR is mergeable: FAIL — blocked\n" in captured_stdout
166
181
  assert captured_stdout.endswith(EXPECTED_FAIL_SUMMARY)
167
182
  assert exit_code == 1
168
183
 
@@ -177,7 +192,7 @@ def test_gh_error_exit_code_is_two_and_propagates_from_head_fetch(
177
192
  with pytest.raises(SystemExit) as raised:
178
193
  check_convergence.check_all(
179
194
  owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=False
180
- )
195
+ , is_bugteam_post_blocked=False, is_codex_down=False, live_codex_clean_at=None)
181
196
  assert EXIT_CODE_GH_ERROR == 2
182
197
  assert raised.value.code == 2
183
198
 
@@ -216,6 +231,8 @@ def test_skip_bugteam_gate_when_bugteam_post_blocked_is_true(
216
231
  is_bugbot_down=False,
217
232
  is_copilot_down=False,
218
233
  is_bugteam_post_blocked=True,
234
+ is_codex_down=False,
235
+ live_codex_clean_at=None,
219
236
  )
220
237
  assert "bypassed (bugteam_post_blocked)" in capsys.readouterr().out
221
238
  assert exit_code == 0
@@ -241,6 +258,8 @@ def test_run_bugteam_gate_when_bugteam_post_blocked_is_false(
241
258
  is_bugbot_down=False,
242
259
  is_copilot_down=False,
243
260
  is_bugteam_post_blocked=False,
261
+ is_codex_down=False,
262
+ live_codex_clean_at=None,
244
263
  )
245
264
  assert "_check_bugteam_clean" in invoked_gate_names
246
265
  assert "bypassed (bugteam_post_blocked)" not in capsys.readouterr().out
@@ -311,6 +330,9 @@ def test_main_derives_bugteam_post_blocked_from_env_when_flag_omitted(
311
330
  is_bugbot_down: bool,
312
331
  is_copilot_down: bool,
313
332
  is_bugteam_post_blocked: bool = False,
333
+ is_codex_down: bool = False,
334
+ live_codex_clean_at: str | None = None,
335
+ **_bypass_note_kwargs: str,
314
336
  ) -> int:
315
337
  captured_bugteam_post_blocked.append(is_bugteam_post_blocked)
316
338
  return 0
@@ -319,3 +341,53 @@ def test_main_derives_bugteam_post_blocked_from_env_when_flag_omitted(
319
341
  exit_code = check_convergence.main(["--owner", "o", "--repo", "r", "--pr-number", "1"])
320
342
  assert exit_code == 0
321
343
  assert captured_bugteam_post_blocked == [True]
344
+
345
+
346
+ def test_copilot_probe_error_still_runs_live_gates(
347
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
348
+ ) -> None:
349
+ """A probe error must not hard-fail; live GitHub gates decide pass/fail."""
350
+ _patch_gates_clean(monkeypatch)
351
+ live_copilot_calls: list[str] = []
352
+ live_pending_calls: list[str] = []
353
+
354
+ def _recording_bot_review(**_keywords: object) -> tuple[bool, str]:
355
+ live_copilot_calls.append("copilot")
356
+ return True, "clean"
357
+
358
+ def _recording_pending(**_keywords: object) -> tuple[bool, str]:
359
+ live_pending_calls.append("pending")
360
+ return True, "clean"
361
+
362
+ monkeypatch.setattr(check_convergence, "_check_bot_review", _recording_bot_review)
363
+ monkeypatch.setattr(
364
+ check_convergence, "_check_no_pending_reviews", _recording_pending
365
+ )
366
+ monkeypatch.setattr(
367
+ check_convergence,
368
+ "_resolve_copilot_waiver",
369
+ lambda _flag: check_convergence_availability.ReviewerWaiver(
370
+ False, "", "quota API down"
371
+ ),
372
+ )
373
+ monkeypatch.setattr(
374
+ check_convergence,
375
+ "_resolve_bugbot_waiver",
376
+ lambda _flag: check_convergence_availability.ReviewerWaiver(True, "bugbot_down", ""),
377
+ )
378
+ exit_code = check_convergence.main(
379
+ [
380
+ "--owner",
381
+ "o",
382
+ "--repo",
383
+ "r",
384
+ "--pr-number",
385
+ "1",
386
+ "--bugbot-down",
387
+ ]
388
+ )
389
+ captured = capsys.readouterr().out
390
+ assert live_copilot_calls == ["copilot"]
391
+ assert live_pending_calls == ["pending"]
392
+ assert "enforced (probe error" not in captured
393
+ assert exit_code == 0
@@ -0,0 +1,179 @@
1
+ """Fixture-mode tests for check_convergence --fixture replay."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+ from types import ModuleType
10
+
11
+ import pytest
12
+
13
+ import _pr_converge_path_setup # noqa: F401
14
+
15
+ _SCRIPTS_DIRECTORY = Path(__file__).absolute().parent
16
+ _PR_CONVERGE_DIRECTORY = _SCRIPTS_DIRECTORY.parent
17
+
18
+ CLEAN_LABEL = "Clean \u2014 no findings"
19
+ CLEAN_BODY = (
20
+ f"**Bugteam audit completed** \u2014 {CLEAN_LABEL}\n\n"
21
+ "No findings.\n"
22
+ )
23
+ HEAD_SHA = "ae8005aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
24
+
25
+
26
+ def _load_module() -> ModuleType:
27
+ for each_cached_name in [
28
+ each_key
29
+ for each_key in list(sys.modules)
30
+ if each_key == "config" or each_key.startswith("config.")
31
+ ]:
32
+ sys.modules.pop(each_cached_name, None)
33
+ if str(_PR_CONVERGE_DIRECTORY) in sys.path:
34
+ sys.path.remove(str(_PR_CONVERGE_DIRECTORY))
35
+ sys.path.insert(0, str(_PR_CONVERGE_DIRECTORY))
36
+ module_path = _SCRIPTS_DIRECTORY / "check_convergence.py"
37
+ spec = importlib.util.spec_from_file_location(
38
+ "check_convergence_fixture_under_test", module_path
39
+ )
40
+ assert spec is not None
41
+ assert spec.loader is not None
42
+ module = importlib.util.module_from_spec(spec)
43
+ spec.loader.exec_module(module)
44
+ return module
45
+
46
+
47
+ check_convergence = _load_module()
48
+
49
+
50
+ def _write_passing_fixture(tmp_path: Path) -> Path:
51
+ fixture_path = tmp_path / "pr-at-merge.json"
52
+ payload = {
53
+ "head_sha": HEAD_SHA,
54
+ "pr_object": {"mergeable": True, "mergeable_state": "clean"},
55
+ "reviews": [
56
+ {
57
+ "id": 1,
58
+ "body": CLEAN_BODY,
59
+ "commit_id": HEAD_SHA,
60
+ }
61
+ ],
62
+ "unresolved_bot_threads_passed": True,
63
+ "unresolved_bot_threads_detail": "0 unresolved",
64
+ "pending_reviews_passed": True,
65
+ "pending_reviews_detail": "none pending",
66
+ }
67
+ fixture_path.write_text(json.dumps(payload), encoding="utf-8")
68
+ return fixture_path
69
+
70
+
71
+ def should_exit_zero_from_fixture_with_bugbot_and_copilot_down(
72
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
73
+ ) -> None:
74
+ fixture_path = _write_passing_fixture(tmp_path)
75
+ exit_code = check_convergence.main(
76
+ [
77
+ "--owner",
78
+ "jl-cmd",
79
+ "--repo",
80
+ "claude-dev-env",
81
+ "--pr-number",
82
+ "53",
83
+ "--fixture",
84
+ str(fixture_path),
85
+ "--bugbot-down",
86
+ "--copilot-down",
87
+ ]
88
+ )
89
+ captured = capsys.readouterr().out
90
+ assert exit_code == 0
91
+ assert "All pre-conditions met" in captured
92
+ assert "bypassed (bugbot_down)" in captured
93
+ assert "bypassed (copilot_down)" in captured
94
+ assert "clean bugteam audit" in captured
95
+ assert "PR is mergeable: PASS" in captured
96
+
97
+
98
+ def should_exit_one_from_fixture_when_bugteam_clean_missing(
99
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
100
+ ) -> None:
101
+ fixture_path = tmp_path / "missing-bugteam.json"
102
+ payload = {
103
+ "head_sha": HEAD_SHA,
104
+ "pr_object": {"mergeable": True, "mergeable_state": "clean"},
105
+ "reviews": [],
106
+ "unresolved_bot_threads_passed": True,
107
+ "unresolved_bot_threads_detail": "0 unresolved",
108
+ "pending_reviews_passed": True,
109
+ "pending_reviews_detail": "none pending",
110
+ }
111
+ fixture_path.write_text(json.dumps(payload), encoding="utf-8")
112
+ exit_code = check_convergence.main(
113
+ [
114
+ "--owner",
115
+ "jl-cmd",
116
+ "--repo",
117
+ "claude-dev-env",
118
+ "--pr-number",
119
+ "80",
120
+ "--fixture",
121
+ str(fixture_path),
122
+ "--bugbot-down",
123
+ "--copilot-down",
124
+ ]
125
+ )
126
+ captured = capsys.readouterr().out
127
+ assert exit_code == 1
128
+ assert "no bugteam review found" in captured
129
+ assert "do not mark ready" in captured
130
+
131
+
132
+ def should_parse_fixture_flag() -> None:
133
+ arguments = check_convergence.parse_arguments(
134
+ [
135
+ "--owner",
136
+ "o",
137
+ "--repo",
138
+ "r",
139
+ "--pr-number",
140
+ "1",
141
+ "--fixture",
142
+ "snap.json",
143
+ "--bugbot-down",
144
+ "--copilot-down",
145
+ ]
146
+ )
147
+ assert arguments.fixture == "snap.json"
148
+ assert arguments.bugbot_down is True
149
+ assert arguments.copilot_down is True
150
+
151
+
152
+ def should_evaluate_bugteam_clean_from_reviews_on_clean_body() -> None:
153
+ head = HEAD_SHA
154
+ passed, detail = check_convergence._evaluate_bugteam_clean_from_reviews(
155
+ [{"id": 9, "body": CLEAN_BODY, "commit_id": head}],
156
+ head,
157
+ )
158
+ assert passed is True
159
+ assert "clean bugteam audit" in detail
160
+
161
+
162
+ def should_load_convergence_fixture_from_disk(tmp_path: Path) -> None:
163
+ fixture_path = _write_passing_fixture(tmp_path)
164
+ fixture = check_convergence._load_convergence_fixture(fixture_path)
165
+ assert fixture.head_sha == HEAD_SHA
166
+ assert fixture.pr_object["mergeable"] is True
167
+ assert len(fixture.reviews) == 1
168
+
169
+
170
+ def should_reject_fixture_missing_safety_gate_flags(tmp_path: Path) -> None:
171
+ fixture_path = tmp_path / "missing-safety-flags.json"
172
+ payload = {
173
+ "head_sha": HEAD_SHA,
174
+ "pr_object": {"mergeable": True, "mergeable_state": "clean"},
175
+ "reviews": [{"id": 1, "body": CLEAN_BODY, "commit_id": HEAD_SHA}],
176
+ }
177
+ fixture_path.write_text(json.dumps(payload), encoding="utf-8")
178
+ with pytest.raises(ValueError):
179
+ check_convergence._load_convergence_fixture(fixture_path)
@@ -1,68 +1,84 @@
1
- """Behavioral tests for the review and Bugbot convergence gate leaves.
2
-
3
- ::
4
-
5
- _flatten_paginated_reviews(two pages) -> newest-first flat list
6
- _bugbot_run_conclusion_detail(success) -> (True, "check run ...")
7
- _check_bugbot(named complete run) -> (True, "check run ...")
8
-
9
- Each test drives the real leaf, stubbing only the ``gh`` transport helpers.
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- import json
15
-
16
- import pytest
17
-
18
- import check_convergence_gates as gates
19
-
20
-
21
- def test_flatten_paginated_reviews_flattens_and_sorts_newest_first() -> None:
22
- pages = [
23
- [{"id": 1, "submitted_at": "2026-01-01T00:00:00Z"}],
24
- [{"id": 2, "submitted_at": "2026-03-01T00:00:00Z"}],
25
- ]
26
- flattened = gates._flatten_paginated_reviews(json.dumps(pages))
27
- assert flattened is not None
28
- assert [each_review["id"] for each_review in flattened] == [2, 1]
29
-
30
-
31
- def test_flatten_paginated_reviews_returns_none_for_non_list_payload() -> None:
32
- assert (
33
- gates._flatten_paginated_reviews(json.dumps({"message": "Not Found"})) is None
34
- )
35
-
36
-
37
- def test_bugbot_run_conclusion_detail_passes_on_a_complete_conclusion() -> None:
38
- complete_conclusion = gates.ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS[0]
39
- passed, detail = gates._bugbot_run_conclusion_detail(
40
- {"id": 55, "conclusion": complete_conclusion, "html_url": ""}
41
- )
42
- assert passed is True
43
- assert "check run 55" in detail
44
-
45
-
46
- def test_check_bugbot_reports_success_when_the_named_run_is_complete(
47
- monkeypatch: pytest.MonkeyPatch,
48
- ) -> None:
49
- complete_conclusion = gates.ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS[0]
50
- run_name = f"x {gates.BUGBOT_CHECK_RUN_NAME_SUBSTRING} y"
51
- payload = {
52
- "check_runs": [
53
- {
54
- "name": run_name,
55
- "id": 9,
56
- "conclusion": complete_conclusion,
57
- "html_url": "",
58
- }
59
- ]
60
- }
61
-
62
- def _stub_gh_api(endpoint_path: str) -> tuple[int, str]:
63
- return 0, json.dumps(payload)
64
-
65
- monkeypatch.setattr(gates, "_gh_api", _stub_gh_api)
66
- passed, detail = gates._check_bugbot(owner="o", repo="r", sha="abc")
67
- assert passed is True
68
- assert "check run 9" in detail
1
+ """Behavioral tests for the review and Bugbot convergence gate leaves.
2
+
3
+ ::
4
+
5
+ _flatten_paginated_reviews(two pages) -> newest-first flat list
6
+ _bugbot_run_conclusion_detail(success) -> (True, "check run ...")
7
+ _check_bugbot(named complete run) -> (True, "check run ...")
8
+
9
+ Each test drives the real leaf, stubbing only the ``gh`` transport helpers.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+
16
+ import pytest
17
+
18
+ import check_convergence_gates as gates
19
+
20
+
21
+ def test_flatten_paginated_reviews_flattens_and_sorts_newest_first() -> None:
22
+ pages = [
23
+ [{"id": 1, "submitted_at": "2026-01-01T00:00:00Z"}],
24
+ [{"id": 2, "submitted_at": "2026-03-01T00:00:00Z"}],
25
+ ]
26
+ flattened = gates._flatten_paginated_reviews(json.dumps(pages))
27
+ assert flattened is not None
28
+ assert [each_review["id"] for each_review in flattened] == [2, 1]
29
+
30
+
31
+ def test_flatten_paginated_reviews_returns_none_for_non_list_payload() -> None:
32
+ assert (
33
+ gates._flatten_paginated_reviews(json.dumps({"message": "Not Found"})) is None
34
+ )
35
+
36
+
37
+ def test_bugbot_run_conclusion_detail_passes_on_a_complete_conclusion() -> None:
38
+ complete_conclusion = gates.ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS[0]
39
+ passed, detail = gates._bugbot_run_conclusion_detail(
40
+ {"id": 55, "conclusion": complete_conclusion, "html_url": ""}
41
+ )
42
+ assert passed is True
43
+ assert "check run 55" in detail
44
+
45
+
46
+ def test_check_bugbot_reports_success_when_the_named_run_is_complete(
47
+ monkeypatch: pytest.MonkeyPatch,
48
+ ) -> None:
49
+ complete_conclusion = gates.ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS[0]
50
+ run_name = f"x {gates.BUGBOT_CHECK_RUN_NAME_SUBSTRING} y"
51
+ payload = {
52
+ "check_runs": [
53
+ {
54
+ "name": run_name,
55
+ "id": 9,
56
+ "conclusion": complete_conclusion,
57
+ "html_url": "",
58
+ }
59
+ ]
60
+ }
61
+
62
+ def _stub_gh_api(endpoint_path: str) -> tuple[int, str]:
63
+ return 0, json.dumps(payload)
64
+
65
+ monkeypatch.setattr(gates, "_gh_api", _stub_gh_api)
66
+ passed, detail = gates._check_bugbot(owner="o", repo="r", sha="abc")
67
+ assert passed is True
68
+ assert "check run 9" in detail
69
+
70
+
71
+ def should_evaluate_mergeable_from_pr_object_clean() -> None:
72
+ passed, detail = gates._evaluate_mergeable_from_pr_object(
73
+ {"mergeable": True, "mergeable_state": "clean"}
74
+ )
75
+ assert passed is True
76
+ assert detail == "clean"
77
+
78
+
79
+ def should_evaluate_mergeable_from_pr_object_unknown() -> None:
80
+ passed, detail = gates._evaluate_mergeable_from_pr_object(
81
+ {"mergeable": None, "mergeable_state": "unknown"}
82
+ )
83
+ assert passed is False
84
+ assert detail == "unknown"
@@ -67,6 +67,30 @@ def test_count_unresolved_bot_threads_counts_a_single_cursor_thread(
67
67
  assert "src/app.py" in detail
68
68
 
69
69
 
70
+ def test_check_no_pending_reviews_fails_closed_on_unparseable_response(
71
+ monkeypatch: pytest.MonkeyPatch,
72
+ ) -> None:
73
+ def _stub_gh_api(endpoint_path: str) -> tuple[int, str]:
74
+ return 0, "not json at all"
75
+
76
+ monkeypatch.setattr(thread_gates, "_gh_api", _stub_gh_api)
77
+ passed, detail = thread_gates._check_no_pending_reviews(owner="o", repo="r", number=1)
78
+ assert passed is False
79
+ assert "not valid JSON" in detail
80
+
81
+
82
+ def test_check_no_pending_reviews_passes_when_no_copilot_requested(
83
+ monkeypatch: pytest.MonkeyPatch,
84
+ ) -> None:
85
+ def _stub_gh_api(endpoint_path: str) -> tuple[int, str]:
86
+ return 0, json.dumps({"users": [], "teams": []})
87
+
88
+ monkeypatch.setattr(thread_gates, "_gh_api", _stub_gh_api)
89
+ passed, detail = thread_gates._check_no_pending_reviews(owner="o", repo="r", number=1)
90
+ assert passed is True
91
+ assert "no pending reviewers" in detail
92
+
93
+
70
94
  def test_format_unresolved_detail_bounds_the_listed_paths() -> None:
71
95
  over_limit = thread_gates.UNRESOLVED_THREAD_DETAIL_MAX + 2
72
96
  all_threads = [{"path": f"file{each_index}.py"} for each_index in range(over_limit)]
@@ -0,0 +1,106 @@
1
+ """Contract checks for Step 5 host-aware CODE_REVIEW wiring.
2
+
3
+ Issue #103: Step 5 names ``invoke_code_review.py``, the mode decision inputs
4
+ (host profile, session model), stdin/cwd rules, and the clean/dirty branches
5
+ without changing ``code_review_clean_at`` semantics.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+
13
+ SKILL_ROOT = Path(__file__).resolve().parent
14
+ SKILL_MARKDOWN_PATH = SKILL_ROOT / "SKILL.md"
15
+ PER_TICK_MARKDOWN_PATH = SKILL_ROOT / "reference" / "per-tick.md"
16
+
17
+ HELPER_SCRIPT_NAME = "invoke_code_review.py"
18
+ SESSION_MODEL_FLAG = "--session-model"
19
+ HOST_PROFILE_PHRASE = "host profile"
20
+ SESSION_MODEL_PHRASE = "session model"
21
+ IN_SESSION_MODE = "in_session"
22
+ CHAIN_MODE = "chain"
23
+ DIRTY_TREE_KEY = "dirty_tree"
24
+ RETURNCODE_KEY = "returncode"
25
+ SERVED_COMMAND_KEY = "served_command"
26
+ SUCCESSFUL_SERVE_PHRASE = "successful serve"
27
+ FAILED_REVIEW_PHRASE = "Failed review"
28
+ CODE_REVIEW_CLEAN_AT = "code_review_clean_at"
29
+ CLEAN_STAMP_HELPER = "is_code_review_clean_stamp_allowed"
30
+ NEVER_COMMITS_PHRASE = "never commits"
31
+ NEVER_PUSHES_PHRASE = "never pushes"
32
+ EMPTY_STDIN_PHRASE = "empty"
33
+ CWD_FLAG = "--cwd"
34
+ OPUS_MODEL = "opus"
35
+ HIGH_EFFORT_SLASH = "/code-review high --fix"
36
+
37
+
38
+ def _read_markdown(markdown_path: Path) -> str:
39
+ return markdown_path.read_text(encoding="utf-8")
40
+
41
+
42
+ def _code_review_section(per_tick_text: str) -> str:
43
+ section_start = per_tick_text.index("### `phase == CODE_REVIEW`")
44
+ section_end = per_tick_text.index("### `phase == BUGTEAM`", section_start)
45
+ return per_tick_text[section_start:section_end]
46
+
47
+
48
+ def _step_five_checklist(skill_text: str) -> str:
49
+ step_start = skill_text.index("**Step 5: CODE-REVIEW")
50
+ step_end = skill_text.index("**Step 6: BUGTEAM", step_start)
51
+ return skill_text[step_start:step_end]
52
+
53
+
54
+ def test_per_tick_code_review_names_helper_and_mode_inputs() -> None:
55
+ code_review_section = _code_review_section(_read_markdown(PER_TICK_MARKDOWN_PATH))
56
+ assert HELPER_SCRIPT_NAME in code_review_section
57
+ assert HOST_PROFILE_PHRASE in code_review_section
58
+ assert SESSION_MODEL_PHRASE in code_review_section
59
+ assert SESSION_MODEL_FLAG in code_review_section
60
+ assert CWD_FLAG in code_review_section
61
+ assert IN_SESSION_MODE in code_review_section
62
+ assert CHAIN_MODE in code_review_section
63
+ assert DIRTY_TREE_KEY in code_review_section
64
+ assert RETURNCODE_KEY in code_review_section
65
+ assert SERVED_COMMAND_KEY in code_review_section
66
+ assert SUCCESSFUL_SERVE_PHRASE in code_review_section
67
+ assert FAILED_REVIEW_PHRASE in code_review_section
68
+ assert CLEAN_STAMP_HELPER in code_review_section
69
+ assert HIGH_EFFORT_SLASH in code_review_section
70
+ assert OPUS_MODEL in code_review_section
71
+ assert NEVER_COMMITS_PHRASE in code_review_section
72
+ assert NEVER_PUSHES_PHRASE in code_review_section
73
+ assert EMPTY_STDIN_PHRASE in code_review_section
74
+ assert CODE_REVIEW_CLEAN_AT in code_review_section
75
+
76
+
77
+ def test_skill_step_five_checklist_names_helper_and_mode_inputs() -> None:
78
+ step_five_checklist = _step_five_checklist(_read_markdown(SKILL_MARKDOWN_PATH))
79
+ assert HELPER_SCRIPT_NAME in step_five_checklist
80
+ assert HOST_PROFILE_PHRASE in step_five_checklist
81
+ assert SESSION_MODEL_PHRASE in step_five_checklist
82
+ assert SESSION_MODEL_FLAG in step_five_checklist
83
+ assert CWD_FLAG in step_five_checklist
84
+ assert IN_SESSION_MODE in step_five_checklist
85
+ assert CHAIN_MODE in step_five_checklist
86
+ assert DIRTY_TREE_KEY in step_five_checklist
87
+ assert RETURNCODE_KEY in step_five_checklist
88
+ assert SERVED_COMMAND_KEY in step_five_checklist
89
+ assert SUCCESSFUL_SERVE_PHRASE in step_five_checklist
90
+ assert HIGH_EFFORT_SLASH in step_five_checklist
91
+ assert OPUS_MODEL in step_five_checklist
92
+ assert NEVER_COMMITS_PHRASE in step_five_checklist
93
+ assert NEVER_PUSHES_PHRASE in step_five_checklist
94
+ assert CODE_REVIEW_CLEAN_AT in step_five_checklist
95
+
96
+
97
+ def test_per_tick_clean_stamp_requires_successful_serve() -> None:
98
+ code_review_section = _code_review_section(_read_markdown(PER_TICK_MARKDOWN_PATH))
99
+ assert FAILED_REVIEW_PHRASE in code_review_section
100
+ assert "returncode != 0" in code_review_section
101
+ assert "null" in code_review_section
102
+ assert SERVED_COMMAND_KEY in code_review_section
103
+ assert "do not set" in code_review_section.lower() or "Do not set" in code_review_section
104
+ assert CODE_REVIEW_CLEAN_AT in code_review_section
105
+ assert SUCCESSFUL_SERVE_PHRASE in code_review_section
106
+ assert DIRTY_TREE_KEY in code_review_section