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
@@ -0,0 +1,123 @@
1
+ # hooks/blocking
2
+
3
+ PreToolUse hooks that deny (block) tool calls when a rule is violated. The main enforcer is `code_rules_enforcer.py`, which routes each Write/Edit through a suite of focused check modules. Every other file in this directory is either a standalone blocker, a check module, or a test.
4
+
5
+ ## Subdirectory
6
+
7
+ | Directory | Role |
8
+ |---|---|
9
+ | `tdd_enforcer_parts/` | Concern modules the `tdd_enforcer.py` entry hook wires together: path classification, content analysis, candidate-path resolution, freshness, git-tracking restore detection, decisions, and constants |
10
+ | `claude_md_orphan_file_blocker_parts/` | Concern modules the `claude_md_orphan_file_blocker.py` entry hook wires together: reference extraction, subtree scan, scan plan, decision, and constants |
11
+ | `package_inventory_stale_blocker_parts/` | Concern modules the `package_inventory_stale_blocker.py` entry hook wires together: inventory detection, decision, and constants |
12
+ | `inventory_intent_records/` | The shared per-session pending-intent store both inventory blockers read to break the file/row add-order deadlock |
13
+ | `pii_prevention_blocker_parts/` | Concern modules the `pii_prevention_blocker.py` entry hook wires together: per-repository scan exemption, the per-repository allowlist of exact values a commit may carry, and resolving the repository a commit command targets (with `config/` for the resolution deny-message constants) |
14
+ | `tests/` | pytest suite for `pii_prevention_blocker.py` repository resolution and the `pii_prevention_blocker_parts` modules |
15
+
16
+ ## Core enforcer
17
+
18
+ | File | What it does |
19
+ |---|---|
20
+ | `code_rules_enforcer.py` | Entry point — reads PreToolUse stdin, reconstructs post-edit content, dispatches all check modules, and returns a block with a per-issue list when any check fails |
21
+
22
+ The check modules it calls are the `code_rules_<concern>.py` files below.
23
+
24
+ ## Check modules (imported by `code_rules_enforcer.py`)
25
+
26
+ | Module | Concern |
27
+ |---|---|
28
+ | `code_rules_annotations_length.py` | Parameter/return annotations, function length, pytest fixture annotation requirements |
29
+ | `code_rules_banned_identifiers.py` | Banned short names (`ctx`, `cfg`, `msg`, etc.), banned prefixes (`handle_`, `process_`, etc.) |
30
+ | `code_rules_boolean_mustcheck.py` | Boolean naming (`is_`/`has_`/… prefixes) and must-check return values |
31
+ | `code_rules_command_dispatch.py` | A `hooks/blocking/` command classifier matching a multi-word command regex without a start anchor or first-word tokenization |
32
+ | `code_rules_comments.py` | No new inline comments; advisory on deletion of existing ones |
33
+ | `code_rules_constants_config.py` | Constants must live in `config/`; file-global constant use-count |
34
+ | `code_rules_dead_argparse_argument.py` | Argparse arguments with no references in the same file |
35
+ | `code_rules_dead_config_field.py` | `*Config` / `*Selectors` dataclass fields with no live references |
36
+ | `code_rules_dead_dataclass_field.py` | Dataclass fields with no consuming references |
37
+ | `code_rules_dead_module_constant.py` | `UPPER_SNAKE` constants in `*_constants.py` modules with no importers |
38
+ | `code_rules_dead_split_branch.py` | A conditional whose falsy branch is unreachable because the tested value comes from a separator `str.split()`, which never returns an empty list |
39
+ | `code_rules_docstrings.py` | Google-style docstrings; `Args:` section matches signature; fallback-branch coverage |
40
+ | `code_rules_duplicate_body.py` | A function body copied from a sibling module, or a helper body inlined as a block inside a larger function in the same file |
41
+ | `code_rules_imports_logging.py` | Imports at top of file; logging format-arg style; printf tokens in `str.format`-logger messages |
42
+ | `code_rules_js_conventions.py` | Boolean-prefix naming and banned identifiers for JavaScript/TypeScript declarations and `@param {boolean}` JSDoc, scoped to changed lines |
43
+ | `code_rules_magic_values.py` | No magic numbers or strings in production code bodies |
44
+ | `code_rules_mock_completeness.py` | Mock calls that skip required arguments |
45
+ | `code_rules_naming_collection.py` | Collection names must use `all_*` prefix |
46
+ | `code_rules_optional_params.py` | No optional parameters where a required one would do |
47
+ | `code_rules_orphan_css_class.py` | CSS class attributes in Python markup with no matching `.<class>` selector |
48
+ | `code_rules_paired_test.py` | A public function omitted by a module's established paired test suite must get a behavioral test — checked on both the production-module write and the stem-matched test-file write |
49
+ | `code_rules_path_utils.py` | Path utility helpers shared across check modules |
50
+ | `code_rules_paths_syspath.py` | `sys.path.insert` must be guarded |
51
+ | `code_rules_probe_chains.py` | Probe-chain detection logic |
52
+ | `code_rules_probe_detection.py` | Probe pattern detection helpers |
53
+ | `code_rules_probe_recording.py` | Probe recording utilities |
54
+ | `code_rules_scope_binding.py` | Scope/binding analysis utilities |
55
+ | `code_rules_shared.py` | Shared dataclasses and helpers used by multiple check modules |
56
+ | `code_rules_string_magic.py` | Magic string detection with masking and f-string support; whitespace-only indentation literals in function bodies |
57
+ | `code_rules_test_assertions.py` | Test assertion style rules |
58
+ | `code_rules_test_layout.py` | Dead scaffolding in a test module: a private module constant read by no other line, and an unused parameter on a private test helper |
59
+ | `code_rules_test_branching_except.py` | No bare or broad `except` in test branches |
60
+ | `code_rules_test_isolation.py` | Tests must not rely on home-dir or temp-dir side effects |
61
+ | `code_rules_type_escape.py` | No `Any` imports, `cast()`, or `# type: ignore` outside boundary files |
62
+ | `code_rules_typeddict_stub.py` | TypedDict pairs (`_encode_*`/`_decode_*`) must both exist in the same module |
63
+ | `code_rules_unused_imports.py` | Unused module-level imports |
64
+
65
+ ## Other standalone blockers
66
+
67
+ | File | Event | What it blocks |
68
+ |---|---|---|
69
+ | `orchestrator_refresh_reschedule_gate.py` | PreToolUse (ScheduleWakeup/CronCreate) | `/orchestrator-refresh` re-arm when run status is not `active` |
70
+ | `block_main_commit.py` | PreToolUse (Bash) | `git commit`/`git push` directly to `main` |
71
+ | `bot_mention_comment_blocker.py` | PreToolUse (Write/Edit) | PR review comments that @-mention a bot |
72
+ | `claude_md_orphan_file_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | Per-directory `CLAUDE.md` table cells naming a bare filename absent from the directory subtree |
73
+ | `conventional_pr_title_gate.py` | PreToolUse (Bash) | `gh pr create`/`gh pr edit` with a `--title` that is not a Conventional Commit, in a repo whose CI runs a semantic-pull-request title check |
74
+ | `destructive_command_blocker.py` | PreToolUse (Bash/PowerShell) | Shell commands with destructive literals (`rm -rf`, `git reset --hard`, etc.) |
75
+ | `docstring_rule_gate_count_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | A stale spelled-out gate-validator count in `docstring-prose-matches-implementation.md` — the "N more gate validators" / "M gated slices" count drifting from the `check_docstring_*` validators the prose names |
76
+ | `duplicate_rmtree_helper_blocker.py` | PreToolUse (Write/Edit) | A local re-definition of the Windows-safe rmtree helper trio (`_strip_read_only_and_retry`, `_force_remove_tree` / `force_rmtree`) in place of importing a shared helper |
77
+ | `eli11_reply_enforcer.py` | Stop | Final replies breaking the `eli11-replies` shape — more than 6 bullet lines, more than 2 list lines over 20 words each, or multi-line instructions with no numbered step among the lead lines. Code fences, inline code, blockquotes, table rows, and link targets come off before the reply is judged. |
78
+ | `env_var_table_code_drift_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | A markdown env-var summary table row attributing an environment variable to a code file whose source never references that variable name |
79
+ | `es_exe_path_rewriter.py` | PreToolUse | Rewrites paths referencing `.exe` under the Everything search path |
80
+ | `fable_spawn_gate.py` | PreToolUse (Agent/Task) | An `Agent` or `Task` spawn whose prompt carries no `FABLE-SPAWN-AUTHORIZED` token and whose model field reads `fable` in any letter case — the bare alias, or a delimiter segment of a full model id, so `claude-fable-5` is denied too |
81
+ | `gh_body_arg_blocker.py` | PreToolUse (Bash) | `gh` commands passing `--body`/`-b` directly (requires `--body-file` instead) |
82
+ | `gh_pr_author_enforcer.py` | PreToolUse | Enforces PR author identity rules |
83
+ | `gh_pr_author_restore.py` | PostToolUse | Restores PR author after a tool call |
84
+ | `hedging_language_blocker.py` | Stop | Responses with hedging words (`likely`, `probably`, `appears to`); armed only when `CLAUDE_PROSE_STYLE_ENFORCEMENT` is on (default off) |
85
+ | `hook_prose_detector_consistency.py` | PreToolUse (Write/Edit) | Hook docstrings/messages that claim a trigger the detector cannot fire on; armed only when `CLAUDE_PROSE_STYLE_ENFORCEMENT` is on (default off) |
86
+ | `intent_only_ending_blocker.py` | Stop | Responses that end on a plan or intent without doing the work; armed only when `CLAUDE_PROSE_STYLE_ENFORCEMENT` is on (default off) |
87
+ | `open_questions_in_plans_blocker.py` | PreToolUse (Write/Edit) | Plan documents with unresolved open questions |
88
+ | `nas_ssh_binary_enforcer.py` | PreToolUse (Bash) | A bare `ssh`/`scp`/`sftp` command word targeting the NAS (Git Bash's MSYS ssh stalls on an interactive password prompt), or the full `System32/OpenSSH` binary to that host without `-o BatchMode=yes` |
89
+ | `package_inventory_stale_blocker.py` | PreToolUse (Write) | A new production code file created in a directory whose `README.md`/`CLAUDE.md` inventory (or a parent skill's `SKILL.md` Layout table mapping the `scripts/` subdirectory) names two or more sibling files but no entry for the new file |
90
+ | `pii_commit_command.py` | library | Token-aware git-commit detection reused by `pii_prevention_blocker.py` |
91
+ | `pii_payload_scan.py` | library | Write/Edit and durable post-body PII evaluation reused by `pii_prevention_blocker.py` |
92
+ | `pii_prevention_blocker.py` | PreToolUse (Write/Edit/MultiEdit/Bash/PowerShell/MCP GitHub) | Entry hook — content that carries high-confidence personal data or secrets (real emails, home-dir paths, private IPs, credential material) on write, durable GitHub posts, or staged commit paths; resolves the staged-commit repository from the command it gates (via `pii_prevention_blocker_parts`), not the session working directory |
93
+ | `pii_scanner.py` | library | Pure text scanners shared by `pii_prevention_blocker.py` |
94
+ | `piped_pytest_blocker.py` | PreToolUse (Bash) | A pytest run whose output feeds a pipe, where the pipeline reports the exit code of the command on the right |
95
+ | `plain_language_blocker.py` | PreToolUse (Write/Edit/AskUserQuestion) | Heavy or jargon words in user-facing prose when `CLAUDE_PROSE_STYLE_ENFORCEMENT` is on (default off); AskUserQuestion lean-block structure stays always on |
96
+ | `precommit_code_rules_gate.py` | PreToolUse (Bash) | Staged changes that fail the CODE_RULES gate at commit time |
97
+ | `pytest_testpaths_orphan_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | New `test_*.py` files created under a directory absent from a package's explicit pytest `testpaths` allowlist |
98
+ | `question_to_user_enforcer.py` | Stop | User-directed questions not routed through `AskUserQuestion` |
99
+ | `send_user_file_open_locally_blocker.py` | PreToolUse (SendUserFile) | A desk-side file attach (`SendUserFile` with `status` not `proactive`); points to opening the file with its native Windows app |
100
+ | `sensitive_file_protector.py` | PreToolUse (Write/Edit) | Writes to sensitive credential or config files |
101
+ | `session_edit_stage_gate.py` | PreToolUse (Bash) | A `git commit` that would drop files edited this session because they are tracked but left unstaged |
102
+ | `session_handoff_blocker.py` | Stop | Responses suggesting a new session mid-task |
103
+ | `shell_substitution_blocker.py` | PreToolUse (Bash) | A command carrying `$(...)`, a live backtick, or `<(...)`/`>(...)` process substitution, which the allowlist matcher cannot descend into |
104
+ | `stale_comment_reference_blocker.py` | PreToolUse (Edit) | An Edit that rewrites a Python code line while keeping the standalone comment directly above it, when that comment names an identifier the rewrite removes from the line |
105
+ | `state_description_blocker.py` | PreToolUse (Write/Edit) | Historical/comparative language in documentation; armed only when `CLAUDE_PROSE_STYLE_ENFORCEMENT` is on (default off) |
106
+ | `subprocess_budget_completeness.py` | PreToolUse | Subprocess calls missing required budget arguments |
107
+ | `tdd_enforcer.py` | PreToolUse (Write/Edit) | Production code written without a matching failing test |
108
+ | `unscoped_search_blocker.py` | PreToolUse (Bash/PowerShell) | A `find` or recursive listing that walks from the filesystem root, a drive root, bare home, or a network share root |
109
+ | `volatile_path_in_post_blocker.py` | PreToolUse (Bash/MCP GitHub) | `gh` post commands and GitHub MCP post tools whose body references a volatile path (job scratch dir, worktree, or system temp) that outlives the durable post |
110
+ | `windows_rmtree_blocker.py` | PreToolUse (Write/Edit) | `shutil.rmtree` with `ignore_errors=True` on Windows |
111
+ | `workflow_substitution_slot_blocker.py` | PreToolUse (Write/Edit) | Workflow templates with bare per-iteration tokens missing angle-bracket slots |
112
+ | `write_existing_file_blocker.py` | PreToolUse (Write) | Write to a path where a file already exists |
113
+
114
+ ## Supporting modules
115
+
116
+ | File | Role |
117
+ |---|---|
118
+ | `_gh_body_arg_utils.py` | Parsing helpers for `gh_body_arg_blocker.py` |
119
+
120
+ ## Conventions
121
+
122
+ - Tests live beside each hook as `test_<hookname>.py` or `test_<hookname>_<suffix>.py`. Run with `python -m pytest <test_file>`.
123
+ - Tunable constants live in `hooks_constants/<hook_name>_constants.py`.
@@ -1,148 +1 @@
1
- # hooks/blocking
2
-
3
- PreToolUse hooks that deny (block) tool calls when a rule is violated. The main enforcer is `code_rules_enforcer.py`, which routes each Write/Edit through a suite of focused check modules. Every other file in this directory is either a standalone blocker, a check module, or a test.
4
-
5
- ## Subdirectory
6
-
7
- | Directory | Role |
8
- |---|---|
9
- | `config/` | Shared constants for the verified-commit gate family (`verified_commit_constants.py`) |
10
- | `tdd_enforcer_parts/` | Concern modules the `tdd_enforcer.py` entry hook wires together: path classification, content analysis, candidate-path resolution, freshness, git-tracking restore detection, decisions, and constants |
11
- | `verified_commit_gate_parts/` | Concern modules the `verified_commit_gate.py` entry hook wires together: command tokenization, directory-change resolution, gated git-invocation resolution, deny-reason resolution, and deny-payload assembly |
12
- | `code_review_stamp_write_blocker_parts/` | Concern modules the `code_review_stamp_directory_write_blocker.py` entry hook wires together: the split directory-change-into-stamp matcher and the obfuscated-stamp-path-write matcher |
13
- | `claude_md_orphan_file_blocker_parts/` | Concern modules the `claude_md_orphan_file_blocker.py` entry hook wires together: reference extraction, subtree scan, scan plan, decision, and constants |
14
- | `package_inventory_stale_blocker_parts/` | Concern modules the `package_inventory_stale_blocker.py` entry hook wires together: inventory detection, decision, and constants |
15
- | `inventory_intent_records/` | The shared per-session pending-intent store both inventory blockers read to break the file/row add-order deadlock |
16
- | `pii_prevention_blocker_parts/` | Concern modules the `pii_prevention_blocker.py` entry hook wires together: per-repository scan exemption, the per-repository allowlist of exact values a commit may carry, and resolving the repository a commit command targets (with `config/` for the resolution deny-message constants) |
17
- | `tests/` | pytest suite for `pii_prevention_blocker.py` repository resolution and the `pii_prevention_blocker_parts` modules |
18
-
19
- ## Core enforcer
20
-
21
- | File | What it does |
22
- |---|---|
23
- | `code_rules_enforcer.py` | Entry point — reads PreToolUse stdin, reconstructs post-edit content, dispatches all check modules, and returns a block with a per-issue list when any check fails |
24
-
25
- The check modules it calls are the `code_rules_<concern>.py` files below.
26
-
27
- ## Check modules (imported by `code_rules_enforcer.py`)
28
-
29
- | Module | Concern |
30
- |---|---|
31
- | `code_rules_annotations_length.py` | Parameter/return annotations, function length, pytest fixture annotation requirements |
32
- | `code_rules_banned_identifiers.py` | Banned short names (`ctx`, `cfg`, `msg`, etc.), banned prefixes (`handle_`, `process_`, etc.) |
33
- | `code_rules_boolean_mustcheck.py` | Boolean naming (`is_`/`has_`/… prefixes) and must-check return values |
34
- | `code_rules_command_dispatch.py` | A `hooks/blocking/` command classifier matching a multi-word command regex without a start anchor or first-word tokenization |
35
- | `code_rules_comments.py` | No new inline comments; advisory on deletion of existing ones |
36
- | `code_rules_constants_config.py` | Constants must live in `config/`; file-global constant use-count |
37
- | `code_rules_dead_argparse_argument.py` | Argparse arguments with no references in the same file |
38
- | `code_rules_dead_config_field.py` | `*Config` / `*Selectors` dataclass fields with no live references |
39
- | `code_rules_dead_dataclass_field.py` | Dataclass fields with no consuming references |
40
- | `code_rules_dead_module_constant.py` | `UPPER_SNAKE` constants in `*_constants.py` modules with no importers |
41
- | `code_rules_dead_split_branch.py` | A conditional whose falsy branch is unreachable because the tested value comes from a separator `str.split()`, which never returns an empty list |
42
- | `code_rules_docstrings.py` | Google-style docstrings; `Args:` section matches signature; fallback-branch coverage |
43
- | `code_rules_duplicate_body.py` | A function body copied from a sibling module, or a helper body inlined as a block inside a larger function in the same file |
44
- | `code_rules_imports_logging.py` | Imports at top of file; logging format-arg style; printf tokens in `str.format`-logger messages |
45
- | `code_rules_js_conventions.py` | Boolean-prefix naming and banned identifiers for JavaScript/TypeScript declarations and `@param {boolean}` JSDoc, scoped to changed lines |
46
- | `code_rules_magic_values.py` | No magic numbers or strings in production code bodies |
47
- | `code_rules_mock_completeness.py` | Mock calls that skip required arguments |
48
- | `code_rules_naming_collection.py` | Collection names must use `all_*` prefix |
49
- | `code_rules_optional_params.py` | No optional parameters where a required one would do |
50
- | `code_rules_orphan_css_class.py` | CSS class attributes in Python markup with no matching `.<class>` selector |
51
- | `code_rules_paired_test.py` | A public function omitted by a module's established paired test suite must get a behavioral test — checked on both the production-module write and the stem-matched test-file write |
52
- | `code_rules_path_utils.py` | Path utility helpers shared across check modules |
53
- | `code_rules_paths_syspath.py` | `sys.path.insert` must be guarded |
54
- | `code_rules_probe_chains.py` | Probe-chain detection logic |
55
- | `code_rules_probe_detection.py` | Probe pattern detection helpers |
56
- | `code_rules_probe_recording.py` | Probe recording utilities |
57
- | `code_rules_scope_binding.py` | Scope/binding analysis utilities |
58
- | `code_rules_shared.py` | Shared dataclasses and helpers used by multiple check modules |
59
- | `code_rules_string_magic.py` | Magic string detection with masking and f-string support; whitespace-only indentation literals in function bodies |
60
- | `code_rules_test_assertions.py` | Test assertion style rules |
61
- | `code_rules_test_layout.py` | Dead scaffolding in a test module: a private module constant read by no other line, and an unused parameter on a private test helper |
62
- | `code_rules_test_branching_except.py` | No bare or broad `except` in test branches |
63
- | `code_rules_test_isolation.py` | Tests must not rely on home-dir or temp-dir side effects |
64
- | `code_rules_type_escape.py` | No `Any` imports, `cast()`, or `# type: ignore` outside boundary files |
65
- | `code_rules_typeddict_stub.py` | TypedDict pairs (`_encode_*`/`_decode_*`) must both exist in the same module |
66
- | `code_rules_unused_imports.py` | Unused module-level imports |
67
-
68
- ## Other standalone blockers
69
-
70
- | File | Event | What it blocks |
71
- |---|---|---|
72
- | `orchestrator_refresh_reschedule_gate.py` | PreToolUse (ScheduleWakeup/CronCreate) | `/orchestrator-refresh` re-arm when run status is not `active` |
73
- | `block_main_commit.py` | PreToolUse (Bash) | `git commit`/`git push` directly to `main` |
74
- | `bot_mention_comment_blocker.py` | PreToolUse (Write/Edit) | PR review comments that @-mention a bot |
75
- | `claude_md_orphan_file_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | Per-directory `CLAUDE.md` table cells naming a bare filename absent from the directory subtree |
76
- | `code_review_pr_create_gate.py` | PreToolUse (Bash/MCP GitHub) | `gh pr create` or the MCP `create_pull_request` tool without a clean `xhigh` code-review stamp covering the branch surface |
77
- | `code_review_push_gate.py` | PreToolUse (Bash/PowerShell) | `git push` without a clean `low` code-review stamp covering the branch surface |
78
- | `code_review_stamp_directory_write_blocker.py` | PreToolUse (Bash/PowerShell/Write/Edit/MultiEdit) | Shell or file-tool writes into `~/.claude/code-review-stamps/`, and shell references to the stamp store module or its mint call, outside the sanctioned invoker |
79
- | `code_verifier_spawn_preflight_gate.py` | PreToolUse (Agent) | Spawning the `code-verifier` subagent when the branch has a merge conflict vs its base or a CODE_RULES violation on a working-tree-added line, or the CODE_RULES engine fails to load |
80
- | `convergence_gate_blocker.py` | PreToolUse (Bash) | Convergence workflow actions on a conflicting PR |
81
- | `conventional_pr_title_gate.py` | PreToolUse (Bash) | `gh pr create`/`gh pr edit` with a `--title` that is not a Conventional Commit, in a repo whose CI runs a semantic-pull-request title check |
82
- | `destructive_command_blocker.py` | PreToolUse (Bash/PowerShell) | Shell commands with destructive literals (`rm -rf`, `git reset --hard`, etc.) |
83
- | `docstring_rule_gate_count_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | A stale spelled-out gate-validator count in `docstring-prose-matches-implementation.md` — the "N more gate validators" / "M gated slices" count drifting from the `check_docstring_*` validators the prose names |
84
- | `duplicate_rmtree_helper_blocker.py` | PreToolUse (Write/Edit) | A local re-definition of the Windows-safe rmtree helper trio (`_strip_read_only_and_retry`, `_force_remove_tree` / `force_rmtree`) in place of importing a shared helper |
85
- | `eli11_reply_enforcer.py` | Stop | Final replies breaking the `eli11-replies` shape — more than 6 bullet lines, more than 2 list lines over 20 words each, or multi-line instructions with no numbered step among the lead lines. Code fences, inline code, blockquotes, table rows, and link targets come off before the reply is judged. |
86
- | `env_var_table_code_drift_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | A markdown env-var summary table row attributing an environment variable to a code file whose source never references that variable name |
87
- | `es_exe_path_rewriter.py` | PreToolUse | Rewrites paths referencing `.exe` under the Everything search path |
88
- | `fable_spawn_gate.py` | PreToolUse (Agent/Task) | An `Agent` or `Task` spawn whose prompt carries no `FABLE-SPAWN-AUTHORIZED` token and whose model field reads `fable` in any letter case — the bare alias, or a delimiter segment of a full model id, so `claude-fable-5` is denied too |
89
- | `gh_body_arg_blocker.py` | PreToolUse (Bash) | `gh` commands passing `--body`/`-b` directly (requires `--body-file` instead) |
90
- | `gh_pr_author_enforcer.py` | PreToolUse | Enforces PR author identity rules |
91
- | `gh_pr_author_restore.py` | PostToolUse | Restores PR author after a tool call |
92
- | `hedging_language_blocker.py` | Stop | Responses with hedging words (`likely`, `probably`, `appears to`); armed only when `CLAUDE_PROSE_STYLE_ENFORCEMENT` is on (default off) |
93
- | `hook_prose_detector_consistency.py` | PreToolUse (Write/Edit) | Hook docstrings/messages that claim a trigger the detector cannot fire on; armed only when `CLAUDE_PROSE_STYLE_ENFORCEMENT` is on (default off) |
94
- | `intent_only_ending_blocker.py` | Stop | Responses that end on a plan or intent without doing the work; armed only when `CLAUDE_PROSE_STYLE_ENFORCEMENT` is on (default off) |
95
- | `open_questions_in_plans_blocker.py` | PreToolUse (Write/Edit) | Plan documents with unresolved open questions |
96
- | `nas_ssh_binary_enforcer.py` | PreToolUse (Bash) | A bare `ssh`/`scp`/`sftp` command word targeting the NAS (Git Bash's MSYS ssh stalls on an interactive password prompt), or the full `System32/OpenSSH` binary to that host without `-o BatchMode=yes` |
97
- | `package_inventory_stale_blocker.py` | PreToolUse (Write) | A new production code file created in a directory whose `README.md`/`CLAUDE.md` inventory (or a parent skill's `SKILL.md` Layout table mapping the `scripts/` subdirectory) names two or more sibling files but no entry for the new file |
98
- | `pii_commit_command.py` | library | Token-aware git-commit detection reused by `pii_prevention_blocker.py` |
99
- | `pii_payload_scan.py` | library | Write/Edit and durable post-body PII evaluation reused by `pii_prevention_blocker.py` |
100
- | `pii_prevention_blocker.py` | PreToolUse (Write/Edit/MultiEdit/Bash/PowerShell/MCP GitHub) | Entry hook — content that carries high-confidence personal data or secrets (real emails, home-dir paths, private IPs, credential material) on write, durable GitHub posts, or staged commit paths; resolves the staged-commit repository from the command it gates (via `pii_prevention_blocker_parts`), not the session working directory |
101
- | `pii_scanner.py` | library | Pure text scanners shared by `pii_prevention_blocker.py` |
102
- | `piped_pytest_blocker.py` | PreToolUse (Bash) | A pytest run whose output feeds a pipe, where the pipeline reports the exit code of the command on the right |
103
- | `plain_language_blocker.py` | PreToolUse (Write/Edit/AskUserQuestion) | Heavy or jargon words in user-facing prose when `CLAUDE_PROSE_STYLE_ENFORCEMENT` is on (default off); AskUserQuestion lean-block structure stays always on |
104
- | `pr_converge_bugteam_enforcer.py` | PreToolUse | Enforces that bugteam runs in parallel with bugbot in pr-converge loops |
105
- | `pr_description_enforcer.py` | PreToolUse (Bash) | `gh pr create`/`edit`/`comment` bodies that fail the Anthropic claude-code style audit, proof-shaped `gh pr comment` bodies missing proof-of-work parts, and `gh pr ready` while the PR carries no passing proof comment |
106
- | `precommit_code_rules_gate.py` | PreToolUse (Bash) | Staged changes that fail the CODE_RULES gate at commit time |
107
- | `pytest_testpaths_orphan_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | New `test_*.py` files created under a directory absent from a package's explicit pytest `testpaths` allowlist |
108
- | `question_to_user_enforcer.py` | Stop | User-directed questions not routed through `AskUserQuestion` |
109
- | `reviewer_spawn_gate.py` | PreToolUse (Bash) | A sentinel-marked autoconverge reviewer-spawn command (Copilot review request, Bugbot rerun comment) run while `reviewer_availability.py` reports that reviewer down or out of quota |
110
- | `send_user_file_open_locally_blocker.py` | PreToolUse (SendUserFile) | A desk-side file attach (`SendUserFile` with `status` not `proactive`); points to opening the file locally via `Show-Asset.ps1` |
111
- | `sensitive_file_protector.py` | PreToolUse (Write/Edit) | Writes to sensitive credential or config files |
112
- | `session_edit_stage_gate.py` | PreToolUse (Bash) | A `git commit` that would drop files edited this session because they are tracked but left unstaged |
113
- | `session_handoff_blocker.py` | Stop | Responses suggesting a new session mid-task |
114
- | `shell_substitution_blocker.py` | PreToolUse (Bash) | A command carrying `$(...)`, a live backtick, or `<(...)`/`>(...)` process substitution, which the allowlist matcher cannot descend into |
115
- | `stale_comment_reference_blocker.py` | PreToolUse (Edit) | An Edit that rewrites a Python code line while keeping the standalone comment directly above it, when that comment names an identifier the rewrite removes from the line |
116
- | `state_description_blocker.py` | PreToolUse (Write/Edit) | Historical/comparative language in documentation; armed only when `CLAUDE_PROSE_STYLE_ENFORCEMENT` is on (default off) |
117
- | `subprocess_budget_completeness.py` | PreToolUse | Subprocess calls missing required budget arguments |
118
- | `tdd_enforcer.py` | PreToolUse (Write/Edit) | Production code written without a matching failing test |
119
- | `unscoped_search_blocker.py` | PreToolUse (Bash/PowerShell) | A `find` or recursive listing that walks from the filesystem root, a drive root, bare home, or a network share root |
120
- | `verdict_directory_write_blocker.py` | PreToolUse (Bash/PowerShell) | Shell writes into `~/.claude/verification/` |
121
- | `verified_commit_gate.py` | PreToolUse (Bash/PowerShell) | `git commit`/`git push` without a passing verifier verdict |
122
- | `verified_commit_message_accuracy_blocker.py` | PreToolUse | Commit messages that misstate what the diff has |
123
- | `verifier_verdict_minter.py` | SubagentStop | Mints a passing verdict file when a `code-verifier` agent finishes cleanly |
124
- | `volatile_path_in_post_blocker.py` | PreToolUse (Bash/MCP GitHub) | `gh` post commands and GitHub MCP post tools whose body references a volatile path (job scratch dir, worktree, or system temp) that outlives the durable post |
125
- | `windows_rmtree_blocker.py` | PreToolUse (Write/Edit) | `shutil.rmtree` with `ignore_errors=True` on Windows |
126
- | `workflow_substitution_slot_blocker.py` | PreToolUse (Write/Edit) | Workflow templates with bare per-iteration tokens missing angle-bracket slots |
127
- | `write_existing_file_blocker.py` | PreToolUse (Write) | Write to a path where a file already exists |
128
-
129
- ## Supporting modules
130
-
131
- | File | Role |
132
- |---|---|
133
- | `_gh_body_arg_utils.py` | Parsing helpers for `gh_body_arg_blocker.py` |
134
- | `code_review_enforcement_config_bootstrap.py` | Binds `config.code_review_enforcement_constants` to the sibling `config/` file by explicit location, so the code-review gate family resolves its constants regardless of a foreign `config` package's `sys.path` order |
135
- | `code_review_gate_deny.py` | Shared deny scaffold for the push and PR-create code-review gates: the `hookSpecificOutput` deny-payload builder and the log-and-emit helper, so both gates share one deny shape |
136
- | `code_review_stamp_store.py` | Reads and writes the per-work-tree code-review stamp files under `~/.claude/code-review-stamps/`, and decides whether a clean stamp at the needed effort covers the live branch surface |
137
- | `pr_description_body_audit.py` | Body audit logic for `pr_description_enforcer.py` |
138
- | `pr_description_command_parser.py` | `gh` command parsing for `pr_description_enforcer.py` |
139
- | `pr_description_pr_number.py` | PR number extraction logic |
140
- | `pr_description_proof_of_work.py` | Proof-of-work comment audit and `gh pr ready` gate logic for `pr_description_enforcer.py` |
141
- | `pr_description_readability.py` | Readability checks on PR description bodies |
142
- | `verification_verdict_store.py` | Reads and writes verdict files under `~/.claude/verification/` |
143
- | `verified_commit_config_bootstrap.py` | Binds `config.verified_commit_constants` to the sibling `config/` file by explicit location, so the gate family resolves its constants regardless of a foreign `config` package's `sys.path` order |
144
-
145
- ## Conventions
146
-
147
- - Tests live beside each hook as `test_<hookname>.py` or `test_<hookname>_<suffix>.py`. Run with `python -m pytest <test_file>`.
148
- - Tunable constants live in `hooks_constants/<hook_name>_constants.py`; the verified-commit family uses `blocking/config/verified_commit_constants.py`.
1
+ @AGENTS.md
@@ -0,0 +1,28 @@
1
+ # claude_md_orphan_file_blocker_parts
2
+
3
+ The concern modules `claude_md_orphan_file_blocker.py` wires together to block a
4
+ per-directory `CLAUDE.md` that names a file absent from its subtree. The entry
5
+ hook imports them and re-exports their surface for the test suite.
6
+
7
+ ## Modules
8
+
9
+ | File | Purpose |
10
+ |---|---|
11
+ | `references.py` | Extracts the bare filenames a table cell names and the scripts a fenced run command invokes, honoring the `../` relative-path exemptions |
12
+ | `subtree_scan.py` | Resolves the scan root and reports which referenced filenames are absent from it, with a bounded walk plus a direct probe fallback |
13
+ | `scan_plan.py` | Builds the Write/Edit/MultiEdit scan plan and collects the orphan filenames the change introduces, excluding pre-existing orphans |
14
+ | `decision.py` | Builds the deny payload listing the missing files and closing with the retry hint, and emits the decision JSON |
15
+ | `__init__.py` | Package marker |
16
+
17
+ ## Subdirectories
18
+
19
+ | Entry | Description |
20
+ |---|---|
21
+ | `config/` | The region-join newline, the retry hint the deny reason closes with, and the missing-name join separator (`orphan_blocker_constants.py`) |
22
+ | `tests/` | pytest suite with one test module per module above |
23
+
24
+ ## Running tests
25
+
26
+ ```bash
27
+ python -m pytest packages/claude-dev-env/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/
28
+ ```
@@ -1,28 +1 @@
1
- # claude_md_orphan_file_blocker_parts
2
-
3
- The concern modules `claude_md_orphan_file_blocker.py` wires together to block a
4
- per-directory `CLAUDE.md` that names a file absent from its subtree. The entry
5
- hook imports them and re-exports their surface for the test suite.
6
-
7
- ## Modules
8
-
9
- | File | Purpose |
10
- |---|---|
11
- | `references.py` | Extracts the bare filenames a table cell names and the scripts a fenced run command invokes, honoring the `../` relative-path exemptions |
12
- | `subtree_scan.py` | Resolves the scan root and reports which referenced filenames are absent from it, with a bounded walk plus a direct probe fallback |
13
- | `scan_plan.py` | Builds the Write/Edit/MultiEdit scan plan and collects the orphan filenames the change introduces, excluding pre-existing orphans |
14
- | `decision.py` | Builds the deny payload listing the missing files and closing with the retry hint, and emits the decision JSON |
15
- | `__init__.py` | Package marker |
16
-
17
- ## Subdirectories
18
-
19
- | Entry | Description |
20
- |---|---|
21
- | `config/` | The region-join newline, the retry hint the deny reason closes with, and the missing-name join separator (`orphan_blocker_constants.py`) |
22
- | `tests/` | pytest suite with one test module per module above |
23
-
24
- ## Running tests
25
-
26
- ```bash
27
- python -m pytest packages/claude-dev-env/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/
28
- ```
1
+ @AGENTS.md
@@ -390,7 +390,7 @@ def check_banned_noun_word_boundary(
390
390
  def check_banned_prefixes(content: str, file_path: str) -> list[str]:
391
391
  """Flag function and method names using generic banned prefixes.
392
392
 
393
- Per CODE_RULES.md / AGENTS.md Naming, function names use specific verbs.
393
+ Per CODE_RULES.md Naming, function names use specific verbs.
394
394
  Generic prefixes ``handle_``, ``process_``, ``manage_``, ``do_`` are
395
395
  placeholders that hide the actual responsibility and are flagged so the
396
396
  author renames the function to a specific verb.
@@ -29,13 +29,20 @@ The scan is deliberately conservative to keep false positives near zero:
29
29
  exported constant live.
30
30
  - When the package-tree scan leaves a constant unreferenced, the scan widens to
31
31
  the repository root (the nearest ``.git`` ancestor). The widened pass counts a
32
- sibling-tree reference only when a module imports the name through a
33
- ``from <module> import`` whose final dotted segment equals the written
34
- module's filename stem, so a genuine cross-tree consumer of this constants
35
- module keeps the constant live while a same-named constant exported by an
36
- unrelated module never masks a dead one. The widened pass reads a repository
37
- file only to test whether its text names the written module's filename stem;
38
- a file that never mentions the stem cannot carry such an import, so it is
32
+ sibling-tree reference in two shapes. The first is an import of the name
33
+ through a ``from <module> import`` whose final dotted segment equals the
34
+ written module's filename stem. The second is an attribute read on a binding
35
+ of the written module itself: a consumer that writes
36
+ ``import pkg.config.constants``, the same with an alias,
37
+ ``from pkg.config import constants``, or the same with an alias, and then
38
+ reads each constant as an attribute on that binding (``alias.NAME``,
39
+ ``pkg.config.constants.NAME``), keeps every name it reads that way live. Both
40
+ shapes are bound to the written module's stem, so a genuine cross-tree
41
+ consumer of this constants module keeps the constant live while a same-named
42
+ constant exported by an unrelated module never masks a dead one. The widened
43
+ pass reads a repository file only to test whether its text names the written
44
+ module's filename stem; neither shape can appear in a file that never spells
45
+ the stem, so such a file is
39
46
  skipped without spending scan-cap budget, keeping the widened pass bounded to
40
47
  the handful of candidate importer files even in a large repository. A module
41
48
  outside any repository is
@@ -65,6 +72,7 @@ if _blocking_directory not in sys.path:
65
72
  if _hooks_directory not in sys.path:
66
73
  sys.path.insert(0, _hooks_directory)
67
74
 
75
+ from code_rules_probe_chains import _dotted_attribute_chain # noqa: E402
68
76
  from code_rules_shared import ( # noqa: E402
69
77
  is_migration_file,
70
78
  is_test_file,
@@ -268,13 +276,116 @@ def _module_final_segment(module_path: str | None) -> str:
268
276
  return module_path.rsplit(".", 1)[-1]
269
277
 
270
278
 
271
- def _qualified_import_member_names(source: str, module_stem: str) -> set[str]:
272
- """Return names imported from a module whose filename stem is ``module_stem``.
279
+ def _stem_import_member_names(import_node: ast.ImportFrom, module_stem: str) -> set[str]:
280
+ """Return the member names a stem-matched ``from ... import`` brings in.
273
281
 
274
- A cross-package consumer of an exported constant imports it through an
275
- explicit ``from <module> import NAME`` whose module path ends in the defining
276
- module's filename stem. Collecting only those member names binds a
277
- widened-scan reference to the module that actually defines the constant, so a
282
+ ::
283
+
284
+ from pkg.config.constants import NAME -> "NAME"
285
+ from pkg.config.other import NAME -> nothing (stem mismatch)
286
+
287
+ Args:
288
+ import_node: A ``from ... import`` statement node.
289
+ module_stem: The filename stem of the constants module being judged.
290
+
291
+ Returns:
292
+ The imported member names when the module path's final dotted segment
293
+ equals ``module_stem``, empty otherwise.
294
+ """
295
+ if _module_final_segment(import_node.module) != module_stem:
296
+ return set()
297
+ return {each_alias.name for each_alias in import_node.names}
298
+
299
+
300
+ def _import_module_bindings(all_import_aliases: list[ast.alias], module_stem: str) -> set[str]:
301
+ """Return the names an import's aliases bind to the constants module object.
302
+
303
+ ::
304
+
305
+ import pkg.config.constants -> "pkg.config.constants"
306
+ import pkg.config.constants as sg -> "sg"
307
+ from pkg.config import constants -> "constants"
308
+ from . import constants as sg -> "sg"
309
+ import pkg.config.other -> nothing (stem mismatch)
310
+
311
+ A plain ``import`` names the module by its dotted path, so the final
312
+ segment is matched against the stem. A ``from ... import`` alias name is a
313
+ single identifier, which the same final-segment match covers unchanged.
314
+
315
+ Args:
316
+ all_import_aliases: The ``names`` list of an ``Import`` or
317
+ ``ImportFrom`` node.
318
+ module_stem: The filename stem of the constants module being judged.
319
+
320
+ Returns:
321
+ The dotted paths and aliases through which the module object is
322
+ reachable.
323
+ """
324
+ binding_names: set[str] = set()
325
+ for each_alias in all_import_aliases:
326
+ if _module_final_segment(each_alias.name) != module_stem:
327
+ continue
328
+ binding_names.add(each_alias.asname or each_alias.name)
329
+ return binding_names
330
+
331
+
332
+ def _collect_widened_scan_facts(
333
+ tree: ast.Module, module_stem: str
334
+ ) -> tuple[set[str], set[str], list[ast.Attribute]]:
335
+ """Collect a candidate module's import and attribute facts in one walk.
336
+
337
+ Args:
338
+ tree: The parsed candidate module.
339
+ module_stem: The filename stem of the constants module being judged.
340
+
341
+ Returns:
342
+ The member names imported through a stem-matched ``from ... import``,
343
+ the names bound to the constants module object itself, and every
344
+ attribute node in the module.
345
+ """
346
+ member_names: set[str] = set()
347
+ binding_names: set[str] = set()
348
+ all_attribute_nodes: list[ast.Attribute] = []
349
+ for each_node in ast.walk(tree):
350
+ if isinstance(each_node, ast.ImportFrom):
351
+ member_names |= _stem_import_member_names(each_node, module_stem)
352
+ if isinstance(each_node, (ast.Import, ast.ImportFrom)):
353
+ binding_names |= _import_module_bindings(each_node.names, module_stem)
354
+ if isinstance(each_node, ast.Attribute):
355
+ all_attribute_nodes.append(each_node)
356
+ return member_names, binding_names, all_attribute_nodes
357
+
358
+
359
+ def _attribute_read_names(
360
+ all_attribute_nodes: list[ast.Attribute], all_binding_names: set[str]
361
+ ) -> set[str]:
362
+ """Return the constant names read as attributes on a module-object binding.
363
+
364
+ The expression an attribute is read on is spelled back to its dotted source
365
+ form and matched against the bindings, so an alias read (``sg.NAME``) and a
366
+ full dotted path (``pkg.config.constants.NAME``) both resolve, while a read
367
+ on a binding of some other module counts for nothing.
368
+
369
+ Args:
370
+ all_attribute_nodes: Every attribute node in the candidate module.
371
+ all_binding_names: The names bound to the constants module object.
372
+
373
+ Returns:
374
+ The attribute names read on a binding of the constants module.
375
+ """
376
+ if not all_binding_names:
377
+ return set()
378
+ return {
379
+ each_attribute.attr
380
+ for each_attribute in all_attribute_nodes
381
+ if _dotted_attribute_chain(each_attribute.value) in all_binding_names
382
+ }
383
+
384
+
385
+ def _widened_scan_reference_names(source: str, module_stem: str) -> set[str]:
386
+ """Return the names a repository-wide candidate contributes, parsing it once.
387
+
388
+ Both counted shapes are bound to the written module's filename stem, so a
278
389
  same-named constant exported by an unrelated module never masks a dead one.
279
390
 
280
391
  Args:
@@ -282,22 +393,18 @@ def _qualified_import_member_names(source: str, module_stem: str) -> set[str]:
282
393
  module_stem: The filename stem of the constants module being judged.
283
394
 
284
395
  Returns:
285
- The member names imported from a module whose final dotted segment equals
286
- ``module_stem``. A module that fails to parse contributes no names.
396
+ The members imported through a stem-matched ``from <module> import``,
397
+ plus the constant names read as attributes on an imported binding of the
398
+ constants module. A module that fails to parse contributes no names.
287
399
  """
288
400
  try:
289
401
  tree = ast.parse(source)
290
402
  except SyntaxError:
291
403
  return set()
292
- member_names: set[str] = set()
293
- for each_node in ast.walk(tree):
294
- if not isinstance(each_node, ast.ImportFrom):
295
- continue
296
- if _module_final_segment(each_node.module) != module_stem:
297
- continue
298
- for each_alias in each_node.names:
299
- member_names.add(each_alias.name)
300
- return member_names
404
+ member_names, binding_names, all_attribute_nodes = _collect_widened_scan_facts(
405
+ tree, module_stem
406
+ )
407
+ return member_names | _attribute_read_names(all_attribute_nodes, binding_names)
301
408
 
302
409
 
303
410
  def _scan_root_for_constants_module(file_path: str) -> Path:
@@ -350,11 +457,13 @@ def _read_candidate_source(file_path: Path, required_substring: str | None) -> s
350
457
  required_substring = None -> read and keep every file
351
458
  required_substring = the module stem -> keep only files naming it
352
459
  from pkg.foo_constants import BAR -> names the stem -> candidate
460
+ from pkg import foo_constants as c -> names the stem -> candidate
353
461
  def unrelated() -> int: ... -> no stem mention -> skipped
354
462
 
355
- The widened repository pass looks only for a ``from <module> import`` whose
356
- final dotted segment equals the constants module's filename stem, and such an
357
- import always spells that stem in the file's text. A file whose text never
463
+ The widened repository pass looks only for an import bound to the constants
464
+ module's filename stem a ``from <module> import`` whose final dotted segment
465
+ equals the stem, or an import of the module object itself and every such
466
+ import spells that stem in the file's text. A file whose text never
358
467
  mentions the stem cannot carry the import, so returning None for it lets the
359
468
  caller skip the file without spending scan-cap budget, which keeps the widened
360
469
  pass bounded to the candidate importer files even under a large repository.
@@ -422,7 +531,9 @@ def _collect_names_under_root(
422
531
  in before the walk begins.
423
532
  extract_names: Maps one module's source text to the set of names it
424
533
  contributes — the generous reference collector for the package-tree
425
- pass, the stem-bound import collector for the widened pass.
534
+ pass, and for the widened pass the stem-bound collector that counts
535
+ both a stem-matched from-import and an attribute read on a binding
536
+ of the written module.
426
537
  already_scanned_count: The parsed-file count accumulated by a prior
427
538
  pass, so the parse cap bounds the combined work of the
428
539
  package-tree and widened passes.
@@ -552,12 +663,13 @@ def check_dead_module_constants(
552
663
  are governed by the use-count rule instead. A constant is dead when its name
553
664
  appears in no ``.py`` module under the enclosing package tree — not imported,
554
665
  not read, not listed in another module's ``__all__`` literal, not named in a
555
- string annotation and, in the repository-wide scan the check widens to when
556
- the package-tree scan leaves the constant unreferenced, no module imports the
557
- name from a ``from <module> import`` whose final dotted segment equals this
558
- module's filename stem. Binding the widened scan to the stem keeps a genuine
559
- cross-tree consumer counting while a same-named constant exported by an
560
- unrelated module never masks a dead one. A module declaring ``__all__``
666
+ string annotation. When that leaves a constant unreferenced, the scan widens
667
+ to the repository, where two shapes count: a ``from <module> import`` whose
668
+ final dotted segment equals this module's filename stem, and an attribute
669
+ read on a binding of this module (``import pkg.config.constants as alias``
670
+ then the alias followed by the constant name). Binding both to the stem
671
+ keeps a same-named constant in an unrelated module from masking a dead one.
672
+ A module declaring ``__all__``
561
673
  narrows the check to the constants its ``__all__`` list names: each must be
562
674
  imported or read by another module, and the module's own ``__all__`` entry
563
675
  never counts as that consumer, so an exported constant no module consumes is
@@ -616,14 +728,14 @@ def check_dead_module_constants(
616
728
  if has_unreferenced_constant:
617
729
  repository_root = _repository_root_for(written_path)
618
730
  if repository_root is not None and repository_root != scan_root:
619
- collect_qualified_imports = partial(
620
- _qualified_import_member_names, module_stem=written_path.stem
731
+ collect_widened_scan_names = partial(
732
+ _widened_scan_reference_names, module_stem=written_path.stem
621
733
  )
622
734
  widened_names, _widened_count, widened_cap_was_hit = _collect_names_under_root(
623
735
  repository_root,
624
736
  written_path,
625
737
  set(),
626
- collect_qualified_imports,
738
+ collect_widened_scan_names,
627
739
  already_scanned_count=scanned_file_count,
628
740
  excluded_subtree=scan_root,
629
741
  required_substring=written_path.stem,