claude-dev-env 2.12.0 → 2.14.0

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 (399) hide show
  1. package/AGENTS.md +61 -0
  2. package/CLAUDE.md +1 -57
  3. package/_shared/AGENTS.md +15 -0
  4. package/_shared/CLAUDE.md +1 -15
  5. package/_shared/advisor/AGENTS.md +22 -0
  6. package/_shared/advisor/CLAUDE.md +1 -22
  7. package/_shared/pr-loop/AGENTS.md +27 -0
  8. package/_shared/pr-loop/CLAUDE.md +1 -27
  9. package/_shared/pr-loop/scripts/AGENTS.md +35 -0
  10. package/_shared/pr-loop/scripts/CLAUDE.md +1 -35
  11. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/AGENTS.md +42 -0
  13. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +1 -42
  14. package/_shared/pr-loop/scripts/pr_loop_shared_constants/AGENTS.md +26 -0
  15. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -26
  16. package/_shared/pr-loop/scripts/test_code_rules_gate.py +0 -26
  17. package/_shared/pr-loop/scripts/tests/AGENTS.md +44 -0
  18. package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -44
  19. package/_shared/process-tree/AGENTS.md +41 -0
  20. package/_shared/process-tree/CLAUDE.md +1 -41
  21. package/agents/AGENTS.md +31 -0
  22. package/agents/CLAUDE.md +1 -32
  23. package/agents/clean-coder.md +6 -6
  24. package/agents/pr-description-writer.md +11 -81
  25. package/agents/test_agent_frontmatter.py +4 -4
  26. package/audit-rubrics/AGENTS.md +43 -0
  27. package/audit-rubrics/CLAUDE.md +1 -43
  28. package/audit-rubrics/category_rubrics/AGENTS.md +37 -0
  29. package/audit-rubrics/category_rubrics/CLAUDE.md +1 -37
  30. package/audit-rubrics/prompts/AGENTS.md +37 -0
  31. package/audit-rubrics/prompts/CLAUDE.md +1 -37
  32. package/bin/AGENTS.md +112 -0
  33. package/bin/CLAUDE.md +1 -112
  34. package/bin/ever-shipped-skills.mjs +1 -0
  35. package/bin/install-constants.mjs +6 -7
  36. package/bin/install.mjs +71 -13
  37. package/bin/install.prune.test.mjs +26 -0
  38. package/bin/install.settings-defaults.test.mjs +60 -33
  39. package/bin/install.test.mjs +36 -4
  40. package/commands/AGENTS.md +23 -0
  41. package/commands/CLAUDE.md +1 -23
  42. package/commands/hook-log-extract.md +3 -3
  43. package/commands/sr-loop.md +9 -5
  44. package/docs/AGENTS.md +32 -0
  45. package/docs/CLAUDE.md +1 -33
  46. package/docs/CODE_RULES.md +1 -1
  47. package/docs/imagegen.md +22 -0
  48. package/docs/references/AGENTS.md +17 -0
  49. package/docs/references/CLAUDE.md +1 -18
  50. package/hooks/AGENTS.md +31 -0
  51. package/hooks/CLAUDE.md +1 -31
  52. package/hooks/_gh_pr_author_swap_utils.py +14 -0
  53. package/hooks/advisory/AGENTS.md +16 -0
  54. package/hooks/advisory/CLAUDE.md +1 -16
  55. package/hooks/blocking/AGENTS.md +123 -0
  56. package/hooks/blocking/CLAUDE.md +1 -148
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/AGENTS.md +28 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +1 -28
  59. package/hooks/blocking/code_rules_banned_identifiers.py +1 -1
  60. package/hooks/blocking/code_rules_dead_module_constant.py +149 -37
  61. package/hooks/blocking/code_rules_probe_chains.py +6 -2
  62. package/hooks/blocking/config/AGENTS.md +10 -0
  63. package/hooks/blocking/config/CLAUDE.md +1 -22
  64. package/hooks/blocking/conventional_pr_title_gate.py +1 -2
  65. package/hooks/blocking/gh_pr_author_restore.py +7 -2
  66. package/hooks/blocking/intent_only_ending_blocker.py +11 -16
  67. package/hooks/blocking/inventory_intent_records/AGENTS.md +26 -0
  68. package/hooks/blocking/inventory_intent_records/CLAUDE.md +1 -26
  69. package/hooks/blocking/package_inventory_stale_blocker_parts/AGENTS.md +26 -0
  70. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +1 -26
  71. package/hooks/blocking/pii_prevention_blocker_parts/AGENTS.md +24 -0
  72. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +1 -24
  73. package/hooks/blocking/send_user_file_open_locally_blocker.py +1 -1
  74. package/hooks/blocking/stop_dispatcher.py +4 -4
  75. package/hooks/blocking/tdd_enforcer_parts/AGENTS.md +30 -0
  76. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +1 -30
  77. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -3
  78. package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +1 -1
  79. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +171 -0
  80. package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
  81. package/hooks/blocking/test_gh_pr_author_restore.py +2 -1
  82. package/hooks/blocking/test_intent_only_ending_blocker.py +4 -0
  83. package/hooks/blocking/test_pre_tool_use_dispatcher.py +6 -7
  84. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +4 -3
  85. package/hooks/blocking/test_shared_stdin_adoption.py +0 -2
  86. package/hooks/diagnostic/AGENTS.md +43 -0
  87. package/hooks/diagnostic/CLAUDE.md +1 -43
  88. package/hooks/diagnostic/hook_log_extractor.py +4 -38
  89. package/hooks/diagnostic/hook_log_stop_wrapper.py +6 -155
  90. package/hooks/diagnostic/migrations/AGENTS.md +16 -0
  91. package/hooks/diagnostic/migrations/CLAUDE.md +1 -16
  92. package/hooks/diagnostic/queries/AGENTS.md +19 -0
  93. package/hooks/diagnostic/queries/CLAUDE.md +1 -19
  94. package/hooks/diagnostic/test_hook_log_extractor.py +10 -23
  95. package/hooks/diagnostic/test_hook_log_stop_wrapper.py +3 -331
  96. package/hooks/git-hooks/AGENTS.md +31 -0
  97. package/hooks/git-hooks/CLAUDE.md +1 -31
  98. package/hooks/git-hooks/git_hooks_constants/AGENTS.md +21 -0
  99. package/hooks/git-hooks/git_hooks_constants/CLAUDE.md +1 -21
  100. package/hooks/git-hooks/git_hooks_constants/__init__.py +4 -12
  101. package/hooks/git-hooks/pre_push.py +49 -118
  102. package/hooks/git-hooks/test_pre_push.py +111 -137
  103. package/hooks/hooks.json +0 -35
  104. package/hooks/hooks_constants/AGENTS.md +104 -0
  105. package/hooks/hooks_constants/CLAUDE.md +1 -113
  106. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -11
  107. package/hooks/hooks_constants/convergence_branch_constants.py +0 -1
  108. package/hooks/hooks_constants/fable_spawn_gate_constants.py +3 -4
  109. package/hooks/hooks_constants/hook_log_extractor_constants.py +1 -2
  110. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -8
  111. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +1 -6
  112. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
  113. package/hooks/hooks_constants/task_list_loop_starter_constants.py +4 -3
  114. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -26
  115. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -5
  116. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
  117. package/hooks/hooks_constants/working_style_prompt_constants.py +29 -20
  118. package/hooks/lifecycle/AGENTS.md +18 -0
  119. package/hooks/lifecycle/CLAUDE.md +1 -20
  120. package/hooks/observability/AGENTS.md +20 -0
  121. package/hooks/observability/CLAUDE.md +1 -20
  122. package/hooks/session/AGENTS.md +33 -0
  123. package/hooks/session/CLAUDE.md +1 -33
  124. package/hooks/session/gh_pr_author_session_cleanup.py +7 -2
  125. package/hooks/session/task_list_loop_starter.py +3 -3
  126. package/hooks/session/test_gh_pr_author_session_cleanup.py +2 -1
  127. package/hooks/session/test_task_list_loop_starter.py +10 -2
  128. package/hooks/session/test_working_style_prompt.py +9 -4
  129. package/hooks/validation/AGENTS.md +20 -0
  130. package/hooks/validation/CLAUDE.md +1 -20
  131. package/hooks/validators/AGENTS.md +51 -0
  132. package/hooks/validators/CLAUDE.md +1 -51
  133. package/hooks/validators/health_check.py +52 -19
  134. package/hooks/validators/python_style_checks.py +1 -1
  135. package/hooks/validators/test_health_check.py +112 -27
  136. package/hooks/workflow/AGENTS.md +16 -0
  137. package/hooks/workflow/CLAUDE.md +1 -16
  138. package/installable-surfaces.manifest.json +1 -0
  139. package/output-styles/AGENTS.md +15 -0
  140. package/output-styles/CLAUDE.md +1 -15
  141. package/package.json +2 -1
  142. package/rules/AGENTS.md +61 -0
  143. package/rules/CLAUDE.md +1 -63
  144. package/rules/code-standards.md +5 -9
  145. package/rules/doc-prose-cuts.md +1 -1
  146. package/rules/gh-cli-conventions.md +0 -1
  147. package/rules/git-workflow.md +1 -3
  148. package/scripts/AGENTS.md +62 -0
  149. package/scripts/CLAUDE.md +1 -63
  150. package/scripts/_code_review_test_support.py +0 -95
  151. package/scripts/codex_compat_materializer.py +4 -0
  152. package/scripts/dev_env_scripts_constants/AGENTS.md +21 -0
  153. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -21
  154. package/scripts/dev_env_scripts_constants/code_review_constants.py +9 -98
  155. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +1 -44
  156. package/scripts/invoke_code_review.py +8 -451
  157. package/scripts/sync_to_cursor/AGENTS.md +23 -0
  158. package/scripts/sync_to_cursor/CLAUDE.md +1 -23
  159. package/scripts/test_invoke_code_review.py +5 -61
  160. package/scripts/test_invoke_code_review_cli.py +1 -45
  161. package/scripts/test_invoke_code_review_contract.py +1 -118
  162. package/scripts/test_validate_instruction_pairs.py +90 -0
  163. package/scripts/test_verify_installable_package.py +5 -1
  164. package/scripts/tests/AGENTS.md +33 -0
  165. package/scripts/tests/CLAUDE.md +1 -33
  166. package/scripts/tests/test_codex_compat_materializer.py +36 -1
  167. package/scripts/validate_instruction_pairs.py +183 -0
  168. package/settings.json +1 -10
  169. package/skills/AGENTS.md +72 -0
  170. package/skills/CLAUDE.md +1 -64
  171. package/skills/_shared/AGENTS.md +44 -0
  172. package/skills/_shared/CLAUDE.md +1 -44
  173. package/skills/_shared/advisor/AGENTS.md +9 -0
  174. package/skills/_shared/advisor/CLAUDE.md +1 -9
  175. package/skills/_shared/pr-loop/AGENTS.md +50 -0
  176. package/skills/_shared/pr-loop/CLAUDE.md +1 -50
  177. package/skills/_shared/pr-loop/prompts/AGENTS.md +9 -0
  178. package/skills/_shared/pr-loop/prompts/CLAUDE.md +1 -9
  179. package/skills/_shared/pr-loop/scripts/AGENTS.md +35 -0
  180. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -35
  181. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +24 -0
  182. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -24
  183. package/skills/anthropic-plan/AGENTS.md +34 -0
  184. package/skills/anthropic-plan/CLAUDE.md +1 -34
  185. package/skills/anthropic-plan/scripts/AGENTS.md +11 -0
  186. package/skills/anthropic-plan/scripts/CLAUDE.md +1 -11
  187. package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/AGENTS.md +16 -0
  188. package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/CLAUDE.md +1 -16
  189. package/skills/anthropic-plan/templates/AGENTS.md +13 -0
  190. package/skills/anthropic-plan/templates/CLAUDE.md +1 -13
  191. package/skills/anthropic-plan/workflow/AGENTS.md +14 -0
  192. package/skills/anthropic-plan/workflow/CLAUDE.md +1 -14
  193. package/skills/auditing-claude-config/AGENTS.md +21 -0
  194. package/skills/auditing-claude-config/CLAUDE.md +1 -21
  195. package/skills/autoconverge/AGENTS.md +36 -0
  196. package/skills/autoconverge/CLAUDE.md +1 -36
  197. package/skills/autoconverge/reference/AGENTS.md +16 -0
  198. package/skills/autoconverge/reference/CLAUDE.md +1 -16
  199. package/skills/autoconverge/reference/convergence.md +7 -7
  200. package/skills/autoconverge/reference/gotchas.md +3 -10
  201. package/skills/autoconverge/workflow/AGENTS.md +26 -0
  202. package/skills/autoconverge/workflow/CLAUDE.md +1 -26
  203. package/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +16 -0
  204. package/skills/autoconverge/workflow/autoconverge_report_constants/CLAUDE.md +1 -16
  205. package/skills/autoconverge/workflow/converge.contract.test.mjs +53 -1388
  206. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +4 -3
  207. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +33 -493
  208. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +15 -97
  209. package/skills/autoconverge/workflow/converge.mjs +201 -238
  210. package/skills/autoconverge/workflow/converge.precatch.test.mjs +1 -1
  211. package/skills/bugteam/AGENTS.md +30 -0
  212. package/skills/bugteam/CLAUDE.md +1 -30
  213. package/skills/bugteam/CONSTRAINTS.md +1 -1
  214. package/skills/bugteam/SKILL.md +9 -0
  215. package/skills/bugteam/reference/AGENTS.md +20 -0
  216. package/skills/bugteam/reference/CLAUDE.md +1 -20
  217. package/skills/bugteam/reference/audit-and-teammates.md +10 -6
  218. package/skills/bugteam/reference/obstacles/AGENTS.md +24 -0
  219. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -24
  220. package/skills/bugteam/scripts/AGENTS.md +30 -0
  221. package/skills/bugteam/scripts/CLAUDE.md +1 -30
  222. package/skills/bugteam/scripts/bugteam_scripts_constants/AGENTS.md +18 -0
  223. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +1 -18
  224. package/skills/bugteam/test_skill_additions.py +9 -0
  225. package/skills/closeout/reference/handoff-prompt-template.md +1 -1
  226. package/skills/codex-review/AGENTS.md +46 -0
  227. package/skills/codex-review/CLAUDE.md +1 -46
  228. package/skills/codex-review/reference/AGENTS.md +15 -0
  229. package/skills/codex-review/reference/CLAUDE.md +1 -15
  230. package/skills/codex-review/scripts/codex_review_scripts_constants/AGENTS.md +18 -0
  231. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +1 -18
  232. package/skills/codex-review/test_skill_scaffold.py +3 -3
  233. package/skills/comments/SKILL.md +65 -0
  234. package/skills/copilot-review/AGENTS.md +18 -0
  235. package/skills/copilot-review/CLAUDE.md +1 -18
  236. package/skills/descriptions/SKILL.md +51 -0
  237. package/skills/emergencies/SKILL.md +42 -0
  238. package/skills/everything-search/AGENTS.md +17 -0
  239. package/skills/everything-search/CLAUDE.md +1 -17
  240. package/skills/fresh-branch/AGENTS.md +14 -0
  241. package/skills/fresh-branch/CLAUDE.md +1 -14
  242. package/skills/grok-spawn/AGENTS.md +28 -0
  243. package/skills/grok-spawn/CLAUDE.md +1 -28
  244. package/skills/orchestrator/SKILL.md +2 -4
  245. package/skills/plan-to-pr/SKILL.md +6 -7
  246. package/skills/plan-to-pr/reference/final-validation-tasks.md +1 -1
  247. package/skills/plan-to-pr/reference/process-inventory.md +2 -2
  248. package/skills/plan-to-pr/reference/review-loop.md +2 -2
  249. package/skills/plan-to-pr/reference/run-record.schema.json +1 -1
  250. package/skills/plan-to-pr/reference/task-seeds.md +1 -1
  251. package/skills/plan-to-pr/reference/task-ticket.md +3 -4
  252. package/skills/plan-to-pr/scripts/config/constants.py +1 -2
  253. package/skills/plan-to-pr/scripts/test_validate_protocol.py +1 -1
  254. package/skills/plan-to-pr/test_skill_contract.py +2 -2
  255. package/skills/pr-converge/AGENTS.md +32 -0
  256. package/skills/pr-converge/CLAUDE.md +1 -32
  257. package/skills/pr-converge/SKILL.md +13 -16
  258. package/skills/pr-converge/pr_converge_skill_constants/AGENTS.md +26 -0
  259. package/skills/pr-converge/pr_converge_skill_constants/CLAUDE.md +1 -26
  260. package/skills/pr-converge/reference/AGENTS.md +28 -0
  261. package/skills/pr-converge/reference/CLAUDE.md +1 -28
  262. package/skills/pr-converge/reference/obstacles/AGENTS.md +23 -0
  263. package/skills/pr-converge/reference/obstacles/CLAUDE.md +1 -23
  264. package/skills/pr-converge/reference/progress-checklist.md +6 -5
  265. package/skills/pr-converge/reference/state-schema.md +1 -27
  266. package/skills/pr-converge/scripts/AGENTS.md +46 -0
  267. package/skills/pr-converge/scripts/CLAUDE.md +1 -46
  268. package/skills/pr-converge/scripts/pr_converge_scripts_constants/AGENTS.md +18 -0
  269. package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -18
  270. package/skills/pr-converge/workflows/AGENTS.md +16 -0
  271. package/skills/pr-converge/workflows/CLAUDE.md +1 -16
  272. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  273. package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +3 -3
  274. package/skills/pr-loop-lifecycle/SKILL.md +1 -1
  275. package/skills/pr-loop-lifecycle/reference/teardown-publish-permissions.md +1 -1
  276. package/skills/prototype/SKILL.md +4 -4
  277. package/skills/prototype/reference/honest-limitations.md +5 -5
  278. package/skills/prototype/reference/promotion-tasks.md +2 -2
  279. package/skills/prototype/workflows/promotion.md +4 -6
  280. package/skills/rebase/AGENTS.md +32 -0
  281. package/skills/rebase/CLAUDE.md +1 -32
  282. package/skills/recall/AGENTS.md +30 -0
  283. package/skills/recall/CLAUDE.md +1 -30
  284. package/skills/remember/AGENTS.md +31 -0
  285. package/skills/remember/CLAUDE.md +1 -31
  286. package/skills/reviews/SKILL.md +85 -0
  287. package/skills/session-log/AGENTS.md +32 -0
  288. package/skills/session-log/CLAUDE.md +1 -32
  289. package/skills/session-tidy/AGENTS.md +36 -0
  290. package/skills/session-tidy/CLAUDE.md +1 -36
  291. package/skills/skill-builder/AGENTS.md +49 -0
  292. package/skills/skill-builder/CLAUDE.md +1 -49
  293. package/skills/skill-builder/references/AGENTS.md +25 -0
  294. package/skills/skill-builder/references/CLAUDE.md +1 -25
  295. package/skills/skill-builder/references/deterministic-elements.md +1 -1
  296. package/skills/skill-builder/templates/AGENTS.md +14 -0
  297. package/skills/skill-builder/templates/CLAUDE.md +1 -14
  298. package/skills/skill-builder/workflows/AGENTS.md +19 -0
  299. package/skills/skill-builder/workflows/CLAUDE.md +1 -19
  300. package/skills/small-cl/SKILL.md +52 -0
  301. package/skills/task-build/AGENTS.md +29 -0
  302. package/skills/task-build/CLAUDE.md +1 -29
  303. package/skills/update/AGENTS.md +38 -0
  304. package/skills/update/CLAUDE.md +1 -38
  305. package/system-prompts/AGENTS.md +17 -0
  306. package/system-prompts/CLAUDE.md +1 -17
  307. package/system-prompts/software-engineer.xml +3 -3
  308. package/agents/code-verifier.md +0 -74
  309. package/docs/PR_DESCRIPTION_GUIDE.md +0 -157
  310. package/docs/references/code-review-enforcement.md +0 -132
  311. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +0 -53
  312. package/hooks/blocking/code_review_gate_deny.py +0 -74
  313. package/hooks/blocking/code_review_pr_create_gate.py +0 -198
  314. package/hooks/blocking/code_review_push_gate.py +0 -145
  315. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +0 -348
  316. package/hooks/blocking/code_review_stamp_store.py +0 -233
  317. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +0 -7
  318. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +0 -15
  319. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +0 -212
  320. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +0 -138
  321. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +0 -49
  322. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +0 -38
  323. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +0 -483
  324. package/hooks/blocking/config/code_review_enforcement_constants.py +0 -143
  325. package/hooks/blocking/config/test_code_review_enforcement_constants.py +0 -166
  326. package/hooks/blocking/config/verified_commit_constants.py +0 -160
  327. package/hooks/blocking/config/verified_commit_context_constants.py +0 -21
  328. package/hooks/blocking/config/verified_commit_gate_output_constants.py +0 -14
  329. package/hooks/blocking/conftest.py +0 -123
  330. package/hooks/blocking/convergence_gate_blocker.py +0 -310
  331. package/hooks/blocking/pr_converge_bugteam_enforcer.py +0 -170
  332. package/hooks/blocking/pr_description_body_audit.py +0 -148
  333. package/hooks/blocking/pr_description_command_parser.py +0 -237
  334. package/hooks/blocking/pr_description_enforcer.py +0 -227
  335. package/hooks/blocking/pr_description_pr_number.py +0 -155
  336. package/hooks/blocking/pr_description_proof_of_work.py +0 -385
  337. package/hooks/blocking/pr_description_readability.py +0 -364
  338. package/hooks/blocking/reviewer_spawn_gate.py +0 -182
  339. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +0 -62
  340. package/hooks/blocking/test_code_review_gate_deny.py +0 -54
  341. package/hooks/blocking/test_code_review_pr_create_gate.py +0 -199
  342. package/hooks/blocking/test_code_review_push_gate.py +0 -205
  343. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +0 -199
  344. package/hooks/blocking/test_code_review_stamp_store.py +0 -205
  345. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +0 -710
  346. package/hooks/blocking/test_code_verifier_tools_contract.py +0 -28
  347. package/hooks/blocking/test_convergence_gate_blocker.py +0 -428
  348. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +0 -311
  349. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +0 -184
  350. package/hooks/blocking/test_pr_description_enforcer.py +0 -221
  351. package/hooks/blocking/test_pr_description_enforcer_body_audit.py +0 -247
  352. package/hooks/blocking/test_pr_description_enforcer_body_rules.py +0 -493
  353. package/hooks/blocking/test_pr_description_enforcer_command_parser.py +0 -366
  354. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +0 -159
  355. package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +0 -175
  356. package/hooks/blocking/test_pr_description_enforcer_readability.py +0 -443
  357. package/hooks/blocking/test_pr_description_proof_of_work.py +0 -313
  358. package/hooks/blocking/test_reviewer_spawn_gate.py +0 -230
  359. package/hooks/blocking/test_verdict_directory_write_blocker.py +0 -804
  360. package/hooks/blocking/test_verification_verdict_store.py +0 -974
  361. package/hooks/blocking/test_verified_commit_config_bootstrap.py +0 -67
  362. package/hooks/blocking/test_verified_commit_docs_delta.py +0 -176
  363. package/hooks/blocking/test_verified_commit_gate.py +0 -581
  364. package/hooks/blocking/test_verified_commit_gate_additional_context.py +0 -134
  365. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +0 -131
  366. package/hooks/blocking/test_verifier_verdict_minter.py +0 -299
  367. package/hooks/blocking/tests/test_verified_commit_gate.py +0 -41
  368. package/hooks/blocking/verdict_directory_write_blocker.py +0 -687
  369. package/hooks/blocking/verification_verdict_store.py +0 -1039
  370. package/hooks/blocking/verified_commit_config_bootstrap.py +0 -63
  371. package/hooks/blocking/verified_commit_gate.py +0 -113
  372. package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +0 -28
  373. package/hooks/blocking/verified_commit_gate_parts/__init__.py +0 -1
  374. package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +0 -174
  375. package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +0 -53
  376. package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +0 -80
  377. package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +0 -170
  378. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +0 -217
  379. package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +0 -10
  380. package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +0 -94
  381. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +0 -17
  382. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +0 -38
  383. package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +0 -71
  384. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +0 -96
  385. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +0 -167
  386. package/hooks/blocking/verifier_verdict_minter.py +0 -280
  387. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +0 -64
  388. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +0 -53
  389. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_constants.py +0 -55
  390. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_state.py +0 -67
  391. package/hooks/hooks_constants/pr_description_enforcer_constants.py +0 -167
  392. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -107
  393. package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +0 -41
  394. package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +0 -292
  395. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +0 -198
  396. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +0 -283
  397. package/rules/proof-of-work-pr-comments.md +0 -22
  398. package/rules/verified-commit-gate-skip.md +0 -4
  399. package/scripts/Show-Asset.ps1 +0 -106
@@ -1,199 +0,0 @@
1
- """Behavioral tests for the PR-create code-review gate.
2
-
3
- Each test drives the gate against a real git work tree with a change surface
4
- and a real stamp store under an isolated home, so the deny/allow decision runs
5
- the same hash-and-coverage path the hook runs in production.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import importlib.util
11
- import subprocess
12
- import sys
13
- from pathlib import Path
14
- from types import ModuleType
15
-
16
- import pytest
17
-
18
- _HOOK_DIR = Path(__file__).resolve().parent
19
- if str(_HOOK_DIR) not in sys.path:
20
- sys.path.insert(0, str(_HOOK_DIR))
21
-
22
- GIT_TIMEOUT_SECONDS = 30
23
- XHIGH_EFFORT = "xhigh"
24
- LOW_EFFORT = "low"
25
- SURFACE_SOURCE = "def add(a: int, b: int) -> int:\n return a + b\n"
26
- SURFACE_CHANGE = "def add(a: int, b: int) -> int:\n return a - b\n"
27
- MCP_CREATE_PR_TOOL = "mcp__plugin_github_github__create_pull_request"
28
-
29
-
30
- def _load_module(module_name: str) -> ModuleType:
31
- module_spec = importlib.util.spec_from_file_location(
32
- module_name, _HOOK_DIR / f"{module_name}.py"
33
- )
34
- assert module_spec is not None
35
- assert module_spec.loader is not None
36
- loaded_module = importlib.util.module_from_spec(module_spec)
37
- module_spec.loader.exec_module(loaded_module)
38
- return loaded_module
39
-
40
-
41
- gate_module = _load_module("code_review_pr_create_gate")
42
- store_module = _load_module("code_review_stamp_store")
43
- deny_module = _load_module("code_review_gate_deny")
44
-
45
-
46
- @pytest.fixture(autouse=True)
47
- def enable_code_review_enforcement(monkeypatch: pytest.MonkeyPatch) -> None:
48
- """Behavior tests exercise the gates with enforcement on."""
49
- monkeypatch.setattr(gate_module, "CODE_REVIEW_ENFORCEMENT_ENABLED", True)
50
-
51
-
52
- def _run_git(repository_directory: Path, *git_arguments: str) -> None:
53
- subprocess.run(
54
- ["git", "-C", str(repository_directory), *git_arguments],
55
- check=True,
56
- capture_output=True,
57
- text=True,
58
- timeout=GIT_TIMEOUT_SECONDS,
59
- )
60
-
61
-
62
- def _make_repo_with_change_surface(tmp_path: Path) -> Path:
63
- origin_directory = tmp_path / "origin.git"
64
- work_directory = tmp_path / "work"
65
- work_directory.mkdir()
66
- subprocess.run(
67
- ["git", "init", "--bare", "--initial-branch=main", str(origin_directory)],
68
- check=True,
69
- capture_output=True,
70
- text=True,
71
- timeout=GIT_TIMEOUT_SECONDS,
72
- )
73
- _run_git(work_directory, "init", "--initial-branch=main")
74
- _run_git(work_directory, "config", "user.email", "tests@example.com")
75
- _run_git(work_directory, "config", "user.name", "Reviewer")
76
- (work_directory / "app.py").write_text(SURFACE_SOURCE, encoding="utf-8")
77
- _run_git(work_directory, "add", "-A")
78
- _run_git(work_directory, "commit", "-m", "base")
79
- _run_git(work_directory, "remote", "add", "origin", str(origin_directory))
80
- _run_git(work_directory, "push", "-u", "origin", "main")
81
- (work_directory / "app.py").write_text(SURFACE_CHANGE, encoding="utf-8")
82
- return work_directory
83
-
84
-
85
- def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: Path) -> None:
86
- home_text = str(fake_home)
87
- monkeypatch.setenv("HOME", home_text)
88
- monkeypatch.setenv("USERPROFILE", home_text)
89
- monkeypatch.delenv("HOMEDRIVE", raising=False)
90
- monkeypatch.delenv("HOMEPATH", raising=False)
91
-
92
-
93
- def _prepared_repo(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
94
- fake_home = tmp_path / "home"
95
- fake_home.mkdir()
96
- _isolate_home(monkeypatch, fake_home)
97
- return _make_repo_with_change_surface(tmp_path)
98
-
99
-
100
- def _record_stamp(work_directory: Path, effort: str) -> None:
101
- surface_hash = store_module.live_surface_hash(str(work_directory))
102
- assert surface_hash is not None
103
- store_module.record_clean_stamp(str(work_directory), surface_hash, effort)
104
-
105
-
106
- def test_command_invokes_gh_pr_create_detects_real_invocation() -> None:
107
- assert gate_module.command_invokes_gh_pr_create("gh pr create --title T --body-file b.md")
108
-
109
-
110
- def test_gh_pr_edit_is_not_a_create() -> None:
111
- assert not gate_module.command_invokes_gh_pr_create("gh pr edit 1 --title T")
112
-
113
-
114
- def test_echoed_prose_is_not_a_create() -> None:
115
- assert not gate_module.command_invokes_gh_pr_create('echo "gh pr create"')
116
-
117
-
118
- def test_is_mcp_create_pull_request_tool_matches_configured_name() -> None:
119
- assert gate_module.is_mcp_create_pull_request_tool(MCP_CREATE_PR_TOOL)
120
- assert not gate_module.is_mcp_create_pull_request_tool("Bash")
121
-
122
-
123
- def test_uncovered_surface_denies_pr_create(
124
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
125
- ) -> None:
126
- work_directory = _prepared_repo(monkeypatch, tmp_path)
127
- deny_reason = gate_module.deny_reason_for_directory(str(work_directory))
128
- assert deny_reason is not None
129
- assert "PR_CREATE_GATE" in deny_reason
130
-
131
-
132
- def test_covering_xhigh_stamp_allows_pr_create(
133
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
134
- ) -> None:
135
- work_directory = _prepared_repo(monkeypatch, tmp_path)
136
- _record_stamp(work_directory, XHIGH_EFFORT)
137
- assert gate_module.deny_reason_for_directory(str(work_directory)) is None
138
-
139
-
140
- def test_low_only_stamp_does_not_satisfy_xhigh(
141
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
142
- ) -> None:
143
- work_directory = _prepared_repo(monkeypatch, tmp_path)
144
- _record_stamp(work_directory, LOW_EFFORT)
145
- deny_reason = gate_module.deny_reason_for_directory(str(work_directory))
146
- assert deny_reason is not None
147
- assert "PR_CREATE_GATE" in deny_reason
148
-
149
-
150
- def test_shell_gh_pr_create_payload_denies_without_stamp(
151
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
152
- ) -> None:
153
- work_directory = _prepared_repo(monkeypatch, tmp_path)
154
- payload = {
155
- "tool_name": "Bash",
156
- "tool_input": {"command": "gh pr create --title T --body-file b.md"},
157
- "cwd": str(work_directory),
158
- }
159
- deny_decision = gate_module.decision_for_payload(payload)
160
- assert deny_decision is not None
161
- assert deny_decision["hookSpecificOutput"]["permissionDecision"] == "deny"
162
-
163
-
164
- def test_mcp_create_pull_request_payload_denies_without_stamp(
165
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
166
- ) -> None:
167
- work_directory = _prepared_repo(monkeypatch, tmp_path)
168
- payload = {
169
- "tool_name": MCP_CREATE_PR_TOOL,
170
- "tool_input": {"owner": "o", "repo": "r"},
171
- "cwd": str(work_directory),
172
- }
173
- deny_decision = gate_module.decision_for_payload(payload)
174
- assert deny_decision is not None
175
- assert deny_decision["hookSpecificOutput"]["permissionDecision"] == "deny"
176
-
177
-
178
- def test_gh_pr_edit_payload_is_not_denied(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
179
- work_directory = _prepared_repo(monkeypatch, tmp_path)
180
- payload = {
181
- "tool_name": "Bash",
182
- "tool_input": {"command": "gh pr edit 1 --title T"},
183
- "cwd": str(work_directory),
184
- }
185
- assert gate_module.decision_for_payload(payload) is None
186
-
187
-
188
- def test_build_deny_payload_shape() -> None:
189
- deny_payload = deny_module.build_code_review_deny_payload("blocked reason")
190
- assert deny_payload["hookSpecificOutput"]["permissionDecision"] == "deny"
191
- assert deny_payload["hookSpecificOutput"]["permissionDecisionReason"] == "blocked reason"
192
-
193
-
194
- def test_enforcement_off_allows_pr_create_without_stamp(
195
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
196
- ) -> None:
197
- monkeypatch.setattr(gate_module, "CODE_REVIEW_ENFORCEMENT_ENABLED", False)
198
- work_directory = _prepared_repo(monkeypatch, tmp_path)
199
- assert gate_module.deny_reason_for_directory(str(work_directory)) is None
@@ -1,205 +0,0 @@
1
- """Behavioral tests for the push code-review gate.
2
-
3
- Each test drives the gate against a real git work tree with a change surface
4
- and a real stamp store under an isolated home, so the deny/allow decision runs
5
- the same hash-and-coverage path the hook runs in production.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import importlib.util
11
- import io
12
- import json
13
- import subprocess
14
- import sys
15
- from pathlib import Path
16
- from types import ModuleType
17
-
18
- import pytest
19
-
20
- _HOOK_DIR = Path(__file__).resolve().parent
21
- if str(_HOOK_DIR) not in sys.path:
22
- sys.path.insert(0, str(_HOOK_DIR))
23
-
24
- GIT_TIMEOUT_SECONDS = 30
25
- LOW_EFFORT = "low"
26
- XHIGH_EFFORT = "xhigh"
27
- CODE_SOURCE = "def add(a: int, b: int) -> int:\n return a + b\n"
28
- CODE_CHANGE = "def add(a: int, b: int) -> int:\n return a - b\n"
29
- DOCS_SOURCE = "# Notes\n\nfirst line\n"
30
- DOCS_CHANGE = "# Notes\n\nsecond line\n"
31
- BYPASS_MARKER = "# code-review-skip"
32
-
33
-
34
- def _load_module(module_name: str) -> ModuleType:
35
- module_spec = importlib.util.spec_from_file_location(
36
- module_name, _HOOK_DIR / f"{module_name}.py"
37
- )
38
- assert module_spec is not None
39
- assert module_spec.loader is not None
40
- loaded_module = importlib.util.module_from_spec(module_spec)
41
- module_spec.loader.exec_module(loaded_module)
42
- return loaded_module
43
-
44
-
45
- gate_module = _load_module("code_review_push_gate")
46
- store_module = _load_module("code_review_stamp_store")
47
-
48
-
49
- @pytest.fixture(autouse=True)
50
- def enable_code_review_enforcement(monkeypatch: pytest.MonkeyPatch) -> None:
51
- """Behavior tests exercise the gates with enforcement on."""
52
- monkeypatch.setattr(gate_module, "CODE_REVIEW_ENFORCEMENT_ENABLED", True)
53
-
54
-
55
- def _run_git(repository_directory: Path, *git_arguments: str) -> None:
56
- subprocess.run(
57
- ["git", "-C", str(repository_directory), *git_arguments],
58
- check=True,
59
- capture_output=True,
60
- text=True,
61
- timeout=GIT_TIMEOUT_SECONDS,
62
- )
63
-
64
-
65
- def _init_pushed_repo(tmp_path: Path, tracked_name: str, base_text: str) -> Path:
66
- origin_directory = tmp_path / "origin.git"
67
- work_directory = tmp_path / "work"
68
- work_directory.mkdir()
69
- subprocess.run(
70
- ["git", "init", "--bare", "--initial-branch=main", str(origin_directory)],
71
- check=True,
72
- capture_output=True,
73
- text=True,
74
- timeout=GIT_TIMEOUT_SECONDS,
75
- )
76
- _run_git(work_directory, "init", "--initial-branch=main")
77
- _run_git(work_directory, "config", "user.email", "tests@example.com")
78
- _run_git(work_directory, "config", "user.name", "Reviewer")
79
- (work_directory / tracked_name).write_text(base_text, encoding="utf-8")
80
- _run_git(work_directory, "add", "-A")
81
- _run_git(work_directory, "commit", "-m", "base")
82
- _run_git(work_directory, "remote", "add", "origin", str(origin_directory))
83
- _run_git(work_directory, "push", "-u", "origin", "main")
84
- return work_directory
85
-
86
-
87
- def _make_code_surface_repo(tmp_path: Path) -> Path:
88
- work_directory = _init_pushed_repo(tmp_path, "app.py", CODE_SOURCE)
89
- (work_directory / "app.py").write_text(CODE_CHANGE, encoding="utf-8")
90
- return work_directory
91
-
92
-
93
- def _make_docs_surface_repo(tmp_path: Path) -> Path:
94
- work_directory = _init_pushed_repo(tmp_path, "notes.md", DOCS_SOURCE)
95
- (work_directory / "notes.md").write_text(DOCS_CHANGE, encoding="utf-8")
96
- return work_directory
97
-
98
-
99
- def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: Path) -> None:
100
- home_text = str(fake_home)
101
- monkeypatch.setenv("HOME", home_text)
102
- monkeypatch.setenv("USERPROFILE", home_text)
103
- monkeypatch.delenv("HOMEDRIVE", raising=False)
104
- monkeypatch.delenv("HOMEPATH", raising=False)
105
-
106
-
107
- def _prepared_code_repo(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
108
- fake_home = tmp_path / "home"
109
- fake_home.mkdir()
110
- _isolate_home(monkeypatch, fake_home)
111
- return _make_code_surface_repo(tmp_path)
112
-
113
-
114
- def _record_stamp(work_directory: Path, effort: str) -> None:
115
- surface_hash = store_module.live_surface_hash(str(work_directory))
116
- assert surface_hash is not None
117
- store_module.record_clean_stamp(str(work_directory), surface_hash, effort)
118
-
119
-
120
- def _run_main(monkeypatch: pytest.MonkeyPatch, command_text: str, work_directory: Path) -> str:
121
- payload_text = json.dumps(
122
- {
123
- "tool_name": "Bash",
124
- "tool_input": {"command": command_text},
125
- "cwd": str(work_directory),
126
- }
127
- )
128
- monkeypatch.setattr(sys, "stdin", io.StringIO(payload_text))
129
- captured_stdout = io.StringIO()
130
- monkeypatch.setattr(sys, "stdout", captured_stdout)
131
- gate_module.main()
132
- return captured_stdout.getvalue()
133
-
134
-
135
- def test_push_without_low_stamp_denies(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
136
- work_directory = _prepared_code_repo(monkeypatch, tmp_path)
137
- deny_reason = gate_module.deny_reason_for_directory(str(work_directory))
138
- assert deny_reason is not None
139
- assert "PUSH_GATE" in deny_reason
140
-
141
-
142
- def test_covering_low_stamp_allows_push(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
143
- work_directory = _prepared_code_repo(monkeypatch, tmp_path)
144
- _record_stamp(work_directory, LOW_EFFORT)
145
- assert gate_module.deny_reason_for_directory(str(work_directory)) is None
146
-
147
-
148
- def test_covering_xhigh_stamp_allows_push(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
149
- work_directory = _prepared_code_repo(monkeypatch, tmp_path)
150
- _record_stamp(work_directory, XHIGH_EFFORT)
151
- assert gate_module.deny_reason_for_directory(str(work_directory)) is None
152
-
153
-
154
- def test_docs_only_surface_is_exempt(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
155
- fake_home = tmp_path / "home"
156
- fake_home.mkdir()
157
- _isolate_home(monkeypatch, fake_home)
158
- work_directory = _make_docs_surface_repo(tmp_path)
159
- assert gate_module.deny_reason_for_directory(str(work_directory)) is None
160
-
161
-
162
- def test_git_push_payload_denies(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
163
- work_directory = _prepared_code_repo(monkeypatch, tmp_path)
164
- emitted = _run_main(monkeypatch, "git push origin main", work_directory)
165
- assert "PUSH_GATE" in emitted
166
-
167
-
168
- def test_git_push_with_leading_flags_denies(
169
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
170
- ) -> None:
171
- work_directory = _prepared_code_repo(monkeypatch, tmp_path)
172
- emitted = _run_main(monkeypatch, "git push --force-with-lease origin main", work_directory)
173
- assert "PUSH_GATE" in emitted
174
-
175
-
176
- def test_git_commit_is_not_gated_by_push_hook(
177
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
178
- ) -> None:
179
- work_directory = _prepared_code_repo(monkeypatch, tmp_path)
180
- emitted = _run_main(monkeypatch, "git commit -m change", work_directory)
181
- assert emitted == ""
182
-
183
-
184
- def test_git_stash_push_is_not_gated(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
185
- work_directory = _prepared_code_repo(monkeypatch, tmp_path)
186
- emitted = _run_main(monkeypatch, "git stash push", work_directory)
187
- assert emitted == ""
188
-
189
-
190
- def test_code_review_skip_marker_allows_push(
191
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
192
- ) -> None:
193
- work_directory = _prepared_code_repo(monkeypatch, tmp_path)
194
- emitted = _run_main(monkeypatch, f"git push origin main {BYPASS_MARKER}", work_directory)
195
- assert emitted == ""
196
-
197
-
198
- def test_enforcement_off_allows_push_without_stamp(
199
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
200
- ) -> None:
201
- monkeypatch.setattr(gate_module, "CODE_REVIEW_ENFORCEMENT_ENABLED", False)
202
- work_directory = _prepared_code_repo(monkeypatch, tmp_path)
203
- assert gate_module.deny_reason_for_directory(str(work_directory)) is None
204
- emitted = _run_main(monkeypatch, "git push origin main", work_directory)
205
- assert emitted == ""
@@ -1,199 +0,0 @@
1
- """Behavioral tests for the code-review stamp-directory write blocker.
2
-
3
- The blocker denies shell and file-tool access to
4
- ``~/.claude/code-review-stamps/`` and shell references to the stamp store
5
- module or its mint call, while the sanctioned invoker command and unrelated
6
- paths pass.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import importlib.util
12
- import io
13
- import json
14
- import sys
15
- from pathlib import Path
16
- from types import ModuleType
17
-
18
- import pytest
19
-
20
- _HOOK_DIR = Path(__file__).resolve().parent
21
- if str(_HOOK_DIR) not in sys.path:
22
- sys.path.insert(0, str(_HOOK_DIR))
23
-
24
- STAMP_STORE_IMPORT_FORGE = (
25
- 'python -c "from code_review_stamp_store import record_clean_stamp; '
26
- "record_clean_stamp(root, live_hash, 'low')\""
27
- )
28
- MINT_CALL_FORGE = "python -c \"record_clean_stamp(root, live_hash, 'xhigh')\""
29
- PYTEST_RUN = "python -m pytest test_code_review_stamp_store.py"
30
- SANCTIONED_INVOKER = "python invoke_code_review.py --record-stamp --cwd . --session-model opus low"
31
- ABSOLUTE_STAMP_ACCESS = "cat ~/.claude/code-review-stamps/abc1234567890abc.json"
32
- DECOY_ECHO_THEN_FORGE = (
33
- 'echo "invoke_code_review.py --record-stamp" >/dev/null; '
34
- "echo x > ~/.claude/code-review-stamps/abc1234567890123.json"
35
- )
36
- DECOY_INVOKER_THEN_FORGE = (
37
- "python invoke_code_review.py --record-stamp --cwd . low ; "
38
- "echo pwned > ~/.claude/code-review-stamps/evil.json"
39
- )
40
-
41
-
42
- def _load_module(module_name: str) -> ModuleType:
43
- module_spec = importlib.util.spec_from_file_location(
44
- module_name, _HOOK_DIR / f"{module_name}.py"
45
- )
46
- assert module_spec is not None
47
- assert module_spec.loader is not None
48
- loaded_module = importlib.util.module_from_spec(module_spec)
49
- module_spec.loader.exec_module(loaded_module)
50
- return loaded_module
51
-
52
-
53
- blocker_module = _load_module("code_review_stamp_directory_write_blocker")
54
-
55
-
56
- @pytest.fixture(autouse=True)
57
- def enable_code_review_enforcement(monkeypatch: pytest.MonkeyPatch) -> None:
58
- """Behavior tests exercise the write-blocker with enforcement on."""
59
- monkeypatch.setattr(blocker_module, "CODE_REVIEW_ENFORCEMENT_ENABLED", True)
60
-
61
-
62
- def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: Path) -> None:
63
- home_text = str(fake_home)
64
- monkeypatch.setenv("HOME", home_text)
65
- monkeypatch.setenv("USERPROFILE", home_text)
66
- monkeypatch.delenv("HOMEDRIVE", raising=False)
67
- monkeypatch.delenv("HOMEPATH", raising=False)
68
-
69
-
70
- def test_absolute_stamp_path_is_blocked() -> None:
71
- assert blocker_module.references_stamp_directory(ABSOLUTE_STAMP_ACCESS)
72
-
73
-
74
- def test_store_module_import_forge_is_blocked() -> None:
75
- assert blocker_module.references_stamp_directory(STAMP_STORE_IMPORT_FORGE)
76
-
77
-
78
- def test_mint_call_forge_is_blocked() -> None:
79
- assert blocker_module.references_stamp_directory(MINT_CALL_FORGE)
80
-
81
-
82
- def test_pytest_run_naming_store_test_is_not_blocked() -> None:
83
- assert not blocker_module.references_stamp_directory(PYTEST_RUN)
84
-
85
-
86
- def test_sanctioned_invoker_command_is_not_blocked() -> None:
87
- assert not blocker_module.references_stamp_directory(SANCTIONED_INVOKER)
88
-
89
-
90
- def test_decoy_echo_of_invoker_then_stamp_forge_is_blocked() -> None:
91
- assert blocker_module.references_stamp_directory(DECOY_ECHO_THEN_FORGE)
92
-
93
-
94
- def test_decoy_invoker_run_then_stamp_forge_is_blocked() -> None:
95
- assert blocker_module.references_stamp_directory(DECOY_INVOKER_THEN_FORGE)
96
-
97
-
98
- def test_write_tool_to_stamp_directory_is_blocked(
99
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
100
- ) -> None:
101
- fake_home = tmp_path / "home"
102
- fake_home.mkdir()
103
- _isolate_home(monkeypatch, fake_home)
104
- stamp_path = fake_home / ".claude" / "code-review-stamps" / "abc.json"
105
- assert blocker_module.path_targets_stamp_directory(str(stamp_path))
106
-
107
-
108
- def test_write_tool_elsewhere_is_allowed(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
109
- fake_home = tmp_path / "home"
110
- fake_home.mkdir()
111
- _isolate_home(monkeypatch, fake_home)
112
- other_path = tmp_path / "src" / "module.py"
113
- assert not blocker_module.path_targets_stamp_directory(str(other_path))
114
-
115
-
116
- def test_write_payload_to_stamp_directory_denies(
117
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
118
- ) -> None:
119
- fake_home = tmp_path / "home"
120
- fake_home.mkdir()
121
- _isolate_home(monkeypatch, fake_home)
122
- stamp_path = fake_home / ".claude" / "code-review-stamps" / "abc.json"
123
- payload = {
124
- "tool_name": "Write",
125
- "tool_input": {"file_path": str(stamp_path), "content": "{}"},
126
- }
127
- deny_decision = blocker_module.decision_for_payload(payload)
128
- assert deny_decision is not None
129
- assert deny_decision["hookSpecificOutput"]["permissionDecision"] == "deny"
130
-
131
-
132
- def test_bash_forge_payload_denies() -> None:
133
- payload = {
134
- "tool_name": "Bash",
135
- "tool_input": {"command": STAMP_STORE_IMPORT_FORGE},
136
- }
137
- deny_decision = blocker_module.decision_for_payload(payload)
138
- assert deny_decision is not None
139
- assert deny_decision["hookSpecificOutput"]["permissionDecision"] == "deny"
140
-
141
-
142
- def test_sanctioned_invoker_payload_is_not_denied() -> None:
143
- payload = {
144
- "tool_name": "Bash",
145
- "tool_input": {"command": SANCTIONED_INVOKER},
146
- }
147
- assert blocker_module.decision_for_payload(payload) is None
148
-
149
-
150
- def test_main_emits_deny_for_bash_forge(monkeypatch: pytest.MonkeyPatch) -> None:
151
- payload_text = json.dumps({"tool_name": "Bash", "tool_input": {"command": MINT_CALL_FORGE}})
152
- monkeypatch.setattr(sys, "stdin", io.StringIO(payload_text))
153
- captured_stdout = io.StringIO()
154
- monkeypatch.setattr(sys, "stdout", captured_stdout)
155
- blocker_module.main()
156
- assert "STAMP_DIRECTORY_GUARD" in captured_stdout.getvalue()
157
-
158
-
159
- STAMP_OBFUSCATION_PATH = "/.claude/code-review-stamps/a.json"
160
- BENIGN_OBFUSCATION_PATH = "/tmp/x"
161
- SPLIT_CD_FORGE = "cd ~/.claude && cd code-review-stamps && echo x > f.json"
162
-
163
-
164
- def _hex_forge(path_text: str) -> str:
165
- return f"python -c \"open(bytes.fromhex('{path_text.encode().hex()}'),'w').write('x')\""
166
-
167
-
168
- def _chr_chain_forge(path_text: str) -> str:
169
- chr_chain = "+".join(f"chr({ord(each_character)})" for each_character in path_text)
170
- return f"python -c \"open({chr_chain},'w').write('x')\""
171
-
172
-
173
- def test_split_directory_change_into_stamp_is_blocked() -> None:
174
- assert blocker_module.references_stamp_directory(SPLIT_CD_FORGE)
175
-
176
-
177
- def test_hex_obfuscated_stamp_path_is_blocked() -> None:
178
- assert blocker_module.references_stamp_directory(_hex_forge(STAMP_OBFUSCATION_PATH))
179
-
180
-
181
- def test_chr_chain_obfuscated_stamp_path_is_blocked() -> None:
182
- assert blocker_module.references_stamp_directory(_chr_chain_forge(STAMP_OBFUSCATION_PATH))
183
-
184
-
185
- def test_benign_decode_to_other_path_is_not_blocked() -> None:
186
- assert not blocker_module.references_stamp_directory(_hex_forge(BENIGN_OBFUSCATION_PATH))
187
-
188
-
189
- def test_enforcement_off_allows_stamp_directory_access(
190
- monkeypatch: pytest.MonkeyPatch,
191
- ) -> None:
192
- monkeypatch.setattr(blocker_module, "CODE_REVIEW_ENFORCEMENT_ENABLED", False)
193
- decision = blocker_module.decision_for_payload(
194
- {
195
- "tool_name": "Bash",
196
- "tool_input": {"command": "rm -rf ~/.claude/code-review-stamps"},
197
- }
198
- )
199
- assert decision is None