claude-dev-env 1.38.1 → 1.40.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 (282) hide show
  1. package/CLAUDE.md +10 -36
  2. package/_shared/pr-loop/audit-reply-template.md +147 -0
  3. package/_shared/pr-loop/fix-protocol.md +25 -4
  4. package/_shared/pr-loop/gh-payloads.md +37 -50
  5. package/_shared/pr-loop/scripts/code_rules_gate.py +0 -60
  6. package/_shared/pr-loop/scripts/config/post_audit_thread_constants.py +199 -0
  7. package/_shared/pr-loop/scripts/config/reviews_disabled_constants.py +8 -0
  8. package/_shared/pr-loop/scripts/post_audit_thread.py +1242 -0
  9. package/_shared/pr-loop/scripts/preflight.py +129 -2
  10. package/_shared/pr-loop/scripts/reviews_disabled.py +59 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -19
  12. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +1116 -0
  13. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +127 -0
  14. package/_shared/pr-loop/scripts/tests/test_preflight.py +41 -0
  15. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +36 -0
  16. package/_shared/pr-loop/state-schema.md +1 -1
  17. package/agents/clean-coder.md +2 -2
  18. package/agents/pr-description-writer.md +150 -52
  19. package/bin/install.mjs +6 -7
  20. package/bin/install.test.mjs +8 -0
  21. package/commands/doc-gist.md +16 -0
  22. package/commands/plan.md +0 -2
  23. package/commands/review-plan.md +1 -1
  24. package/docs/CODE_RULES.md +122 -2
  25. package/docs/PR_DESCRIPTION_GUIDE.md +127 -64
  26. package/hooks/blocking/bot_mention_comment_blocker.py +75 -0
  27. package/hooks/blocking/code_rules_enforcer.py +1143 -129
  28. package/hooks/blocking/convergence_gate_blocker.py +130 -0
  29. package/hooks/blocking/destructive_command_blocker.py +74 -0
  30. package/hooks/blocking/gh_body_arg_blocker.py +30 -0
  31. package/hooks/blocking/md_to_html_blocker.py +119 -0
  32. package/hooks/blocking/pr_description_enforcer.py +57 -22
  33. package/hooks/blocking/test_bot_mention_comment_blocker.py +131 -0
  34. package/hooks/blocking/test_code_rules_enforcer.py +21 -0
  35. package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +70 -0
  36. package/hooks/blocking/test_code_rules_enforcer_any_imports_and_cast.py +92 -0
  37. package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +143 -0
  38. package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +152 -0
  39. package/hooks/blocking/test_code_rules_enforcer_bare_except.py +120 -0
  40. package/hooks/blocking/test_code_rules_enforcer_boundary_types.py +175 -0
  41. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  42. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +50 -0
  43. package/hooks/blocking/test_code_rules_enforcer_docstring_format.py +255 -0
  44. package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +130 -0
  45. package/hooks/blocking/test_code_rules_enforcer_stub_implementations.py +141 -0
  46. package/hooks/blocking/test_code_rules_enforcer_test_branching.py +143 -0
  47. package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +169 -0
  48. package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +99 -0
  49. package/hooks/blocking/test_code_rules_enforcer_typed_dict_pairs.py +141 -0
  50. package/hooks/blocking/test_convergence_gate_blocker.py +63 -0
  51. package/hooks/blocking/test_destructive_command_blocker.py +146 -0
  52. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +102 -0
  53. package/hooks/blocking/test_gh_body_arg_blocker.py +45 -0
  54. package/hooks/blocking/test_md_to_html_blocker.py +317 -0
  55. package/hooks/blocking/test_pr_description_enforcer.py +69 -8
  56. package/hooks/config/any_type_config.py +7 -0
  57. package/hooks/config/banned_identifiers_constants.py +11 -0
  58. package/hooks/config/blocking_check_limits.py +38 -0
  59. package/hooks/config/bot_mention_comment_blocker_constants.py +20 -0
  60. package/hooks/config/code_rules_enforcer_constants.py +53 -0
  61. package/hooks/config/convergence_branch_constants.py +9 -0
  62. package/hooks/config/doc_gist_auto_publish_constants.py +18 -0
  63. package/hooks/config/html_companion_constants.py +20 -0
  64. package/hooks/config/inline_tuple_string_magic_constants.py +22 -0
  65. package/hooks/config/pr_description_enforcer_constants.py +14 -0
  66. package/hooks/config/test_banned_identifiers_constants.py +17 -0
  67. package/hooks/hooks.json +28 -20
  68. package/hooks/pyproject.toml +69 -0
  69. package/hooks/validators/mypy_integration.py +47 -1
  70. package/hooks/validators/run_all_validators.py +3 -3
  71. package/hooks/validators/test_mypy_integration.py +50 -1
  72. package/hooks/workflow/doc_gist_auto_publish.py +144 -0
  73. package/hooks/workflow/md_to_html_companion.py +365 -0
  74. package/hooks/workflow/test_doc_gist_auto_publish.py +117 -0
  75. package/hooks/workflow/test_md_to_html_companion.py +452 -0
  76. package/package.json +1 -1
  77. package/rules/gh-body-file.md +2 -0
  78. package/scripts/Install-SweepEmptyDirs.ps1 +111 -0
  79. package/scripts/check.ps1 +106 -0
  80. package/scripts/config/timing.py +11 -0
  81. package/scripts/sweep_empty_dirs.py +138 -0
  82. package/scripts/sync_to_cursor/rules.py +1 -1
  83. package/scripts/test_sweep_empty_dirs.py +183 -0
  84. package/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +323 -0
  85. package/skills/_shared/pr-loop/scripts/_cli_utils.py +22 -0
  86. package/skills/_shared/pr-loop/scripts/_path_resolver.py +165 -0
  87. package/skills/_shared/pr-loop/scripts/_xml_utils.py +20 -0
  88. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +182 -0
  89. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +185 -0
  90. package/skills/_shared/pr-loop/scripts/config/__init__.py +0 -0
  91. package/skills/_shared/pr-loop/scripts/config/path_resolver_constants.py +78 -0
  92. package/skills/_shared/pr-loop/scripts/init_loop_state.py +135 -0
  93. package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +175 -0
  94. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +182 -0
  95. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +206 -0
  96. package/skills/bugteam/CONSTRAINTS.md +21 -22
  97. package/skills/bugteam/EXAMPLES.md +3 -3
  98. package/skills/bugteam/PROMPTS.md +227 -67
  99. package/skills/bugteam/SKILL.md +132 -455
  100. package/skills/bugteam/reference/README.md +1 -1
  101. package/skills/bugteam/reference/audit-and-teammates.md +112 -39
  102. package/skills/bugteam/reference/audit-contract.md +4 -22
  103. package/skills/bugteam/reference/copilot-gap-analysis.md +8 -5
  104. package/skills/bugteam/reference/design-rationale.md +2 -2
  105. package/skills/bugteam/reference/github-pr-reviews.md +50 -57
  106. package/skills/bugteam/reference/obstacles/audit-assign-ids.md +13 -0
  107. package/skills/bugteam/reference/obstacles/audit-capture-excerpts.md +13 -0
  108. package/skills/bugteam/reference/obstacles/audit-walk-categories.md +13 -0
  109. package/skills/bugteam/reference/obstacles/audit-write-xml.md +13 -0
  110. package/skills/bugteam/reference/obstacles/fix-append-summary.md +13 -0
  111. package/skills/bugteam/reference/obstacles/fix-apply-fixes.md +13 -0
  112. package/skills/bugteam/reference/obstacles/fix-git-add-commit.md +13 -0
  113. package/skills/bugteam/reference/obstacles/fix-git-push.md +13 -0
  114. package/skills/bugteam/reference/obstacles/fix-post-reply.md +13 -0
  115. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +13 -0
  116. package/skills/bugteam/reference/obstacles/fix-py-compile.md +13 -0
  117. package/skills/bugteam/reference/obstacles/fix-read-files.md +13 -0
  118. package/skills/bugteam/reference/obstacles/fix-resolve-thread.md +13 -0
  119. package/skills/bugteam/reference/obstacles/fix-test-suite.md +13 -0
  120. package/skills/bugteam/reference/obstacles/fix-violation-count.md +13 -0
  121. package/skills/bugteam/reference/obstacles/fix-write-xml.md +13 -0
  122. package/skills/bugteam/reference/team-setup.md +111 -9
  123. package/skills/bugteam/reference/teardown-publish-permissions.md +39 -8
  124. package/skills/bugteam/scripts/README.md +60 -0
  125. package/skills/bugteam/scripts/_claude_permissions_common.py +358 -0
  126. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +976 -0
  127. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +375 -0
  128. package/skills/bugteam/scripts/bugteam_preflight.py +328 -0
  129. package/skills/bugteam/scripts/config/bugteam_code_rules_gate_constants.py +25 -0
  130. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +26 -0
  131. package/skills/bugteam/scripts/config/bugteam_preflight_constants.py +35 -0
  132. package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +20 -0
  133. package/skills/bugteam/scripts/config/probe_code_rules_enforcer_check_constants.py +12 -0
  134. package/skills/bugteam/scripts/config/windows_safe_rmtree_constants.py +7 -0
  135. package/skills/bugteam/scripts/grant_project_claude_permissions.py +175 -0
  136. package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +107 -0
  137. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +220 -0
  138. package/skills/bugteam/scripts/test__claude_permissions_common.py +112 -0
  139. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +400 -0
  140. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +384 -0
  141. package/skills/bugteam/scripts/test_bugteam_preflight.py +309 -0
  142. package/skills/bugteam/scripts/test_claude_permissions_common.py +195 -0
  143. package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +55 -0
  144. package/skills/bugteam/scripts/test_probe_code_rules_enforcer_check.py +76 -0
  145. package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +55 -0
  146. package/skills/bugteam/scripts/test_windows_safe_rmtree.py +108 -0
  147. package/skills/bugteam/scripts/windows_safe_rmtree.py +100 -0
  148. package/skills/bugteam/test_skill_additions.py +1 -11
  149. package/skills/code/SKILL.md +176 -0
  150. package/skills/copilot-review/SKILL.md +16 -0
  151. package/skills/doc-gist/SKILL.md +99 -0
  152. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +453 -0
  153. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +515 -0
  154. package/skills/doc-gist/references/examples/03-code-review-pr.html +638 -0
  155. package/skills/doc-gist/references/examples/04-code-understanding.html +491 -0
  156. package/skills/doc-gist/references/examples/05-design-system.html +629 -0
  157. package/skills/doc-gist/references/examples/06-component-variants.html +605 -0
  158. package/skills/doc-gist/references/examples/07-prototype-animation.html +455 -0
  159. package/skills/doc-gist/references/examples/08-prototype-interaction.html +396 -0
  160. package/skills/doc-gist/references/examples/09-slide-deck.html +592 -0
  161. package/skills/doc-gist/references/examples/10-svg-illustrations.html +492 -0
  162. package/skills/doc-gist/references/examples/11-status-report.html +528 -0
  163. package/skills/doc-gist/references/examples/12-incident-report.html +596 -0
  164. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +395 -0
  165. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +381 -0
  166. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +368 -0
  167. package/skills/doc-gist/references/examples/16-implementation-plan.html +702 -0
  168. package/skills/doc-gist/references/examples/17-pr-writeup.html +595 -0
  169. package/skills/doc-gist/references/examples/18-editor-triage-board.html +573 -0
  170. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +663 -0
  171. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +722 -0
  172. package/skills/doc-gist/references/examples/README.md +5 -0
  173. package/skills/doc-gist/scripts/config/__init__.py +0 -0
  174. package/skills/doc-gist/scripts/config/gist_upload_constants.py +16 -0
  175. package/skills/doc-gist/scripts/gist_upload.py +177 -0
  176. package/skills/doc-gist/scripts/test_gist_upload.py +51 -0
  177. package/skills/findbugs/SKILL.md +96 -2
  178. package/skills/monitor-open-prs/SKILL.md +14 -32
  179. package/skills/monitor-open-prs/test_skill_contract.py +0 -11
  180. package/skills/pr-consistency-audit/SKILL.md +112 -0
  181. package/skills/pr-consistency-audit/reference/detection-rules.md +96 -0
  182. package/skills/pr-consistency-audit/reference/illustrations.md +78 -0
  183. package/skills/pr-converge/SKILL.md +229 -23
  184. package/skills/pr-converge/config/__init__.py +0 -0
  185. package/skills/pr-converge/config/constants.py +63 -0
  186. package/skills/pr-converge/reference/convergence-gates.md +138 -44
  187. package/skills/pr-converge/reference/examples.md +43 -11
  188. package/skills/pr-converge/reference/fix-protocol.md +6 -5
  189. package/skills/pr-converge/reference/ground-rules.md +5 -3
  190. package/skills/pr-converge/reference/multi-pr-orchestration.md +44 -19
  191. package/skills/pr-converge/reference/obstacles/fix-post-replies.md +13 -0
  192. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +13 -0
  193. package/skills/pr-converge/reference/obstacles/fix-push.md +13 -0
  194. package/skills/pr-converge/reference/obstacles/fix-read-filelines.md +13 -0
  195. package/skills/pr-converge/reference/obstacles/fix-reset-state.md +13 -0
  196. package/skills/pr-converge/reference/obstacles/fix-resolve-threads.md +13 -0
  197. package/skills/pr-converge/reference/obstacles/fix-spawn-clean-coder.md +13 -0
  198. package/skills/pr-converge/reference/obstacles/fix-stage-commit.md +13 -0
  199. package/skills/pr-converge/reference/obstacles/fix-trigger-bugbot.md +13 -0
  200. package/skills/pr-converge/reference/obstacles/fix-write-test.md +13 -0
  201. package/skills/pr-converge/reference/per-tick.md +107 -31
  202. package/skills/pr-converge/reference/state-schema.md +22 -1
  203. package/skills/pr-converge/reference/stop-conditions.md +9 -7
  204. package/skills/pr-converge/scripts/README.md +34 -46
  205. package/skills/pr-converge/scripts/check_bugbot_ci.py +279 -0
  206. package/skills/pr-converge/scripts/check_convergence.py +497 -0
  207. package/skills/pr-converge/scripts/check_pending_reviews.py +154 -0
  208. package/skills/pr-converge/scripts/config/pr_converge_constants.py +118 -0
  209. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +134 -0
  210. package/skills/pr-converge/scripts/post_fix_reply.py +168 -0
  211. package/skills/pr-converge/scripts/test_check_bugbot_ci.py +312 -0
  212. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -12
  213. package/skills/qbug/SKILL.md +157 -27
  214. package/skills/session-log/SKILL.md +216 -114
  215. package/skills/session-tidy/SKILL.md +1 -1
  216. package/skills/skill-builder/SKILL.md +138 -56
  217. package/skills/skill-builder/references/delegation-map.md +72 -113
  218. package/skills/skill-builder/references/progressive-disclosure.md +122 -0
  219. package/skills/skill-builder/references/self-audit-checklist.md +92 -0
  220. package/skills/skill-builder/references/skill-types.md +228 -0
  221. package/skills/skill-builder/references/thariq-x-post-skills.json +33 -0
  222. package/skills/skill-builder/templates/gap-analysis.md +15 -8
  223. package/skills/skill-builder/workflows/improve-skill.md +86 -57
  224. package/skills/skill-builder/workflows/new-skill.md +80 -168
  225. package/skills/skill-builder/workflows/polish-skill.md +78 -54
  226. package/skills/structure-prompt/SKILL.md +50 -0
  227. package/skills/structure-prompt/reference/adversarial-tuning.md +62 -0
  228. package/skills/structure-prompt/reference/block-classification.md +27 -0
  229. package/skills/structure-prompt/reference/canonical-case.md +48 -0
  230. package/skills/structure-prompt/reference/citation-depth.md +70 -0
  231. package/skills/structure-prompt/reference/cleanup.md +33 -0
  232. package/skills/structure-prompt/reference/constraints.md +33 -0
  233. package/skills/structure-prompt/reference/directives.md +37 -0
  234. package/skills/structure-prompt/reference/examples.md +72 -0
  235. package/skills/structure-prompt/reference/instantiation.md +51 -0
  236. package/skills/structure-prompt/reference/output-contract.md +72 -0
  237. package/skills/structure-prompt/reference/per-category.md +23 -0
  238. package/skills/structure-prompt/reference/persona.md +38 -0
  239. package/skills/structure-prompt/reference/research.md +33 -0
  240. package/skills/structure-prompt/reference/structure.md +28 -0
  241. package/agents/code-standards-agent.md +0 -93
  242. package/agents/groq-coder.md +0 -113
  243. package/agents/plan-executor.md +0 -226
  244. package/agents/project-docs-analyzer.md +0 -53
  245. package/agents/project-structure-organizer-agent.md +0 -72
  246. package/agents/skill-to-agent-converter.md +0 -370
  247. package/agents/skill-writer-agent.md +0 -470
  248. package/agents/user-docs-writer.md +0 -67
  249. package/agents/workflow-visual-documenter.md +0 -82
  250. package/commands/readability-review.md +0 -20
  251. package/hooks/mypy.ini +0 -2
  252. package/hooks/notification/attention_needed_notify.py +0 -71
  253. package/hooks/notification/claude_notification_handler.py +0 -67
  254. package/hooks/notification/notification_utils.py +0 -267
  255. package/hooks/notification/subagent_complete_notify.py +0 -381
  256. package/hooks/notification/test_attention_needed_notify.py +0 -47
  257. package/hooks/notification/test_claude_notification_handler.py +0 -54
  258. package/hooks/notification/test_notification_utils.py +0 -91
  259. package/hooks/notification/test_subagent_complete_notify.py +0 -79
  260. package/scripts/config/groq_bugteam_config.py +0 -230
  261. package/scripts/config/test_groq_bugteam_config.py +0 -83
  262. package/scripts/config/test_spec_implementer_prompt.py +0 -32
  263. package/scripts/groq_bugteam.README.md +0 -131
  264. package/scripts/groq_bugteam.py +0 -647
  265. package/scripts/groq_bugteam_dotenv.py +0 -40
  266. package/scripts/groq_bugteam_spec.py +0 -226
  267. package/scripts/test_groq_bugteam.py +0 -529
  268. package/scripts/test_groq_bugteam_apply_fix_from_spec.py +0 -426
  269. package/scripts/test_groq_bugteam_dotenv.py +0 -66
  270. package/scripts/test_groq_bugteam_spec.py +0 -338
  271. package/skills/bugteam/SKILL_EVALS.md +0 -309
  272. package/skills/dream/SKILL.md +0 -118
  273. package/skills/ingest/SKILL.md +0 -40
  274. package/skills/npm-creator/SKILL.md +0 -187
  275. package/skills/readability-review/SKILL.md +0 -127
  276. package/skills/resume-review/SKILL.md +0 -261
  277. package/skills/rule-audit/SKILL.md +0 -307
  278. package/skills/rule-creator/SKILL.md +0 -150
  279. package/skills/searching-obsidian-vault/SKILL.md +0 -131
  280. package/skills/skill-writer/REFERENCE.md +0 -284
  281. package/skills/skill-writer/SKILL.md +0 -222
  282. package/skills/tdd-team/SKILL.md +0 -128
@@ -0,0 +1,317 @@
1
+ """Tests for md_to_html_blocker hook."""
2
+
3
+ import importlib
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import sys
8
+
9
+
10
+ HOOK_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "md_to_html_blocker.py")
11
+
12
+
13
+ class _RunHook:
14
+ def __call__(self, tool_name: str, tool_input: dict) -> subprocess.CompletedProcess:
15
+ payload = json.dumps({"tool_name": tool_name, "tool_input": tool_input})
16
+ return subprocess.run(
17
+ [sys.executable, HOOK_SCRIPT_PATH],
18
+ input=payload,
19
+ capture_output=True,
20
+ text=True,
21
+ check=False,
22
+ )
23
+
24
+
25
+ _run_hook = _RunHook()
26
+
27
+
28
+ def test_exempt_root_filenames_are_module_constant():
29
+ """Exempt root filenames should be a module-level constant, not inline in the function body."""
30
+ hook_dir = os.path.dirname(HOOK_SCRIPT_PATH)
31
+ if hook_dir not in sys.path:
32
+ sys.path.insert(0, hook_dir)
33
+
34
+ blocker_module = importlib.import_module("md_to_html_blocker")
35
+ importlib.reload(blocker_module)
36
+
37
+ assert hasattr(blocker_module, "_exempt_root_filenames")
38
+ assert "readme.md" in blocker_module._exempt_root_filenames
39
+ assert "changelog.md" in blocker_module._exempt_root_filenames
40
+
41
+
42
+ def test_blocks_write_md_file():
43
+ result = _run_hook(
44
+ "Write",
45
+ {"file_path": "docs/guide.md", "content": "# Hello"},
46
+ )
47
+ assert result.returncode == 0
48
+ output = json.loads(result.stdout)
49
+ assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
50
+
51
+
52
+ def test_blocks_edit_md_file():
53
+ result = _run_hook(
54
+ "Edit",
55
+ {"file_path": "docs/guide.md", "old_string": "a", "new_string": "b"},
56
+ )
57
+ assert result.returncode == 0
58
+ output = json.loads(result.stdout)
59
+ assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
60
+
61
+
62
+ def test_blocks_uppercase_md_extension():
63
+ result = _run_hook(
64
+ "Write",
65
+ {"file_path": "DOCS/GUIDE.MD", "content": "# Hello"},
66
+ )
67
+ assert result.returncode == 0
68
+ output = json.loads(result.stdout)
69
+ assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
70
+
71
+
72
+ def test_passes_html_file():
73
+ result = _run_hook(
74
+ "Write",
75
+ {"file_path": "docs/guide.html", "content": "<h1>Hello</h1>"},
76
+ )
77
+ assert result.returncode == 0
78
+ assert result.stdout == ""
79
+
80
+
81
+ def test_passes_non_markdown_extension():
82
+ result = _run_hook(
83
+ "Write",
84
+ {"file_path": "src/main.py", "content": "x = 1"},
85
+ )
86
+ assert result.returncode == 0
87
+ assert result.stdout == ""
88
+
89
+
90
+ def test_passes_claude_dir():
91
+ result = _run_hook(
92
+ "Write",
93
+ {"file_path": ".claude/rules/foo.md", "content": "# Rule"},
94
+ )
95
+ assert result.returncode == 0
96
+ assert result.stdout == ""
97
+
98
+
99
+ def test_passes_nested_claude_dir():
100
+ result = _run_hook(
101
+ "Write",
102
+ {"file_path": "notes/.claude/plans/plan.md", "content": "# Plan"},
103
+ )
104
+ assert result.returncode == 0
105
+ assert result.stdout == ""
106
+
107
+
108
+ def test_passes_readme_at_root():
109
+ result = _run_hook(
110
+ "Write",
111
+ {"file_path": "README.md", "content": "# README"},
112
+ )
113
+ assert result.returncode == 0
114
+ assert result.stdout == ""
115
+
116
+
117
+ def test_passes_changelog_at_root():
118
+ result = _run_hook(
119
+ "Write",
120
+ {"file_path": "CHANGELOG.md", "content": "# Changelog"},
121
+ )
122
+ assert result.returncode == 0
123
+ assert result.stdout == ""
124
+
125
+
126
+ def test_blocks_readme_not_at_root():
127
+ result = _run_hook(
128
+ "Write",
129
+ {"file_path": "docs/README.md", "content": "# README"},
130
+ )
131
+ assert result.returncode == 0
132
+ output = json.loads(result.stdout)
133
+ assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
134
+
135
+
136
+ def test_blocks_changelog_not_at_root():
137
+ result = _run_hook(
138
+ "Write",
139
+ {"file_path": "sub/CHANGELOG.md", "content": "# Log"},
140
+ )
141
+ assert result.returncode == 0
142
+ output = json.loads(result.stdout)
143
+ assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
144
+
145
+
146
+ def test_unknown_tool_passes():
147
+ result = _run_hook(
148
+ "Grep",
149
+ {"pattern": "foo", "path": "."},
150
+ )
151
+ assert result.returncode == 0
152
+ assert result.stdout == ""
153
+
154
+
155
+ def test_empty_file_path_passes():
156
+ result = _run_hook(
157
+ "Write",
158
+ {"file_path": "", "content": "# Hello"},
159
+ )
160
+ assert result.returncode == 0
161
+ assert result.stdout == ""
162
+
163
+
164
+ def test_non_dict_stdin_passes():
165
+ payload = json.dumps(["not", "a", "dict"])
166
+ result = subprocess.run(
167
+ [sys.executable, HOOK_SCRIPT_PATH],
168
+ input=payload,
169
+ capture_output=True,
170
+ text=True,
171
+ check=False,
172
+ )
173
+ assert result.returncode == 0
174
+ assert result.stdout == ""
175
+
176
+
177
+ def test_non_string_tool_name_passes():
178
+ payload = json.dumps(
179
+ {"tool_name": 123, "tool_input": {"file_path": "docs/guide.md"}}
180
+ )
181
+ result = subprocess.run(
182
+ [sys.executable, HOOK_SCRIPT_PATH],
183
+ input=payload,
184
+ capture_output=True,
185
+ text=True,
186
+ check=False,
187
+ )
188
+ assert result.returncode == 0
189
+ assert result.stdout == ""
190
+
191
+
192
+ def test_non_dict_tool_input_passes():
193
+ payload = json.dumps({"tool_name": "Write", "tool_input": "not_a_dict"})
194
+ result = subprocess.run(
195
+ [sys.executable, HOOK_SCRIPT_PATH],
196
+ input=payload,
197
+ capture_output=True,
198
+ text=True,
199
+ check=False,
200
+ )
201
+ assert result.returncode == 0
202
+ assert result.stdout == ""
203
+
204
+
205
+ def test_denial_has_system_message():
206
+ result = _run_hook(
207
+ "Write",
208
+ {"file_path": "docs/guide.md", "content": "# Hello"},
209
+ )
210
+ assert result.returncode == 0
211
+ output = json.loads(result.stdout)
212
+ assert output["suppressOutput"] is True
213
+ assert isinstance(output["systemMessage"], str)
214
+ assert len(output["systemMessage"]) > 0
215
+
216
+
217
+ def test_denial_has_additional_context():
218
+ result = _run_hook(
219
+ "Write",
220
+ {"file_path": "docs/guide.md", "content": "# Hello"},
221
+ )
222
+ assert result.returncode == 0
223
+ output = json.loads(result.stdout)
224
+ ctx = output["hookSpecificOutput"].get("additionalContext", "")
225
+ assert "HTML" in ctx
226
+ assert (
227
+ "thariqs.github.io" in output["hookSpecificOutput"]["permissionDecisionReason"]
228
+ )
229
+
230
+
231
+ def test_denial_reason_mentions_html_redirect():
232
+ result = _run_hook(
233
+ "Write",
234
+ {"file_path": "docs/guide.md", "content": "# Hello"},
235
+ )
236
+ assert result.returncode == 0
237
+ output = json.loads(result.stdout)
238
+ reason = output["hookSpecificOutput"]["permissionDecisionReason"]
239
+ assert ".html" in reason.lower()
240
+
241
+
242
+ def test_passes_claude_md_file():
243
+ result = _run_hook(
244
+ "Write",
245
+ {"file_path": ".claude/CLAUDE.md", "content": "# CLAUDE.md"},
246
+ )
247
+ assert result.returncode == 0
248
+ assert result.stdout == ""
249
+
250
+
251
+ def test_blocks_windows_path_with_backslash():
252
+ result = _run_hook(
253
+ "Write",
254
+ {"file_path": "docs\\guide.md", "content": "# Hello"},
255
+ )
256
+ assert result.returncode == 0
257
+ output = json.loads(result.stdout)
258
+ assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
259
+
260
+
261
+ def test_passes_windows_path_claude_exempt():
262
+ result = _run_hook(
263
+ "Write",
264
+ {"file_path": "project\\.claude\\rules\\foo.md", "content": "# Rule"},
265
+ )
266
+ assert result.returncode == 0
267
+ assert result.stdout == ""
268
+
269
+
270
+ def test_passes_claude_dir_case_insensitive():
271
+ result = _run_hook(
272
+ "Write",
273
+ {"file_path": ".Claude/rules/foo.md", "content": "# Rule"},
274
+ )
275
+ assert result.returncode == 0
276
+ assert result.stdout == ""
277
+
278
+
279
+ def test_passes_readme_lowercase_at_root():
280
+ result = _run_hook(
281
+ "Write",
282
+ {"file_path": "readme.md", "content": "# readme"},
283
+ )
284
+ assert result.returncode == 0
285
+ assert result.stdout == ""
286
+
287
+
288
+ def test_json_decode_error_passes():
289
+ result = subprocess.run(
290
+ [sys.executable, HOOK_SCRIPT_PATH],
291
+ input="not json",
292
+ capture_output=True,
293
+ text=True,
294
+ check=False,
295
+ )
296
+ assert result.returncode == 0
297
+ assert result.stdout == ""
298
+
299
+
300
+ def test_blocks_claude_path_traversal_bypass():
301
+ result = _run_hook(
302
+ "Write",
303
+ {"file_path": ".claude/../docs/guide.md", "content": "# Bypass"},
304
+ )
305
+ assert result.returncode == 0
306
+ output = json.loads(result.stdout)
307
+ assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
308
+
309
+
310
+ def test_blocks_md_with_curly_braces_in_path():
311
+ result = _run_hook(
312
+ "Write",
313
+ {"file_path": "docs/{template}.md", "content": "# Template"},
314
+ )
315
+ assert result.returncode == 0
316
+ output = json.loads(result.stdout)
317
+ assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
@@ -25,8 +25,21 @@ extract_body_from_command = hook_module.extract_body_from_command
25
25
  validate_pr_body = hook_module.validate_pr_body
26
26
 
27
27
  VALID_BODY = (
28
- "## Description\n\nThis PR fixes a real bug.\n\n"
29
- "## Why\n\nBecause it was broken in production.\n\n"
28
+ "Allow commas in branch names so PRs whose head branch was generated from "
29
+ "a title or external identifier no longer fail validation before any git "
30
+ "operation.\n\n"
31
+ "Fixes #1300.\n\n"
32
+ "## Changes\n\n"
33
+ "- `src/github/operations/branch.ts`: add `,` to the whitelist regex\n"
34
+ "- `test/branch.test.ts`: 3 new cases covering comma-bearing branch names\n\n"
35
+ "## Test plan\n\n"
36
+ "- `bun test test/branch.test.ts`\n"
37
+ "- `bun run typecheck`\n"
38
+ )
39
+
40
+ LEGACY_DESCRIPTION_WHY_HOW_BODY = (
41
+ "## Description\n\nThis PR fixes a real bug in the authentication module.\n\n"
42
+ "## Why\n\nBecause it was broken in production and customers reported failures.\n\n"
30
43
  "## How\n\nRefactored the auth module to handle edge cases correctly.\n"
31
44
  )
32
45
 
@@ -109,15 +122,63 @@ def test_extract_short_flag_shell_var_returns_empty() -> None:
109
122
  assert extract_body_from_command(command) == ""
110
123
 
111
124
 
112
- def test_validate_passes_complete_body() -> None:
125
+ def test_validate_passes_anthropic_standard_body() -> None:
113
126
  assert validate_pr_body(VALID_BODY) == []
114
127
 
115
128
 
116
- def test_validate_blocks_missing_sections() -> None:
117
- violations = validate_pr_body("Some body text without required sections.\n" * 5)
118
- assert any(
119
- "Missing required section" in each_violation for each_violation in violations
129
+ def test_validate_passes_legacy_description_why_how_body() -> None:
130
+ """Existing Description/Why/How bodies must still pass -- the relaxed rule only widens what's accepted."""
131
+ assert validate_pr_body(LEGACY_DESCRIPTION_WHY_HOW_BODY) == []
132
+
133
+
134
+ def test_validate_passes_sectionless_prose_body() -> None:
135
+ """Anthropic's trivial-PR shape is one sentence with no headers."""
136
+ body = (
137
+ "Pin third-party GitHub Actions references to immutable commit SHAs "
138
+ "so a tag move cannot redirect CI to attacker-controlled code."
139
+ )
140
+ assert validate_pr_body(body) == []
141
+
142
+
143
+ def test_validate_blocks_skeleton_body_with_only_headers_and_bullets() -> None:
144
+ """Sections + bullets without any prose Why is rejected -- the substantive-prose check catches this."""
145
+ body = (
146
+ "## Summary\n\n"
147
+ "## Changes\n\n"
148
+ "- `a`\n"
149
+ "- `b`\n"
150
+ "- `c`\n"
151
+ )
152
+ violations = validate_pr_body(body)
153
+ assert any("substantive prose" in each_violation.lower() for each_violation in violations)
154
+
155
+
156
+ def test_validate_blocks_blockquoted_headings_with_no_real_prose() -> None:
157
+ """Regression: blockquote markers must strip BEFORE heading stripping.
158
+
159
+ A line like `> ## Summary` starts with `>`, so `^#+[ \\t].*$` cannot match it
160
+ in heading position. If blockquote markers are stripped after, the bare
161
+ `## Summary` text survives into the prose stream and inflates the count.
162
+ Correct order strips `> ` first, then the line becomes a real heading and
163
+ drops out, leaving an effectively empty body below the 40-character minimum.
164
+ """
165
+ body = "> ## Summary\n> ## Why\n> ## How"
166
+ violations = validate_pr_body(body)
167
+ assert any("substantive prose" in each_violation.lower() for each_violation in violations)
168
+
169
+
170
+ def test_validate_passes_prose_after_bare_hashes_with_no_space() -> None:
171
+ """Bug regression: `##\\n` followed by prose must not have its prose eaten by the heading regex.
172
+
173
+ The previous pattern `^#+\\s.*$` matched `\\s` against the newline, then `.*$` greedily
174
+ consumed the next line. The fix restricts the whitespace class to `[ \\t]` so only true
175
+ headings (`## text`) match, leaving prose-after-bare-hashes intact for substantive-prose counting.
176
+ """
177
+ body = (
178
+ "##\nThis is real prose that should not be eaten by the heading regex, "
179
+ "it should pass the 40-character minimum."
120
180
  )
181
+ assert validate_pr_body(body) == []
121
182
 
122
183
 
123
184
  def test_validate_blocks_vague_language() -> None:
@@ -128,7 +189,7 @@ def test_validate_blocks_vague_language() -> None:
128
189
 
129
190
  def test_validate_blocks_short_body() -> None:
130
191
  violations = validate_pr_body("Too short.")
131
- assert any("too short" in each_violation.lower() for each_violation in violations)
192
+ assert any("substantive prose" in each_violation.lower() for each_violation in violations)
132
193
 
133
194
 
134
195
  def test_body_file_content_validated(tmp_path: pathlib.Path) -> None:
@@ -0,0 +1,7 @@
1
+ """Configuration constants for the Any/cast escape-hatch check."""
2
+
3
+ ALL_ANY_ALLOWED_PATTERNS: tuple[str, ...] = (
4
+ "__init__.py",
5
+ "protocols.py",
6
+ "types.py",
7
+ )
@@ -9,10 +9,21 @@ ALL_BANNED_IDENTIFIERS: frozenset[str] = frozenset(
9
9
  "value",
10
10
  "item",
11
11
  "temp",
12
+ "tmp",
12
13
  "argv",
13
14
  "args",
14
15
  "kwargs",
15
16
  "argc",
17
+ "rc",
18
+ "cfg",
19
+ "ctx",
20
+ "cnt",
21
+ "btn",
22
+ "idx",
23
+ "tmp",
24
+ "msg",
25
+ "elem",
26
+ "val",
16
27
  }
17
28
  )
18
29
  MAX_BANNED_IDENTIFIER_ISSUES: int = 3
@@ -0,0 +1,38 @@
1
+ """Caps and lookup sets for the new B-series blocking checks in code_rules_enforcer.py.
2
+
3
+ Each constant is consumed by exactly one check function in the enforcer. They
4
+ live here (not at module scope of the enforcer) so the enforcer file stays
5
+ under the file-global-constants use-count rule (CODE_RULES §file-global-constants).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+
11
+ MAX_BANNED_PREFIX_ISSUES: int = 3
12
+ MAX_STUB_IMPLEMENTATION_ISSUES: int = 3
13
+ MAX_TYPED_DICT_PAIR_ISSUES: int = 3
14
+ MAX_TEST_BRANCHING_ISSUES: int = 3
15
+ MAX_BARE_EXCEPT_ISSUES: int = 3
16
+ MAX_BOUNDARY_TYPE_ISSUES: int = 5
17
+ ALL_BANNED_PREFIX_NAMES: tuple[str, ...] = ("handle_", "process_", "manage_", "do_")
18
+ MAX_DOCSTRING_FORMAT_ISSUES: int = 5
19
+ MAX_TYPE_ESCAPE_HATCH_ISSUES: int = 5
20
+ MAX_THIN_WRAPPER_ISSUES: int = 1
21
+ DOCSTRING_TRIVIAL_FUNCTION_BODY_LINE_LIMIT: int = 3
22
+
23
+ ALL_BARE_EXCEPT_BANNED_HANDLER_NAMES: frozenset[str] = frozenset({"Exception", "BaseException"})
24
+ ALL_BOUNDARY_TYPE_EXEMPT_FILENAMES: frozenset[str] = frozenset({"protocols.py", "types.py"})
25
+ ALL_DOCSTRING_IMPLICIT_INSTANCE_PARAMETER_NAMES: frozenset[str] = frozenset({"self", "cls"})
26
+ ALL_DOCSTRING_EXEMPT_DECORATOR_NAMES: frozenset[str] = frozenset(
27
+ {"property", "abstractmethod", "abstractproperty", "abc.abstractmethod", "overload"}
28
+ )
29
+ ALL_TEST_INDICATING_ENVIRONMENT_VARIABLE_NAMES: frozenset[str] = frozenset(
30
+ {
31
+ "TESTING",
32
+ "PYTEST_CURRENT_TEST",
33
+ "TEST_MODE",
34
+ "IS_TEST",
35
+ "IS_TESTING",
36
+ "UNIT_TEST",
37
+ }
38
+ )
@@ -0,0 +1,20 @@
1
+ """Configuration constants for the bot_mention_comment_blocker PreToolUse hook."""
2
+
3
+ TOOL_NAME: str = "mcp__plugin_github_github__add_issue_comment"
4
+
5
+ CURSOR_MENTION_TOKEN: str = "@cursor"
6
+ COPILOT_MENTION_TOKEN: str = "@copilot"
7
+
8
+ CORRECTIVE_MESSAGE_CURSOR: str = (
9
+ "BLOCKED [bot-mention]: Invalid comment. "
10
+ "Post exactly ``bugbot run`` with no other text as your issue comment "
11
+ "to trigger Bugbot."
12
+ )
13
+
14
+ CORRECTIVE_MESSAGE_COPILOT: str = (
15
+ "BLOCKED [bot-mention]: Invalid comment. "
16
+ "To request a Copilot review, use the GitHub REST API:\n"
17
+ " gh api --method POST repos/<owner>/<repo>/pulls/<number>/requested_reviewers \\\n"
18
+ " -f 'reviewers[]=copilot-pull-request-reviewer[bot]'\n"
19
+ "See ~/.claude/skills/pr-converge/reference/convergence-gates.md."
20
+ )
@@ -0,0 +1,53 @@
1
+ """Constants for code_rules_enforcer.py.
2
+
3
+ Extracted from code_rules_enforcer.py to satisfy the constants-location rule.
4
+ """
5
+
6
+ import re
7
+
8
+ ALL_PYTHON_EXTENSIONS = {".py"}
9
+ ALL_JAVASCRIPT_EXTENSIONS = {".js", ".ts", ".tsx", ".jsx"}
10
+ ALL_CODE_EXTENSIONS = ALL_PYTHON_EXTENSIONS | ALL_JAVASCRIPT_EXTENSIONS
11
+
12
+ ALL_TEST_PATH_PATTERNS = {"test_", "_test.", ".test.", ".spec.", "/tests/", "\\tests\\", "/tests.py", "\\tests.py"}
13
+ ALL_HOOK_INFRASTRUCTURE_PATTERNS = {"/.claude/hooks/", "\\.claude\\hooks\\", "\\.claude/hooks/", "/packages/claude-dev-env/hooks/", "\\packages\\claude-dev-env\\hooks\\"}
14
+ ALL_WORKFLOW_REGISTRY_PATTERNS = {"/workflow/", "\\workflow\\", "_tab.py", "/states.py", "\\states.py", "/modules.py", "\\modules.py"}
15
+ ALL_MIGRATION_PATH_PATTERNS = {"/migrations/", "\\migrations\\"}
16
+
17
+ ADVISORY_LINE_THRESHOLD_SOFT = 400
18
+ ADVISORY_LINE_THRESHOLD_HARD = 1000
19
+
20
+ ALL_BOOLEAN_NAME_PREFIXES: tuple[str, ...] = ("is_", "has_", "should_", "can_")
21
+ UPPER_SNAKE_CONSTANT_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]*$")
22
+
23
+
24
+ TYPE_CHECKING_BLOCK_PATTERN = re.compile(r"^(?P<indent>\s*)if\s+(typing\.)?TYPE_CHECKING\s*:\s*$")
25
+ ALL_IMPORT_STATEMENT_PREFIXES: tuple[str, ...] = ("import ", "from ")
26
+ NOT_INSIDE_TYPE_CHECKING_BLOCK = -1
27
+ FILE_GLOBAL_UPPER_SNAKE_PATTERN = re.compile(r"^_?[A-Z][A-Z0-9_]*$")
28
+
29
+ ALL_COLLECTION_TYPE_NAMES: frozenset[str] = frozenset({
30
+ "list", "tuple", "set", "frozenset",
31
+ "Iterable", "Sequence", "Mapping", "MutableMapping", "FrozenSet",
32
+ })
33
+ ALL_SUBSCRIPT_ONLY_COLLECTION_TYPE_NAMES: frozenset[str] = frozenset({"dict"})
34
+ COLLECTION_BY_NAME_PATTERN: re.Pattern[str] = re.compile(r"^[a-z][a-z0-9]*_by_[a-z][a-z0-9_]*$")
35
+ ALL_CLI_FILE_PATH_MARKERS: tuple[str, ...] = ("/scripts/", "\\scripts\\", "_cli.py", "/cli.py", "\\cli.py")
36
+
37
+ LOGGING_FSTRING_PATTERN = re.compile(
38
+ r'\b(?:log_(?:debug|info|warning|error|critical|exception)'
39
+ r'|(?:logger|logging|log)\.(?:debug|info|warning|error|critical|exception))'
40
+ r'\s*\(\s*(?:[rR][fF]|[fF][rR]?)["\']'
41
+ )
42
+ ALL_BUILTIN_DICT_METHOD_NAMES: frozenset[str] = frozenset({
43
+ "get", "items", "keys", "values", "update", "pop",
44
+ "setdefault", "copy", "clear",
45
+ })
46
+ ALL_UNION_TYPING_NAMES: frozenset[str] = frozenset({"Optional", "Union"})
47
+ ALL_SELF_AND_CLS_PARAMETER_NAMES: frozenset[str] = frozenset({"self", "cls"})
48
+ ALL_LOOP_INDEX_LETTER_EXEMPTIONS: frozenset[str] = frozenset({"i", "j", "k", "_"})
49
+ EACH_PREFIX = "each_"
50
+ BARE_EACH_TOKEN = "each"
51
+ INLINE_COLLECTION_MIN_LENGTH = 3
52
+ ALL_CAPS_WITH_UNDERSCORE_PATTERN = re.compile(r"^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$")
53
+ DOTTED_SEGMENT_PATTERN = re.compile(r"^\.[a-z][a-z0-9_]*$")
@@ -0,0 +1,9 @@
1
+ """Convergence branch naming policy constants.
2
+
3
+ Shared by destructive_command_blocker convergence-branch exemptions
4
+ and convergence_gate_blocker pre-flight checks.
5
+ """
6
+
7
+ ALL_CONVERGENCE_BRANCH_PREFIXES: tuple[str, ...] = ("claude/", "worktree-")
8
+ CONVERGENCE_BRANCH_SUFFIX_PATTERN: str = r"pr-.*-converge$"
9
+ CONVERGENCE_FORCE_PUSH_DETECTION_PATTERN: str = r"\bgit\s+push\b\s+(.*(?:--force|-f)\b.*)"
@@ -0,0 +1,18 @@
1
+ """Constants for the doc-gist auto-publish PostToolUse hook.
2
+
3
+ PUBLISH_SENTINEL: HTML comment marker. Claude includes it in HTML it intends to share;
4
+ absent in HTML that is part of code, tests, or fixtures. Hook is a no-op without it.
5
+
6
+ HTML_FILE_EXTENSION: only files ending in this extension are candidates for the hook;
7
+ Markdown, source files, etc. are skipped.
8
+
9
+ ALL_TARGET_TOOL_NAMES: tool names the hook fires after. The hook is a PostToolUse
10
+ listener; only Write and Edit produce a writable file path that the marker check
11
+ can be applied to.
12
+ """
13
+
14
+ PUBLISH_SENTINEL = "<!-- @publish-as-gist -->"
15
+ HTML_FILE_EXTENSION = ".html"
16
+ ALL_TARGET_TOOL_NAMES = ("Write", "Edit")
17
+ HOOK_SUBPROCESS_TIMEOUT_SECONDS = 50
18
+ UPLOAD_SCRIPT_RELATIVE_PATH = "skills/doc-gist/scripts/gist_upload.py"
@@ -0,0 +1,20 @@
1
+ """CSS theme constants and URL scheme denylist for md-to-html companion hook."""
2
+
3
+ CSS_BG_COLOR = "13, 17, 23"
4
+ CSS_FG_COLOR = "201, 209, 217"
5
+ CSS_BORDER_COLOR = "48, 54, 61"
6
+ CSS_ACCENT_COLOR = "88, 166, 255"
7
+ CSS_MUTED_COLOR = "139, 148, 158"
8
+ CSS_SURFACE_COLOR = "22, 27, 34"
9
+ CSS_STRONG_COLOR = "240, 246, 252"
10
+ CSS_LINE_HEIGHT = "1.6"
11
+ CSS_BODY_PADDING = "2rem"
12
+ CSS_MAX_WIDTH = "960px"
13
+ CSS_H1_SIZE = "1.6rem"
14
+ CSS_H2_SIZE = "1.25rem"
15
+ CSS_H3_SIZE = "1.1rem"
16
+ CSS_CODE_SIZE = "0.85rem"
17
+ CSS_TABLE_WIDTH = "100%"
18
+ CSS_TH_WEIGHT = "600"
19
+
20
+ BLOCKED_URL_SCHEMES = frozenset({"javascript", "data"})
@@ -0,0 +1,22 @@
1
+ """Constants for the inline-tuple snake_case-string-magic check in code_rules_enforcer.
2
+
3
+ Mirrors the column-name/key heuristic previously held only in
4
+ ``_shared/pr-loop/scripts/code_rules_gate.py`` so the Write/Edit hook can
5
+ catch the same pattern that the commit-time gate caught.
6
+ """
7
+
8
+ EXPECTED_TUPLE_PAIR_LENGTH: int = 2
9
+
10
+ MAX_INLINE_TUPLE_STRING_MAGIC_ISSUES: int = 3
11
+
12
+ MINIMUM_SNAKE_CASE_LENGTH_AFTER_FIRST_CHAR: int = 2
13
+
14
+ SNAKE_CASE_LITERAL_PATTERN: str = (
15
+ r"^[a-z][a-z0-9_]{" + str(MINIMUM_SNAKE_CASE_LENGTH_AFTER_FIRST_CHAR) + r",}$"
16
+ )
17
+
18
+ ALL_SNAKE_CASE_KEYWORD_EXEMPTIONS: frozenset[str] = frozenset(
19
+ {"true", "false", "none", "null"}
20
+ )
21
+
22
+ INLINE_TUPLE_STRING_MAGIC_MESSAGE_SUFFIX: str = "extract to config"
@@ -0,0 +1,14 @@
1
+ """Configuration constants for the pr_description_enforcer PreToolUse hook."""
2
+
3
+ import re
4
+
5
+ MINIMUM_SUBSTANTIVE_PROSE_CHARS: int = 40
6
+
7
+ FENCED_CODE_BLOCK_PATTERN: re.Pattern[str] = re.compile(r"```.*?```", re.DOTALL)
8
+ INLINE_CODE_PATTERN: re.Pattern[str] = re.compile(r"`[^`]*`")
9
+ HEADING_LINE_PATTERN: re.Pattern[str] = re.compile(r"^#+[ \t].*$", re.MULTILINE)
10
+ BOLD_PAIR_PATTERN: re.Pattern[str] = re.compile(r"\*\*([^*]+?)\*\*")
11
+ BULLET_MARKER_PATTERN: re.Pattern[str] = re.compile(r"^\s*[-*+]\s+", re.MULTILINE)
12
+ BLOCKQUOTE_MARKER_PATTERN: re.Pattern[str] = re.compile(r"^\s*>\s+", re.MULTILINE)
13
+ LINK_TEXT_PATTERN: re.Pattern[str] = re.compile(r"\[([^\]]+)\]\([^)]+\)")
14
+ WHITESPACE_RUN_PATTERN: re.Pattern[str] = re.compile(r"\s+")
@@ -34,6 +34,23 @@ def test_all_banned_identifiers_includes_canonical_offenders() -> None:
34
34
  assert canonical_offenders <= ALL_BANNED_IDENTIFIERS
35
35
 
36
36
 
37
+ def test_all_banned_identifiers_includes_abbreviation_offenders() -> None:
38
+ abbreviation_offenders = {
39
+ "rc",
40
+ "cfg",
41
+ "ctx",
42
+ "cnt",
43
+ "btn",
44
+ "idx",
45
+ "cnt",
46
+ "tmp",
47
+ "msg",
48
+ "elem",
49
+ "val",
50
+ }
51
+ assert abbreviation_offenders <= ALL_BANNED_IDENTIFIERS
52
+
53
+
37
54
  def test_max_banned_identifier_issues_is_positive_cap() -> None:
38
55
  assert MAX_BANNED_IDENTIFIER_ISSUES > 0
39
56