claude-dev-env 1.38.0 → 1.39.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 (271) 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 +189 -0
  7. package/_shared/pr-loop/scripts/post_audit_thread.py +947 -0
  8. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -19
  9. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +923 -0
  10. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +127 -0
  11. package/_shared/pr-loop/state-schema.md +1 -1
  12. package/agents/clean-coder.md +2 -2
  13. package/bin/install.mjs +6 -7
  14. package/bin/install.test.mjs +8 -0
  15. package/commands/doc-gist.md +16 -0
  16. package/commands/plan.md +0 -2
  17. package/commands/review-plan.md +1 -1
  18. package/docs/CODE_RULES.md +122 -2
  19. package/hooks/blocking/bot_mention_comment_blocker.py +75 -0
  20. package/hooks/blocking/code_rules_enforcer.py +1236 -161
  21. package/hooks/blocking/convergence_gate_blocker.py +130 -0
  22. package/hooks/blocking/destructive_command_blocker.py +74 -0
  23. package/hooks/blocking/gh_body_arg_blocker.py +30 -0
  24. package/hooks/blocking/md_to_html_blocker.py +119 -0
  25. package/hooks/blocking/test_bot_mention_comment_blocker.py +131 -0
  26. package/hooks/blocking/test_code_rules_enforcer.py +21 -0
  27. package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +70 -0
  28. package/hooks/blocking/test_code_rules_enforcer_any_imports_and_cast.py +92 -0
  29. package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +143 -0
  30. package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +152 -0
  31. package/hooks/blocking/test_code_rules_enforcer_bare_except.py +120 -0
  32. package/hooks/blocking/test_code_rules_enforcer_boundary_types.py +175 -0
  33. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  34. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +50 -0
  35. package/hooks/blocking/test_code_rules_enforcer_docstring_format.py +255 -0
  36. package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +130 -0
  37. package/hooks/blocking/test_code_rules_enforcer_stub_implementations.py +141 -0
  38. package/hooks/blocking/test_code_rules_enforcer_test_branching.py +143 -0
  39. package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +169 -0
  40. package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +99 -0
  41. package/hooks/blocking/test_code_rules_enforcer_typed_dict_pairs.py +141 -0
  42. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +158 -0
  43. package/hooks/blocking/test_convergence_gate_blocker.py +63 -0
  44. package/hooks/blocking/test_destructive_command_blocker.py +146 -0
  45. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +102 -0
  46. package/hooks/blocking/test_gh_body_arg_blocker.py +45 -0
  47. package/hooks/blocking/test_md_to_html_blocker.py +317 -0
  48. package/hooks/config/any_type_config.py +7 -0
  49. package/hooks/config/banned_identifiers_constants.py +11 -0
  50. package/hooks/config/blocking_check_limits.py +38 -0
  51. package/hooks/config/bot_mention_comment_blocker_constants.py +20 -0
  52. package/hooks/config/code_rules_enforcer_constants.py +53 -0
  53. package/hooks/config/convergence_branch_constants.py +9 -0
  54. package/hooks/config/doc_gist_auto_publish_constants.py +18 -0
  55. package/hooks/config/html_companion_constants.py +20 -0
  56. package/hooks/config/inline_tuple_string_magic_constants.py +22 -0
  57. package/hooks/config/test_banned_identifiers_constants.py +17 -0
  58. package/hooks/hooks.json +28 -20
  59. package/hooks/pyproject.toml +69 -0
  60. package/hooks/validators/mypy_integration.py +47 -1
  61. package/hooks/validators/run_all_validators.py +3 -3
  62. package/hooks/validators/test_mypy_integration.py +50 -1
  63. package/hooks/workflow/doc_gist_auto_publish.py +144 -0
  64. package/hooks/workflow/md_to_html_companion.py +365 -0
  65. package/hooks/workflow/test_doc_gist_auto_publish.py +117 -0
  66. package/hooks/workflow/test_md_to_html_companion.py +452 -0
  67. package/package.json +1 -1
  68. package/rules/gh-body-file.md +2 -0
  69. package/scripts/Install-SweepEmptyDirs.ps1 +111 -0
  70. package/scripts/check.ps1 +106 -0
  71. package/scripts/config/timing.py +11 -0
  72. package/scripts/sweep_empty_dirs.py +138 -0
  73. package/scripts/sync_to_cursor/rules.py +1 -1
  74. package/scripts/test_sweep_empty_dirs.py +183 -0
  75. package/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +323 -0
  76. package/skills/_shared/pr-loop/scripts/_cli_utils.py +22 -0
  77. package/skills/_shared/pr-loop/scripts/_path_resolver.py +165 -0
  78. package/skills/_shared/pr-loop/scripts/_xml_utils.py +20 -0
  79. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +182 -0
  80. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +185 -0
  81. package/skills/_shared/pr-loop/scripts/config/__init__.py +0 -0
  82. package/skills/_shared/pr-loop/scripts/config/path_resolver_constants.py +78 -0
  83. package/skills/_shared/pr-loop/scripts/init_loop_state.py +135 -0
  84. package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +175 -0
  85. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +182 -0
  86. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +206 -0
  87. package/skills/bugteam/CONSTRAINTS.md +21 -22
  88. package/skills/bugteam/EXAMPLES.md +3 -3
  89. package/skills/bugteam/PROMPTS.md +227 -67
  90. package/skills/bugteam/SKILL.md +114 -455
  91. package/skills/bugteam/reference/README.md +1 -1
  92. package/skills/bugteam/reference/audit-and-teammates.md +112 -39
  93. package/skills/bugteam/reference/audit-contract.md +4 -22
  94. package/skills/bugteam/reference/copilot-gap-analysis.md +8 -5
  95. package/skills/bugteam/reference/design-rationale.md +2 -2
  96. package/skills/bugteam/reference/github-pr-reviews.md +50 -57
  97. package/skills/bugteam/reference/obstacles/audit-assign-ids.md +13 -0
  98. package/skills/bugteam/reference/obstacles/audit-capture-excerpts.md +13 -0
  99. package/skills/bugteam/reference/obstacles/audit-walk-categories.md +13 -0
  100. package/skills/bugteam/reference/obstacles/audit-write-xml.md +13 -0
  101. package/skills/bugteam/reference/obstacles/fix-append-summary.md +13 -0
  102. package/skills/bugteam/reference/obstacles/fix-apply-fixes.md +13 -0
  103. package/skills/bugteam/reference/obstacles/fix-git-add-commit.md +13 -0
  104. package/skills/bugteam/reference/obstacles/fix-git-push.md +13 -0
  105. package/skills/bugteam/reference/obstacles/fix-post-reply.md +13 -0
  106. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +13 -0
  107. package/skills/bugteam/reference/obstacles/fix-py-compile.md +13 -0
  108. package/skills/bugteam/reference/obstacles/fix-read-files.md +13 -0
  109. package/skills/bugteam/reference/obstacles/fix-resolve-thread.md +13 -0
  110. package/skills/bugteam/reference/obstacles/fix-test-suite.md +13 -0
  111. package/skills/bugteam/reference/obstacles/fix-violation-count.md +13 -0
  112. package/skills/bugteam/reference/obstacles/fix-write-xml.md +13 -0
  113. package/skills/bugteam/reference/team-setup.md +106 -9
  114. package/skills/bugteam/reference/teardown-publish-permissions.md +39 -8
  115. package/skills/bugteam/scripts/README.md +60 -0
  116. package/skills/bugteam/scripts/_claude_permissions_common.py +358 -0
  117. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +976 -0
  118. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +375 -0
  119. package/skills/bugteam/scripts/bugteam_preflight.py +294 -0
  120. package/skills/bugteam/scripts/config/bugteam_code_rules_gate_constants.py +25 -0
  121. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +26 -0
  122. package/skills/bugteam/scripts/config/bugteam_preflight_constants.py +35 -0
  123. package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +20 -0
  124. package/skills/bugteam/scripts/config/probe_code_rules_enforcer_check_constants.py +12 -0
  125. package/skills/bugteam/scripts/config/windows_safe_rmtree_constants.py +7 -0
  126. package/skills/bugteam/scripts/grant_project_claude_permissions.py +175 -0
  127. package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +107 -0
  128. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +220 -0
  129. package/skills/bugteam/scripts/test__claude_permissions_common.py +112 -0
  130. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +400 -0
  131. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +384 -0
  132. package/skills/bugteam/scripts/test_bugteam_preflight.py +268 -0
  133. package/skills/bugteam/scripts/test_claude_permissions_common.py +195 -0
  134. package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +55 -0
  135. package/skills/bugteam/scripts/test_probe_code_rules_enforcer_check.py +76 -0
  136. package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +55 -0
  137. package/skills/bugteam/scripts/test_windows_safe_rmtree.py +108 -0
  138. package/skills/bugteam/scripts/windows_safe_rmtree.py +100 -0
  139. package/skills/bugteam/test_skill_additions.py +1 -11
  140. package/skills/code/SKILL.md +176 -0
  141. package/skills/doc-gist/SKILL.md +99 -0
  142. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +453 -0
  143. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +515 -0
  144. package/skills/doc-gist/references/examples/03-code-review-pr.html +638 -0
  145. package/skills/doc-gist/references/examples/04-code-understanding.html +491 -0
  146. package/skills/doc-gist/references/examples/05-design-system.html +629 -0
  147. package/skills/doc-gist/references/examples/06-component-variants.html +605 -0
  148. package/skills/doc-gist/references/examples/07-prototype-animation.html +455 -0
  149. package/skills/doc-gist/references/examples/08-prototype-interaction.html +396 -0
  150. package/skills/doc-gist/references/examples/09-slide-deck.html +592 -0
  151. package/skills/doc-gist/references/examples/10-svg-illustrations.html +492 -0
  152. package/skills/doc-gist/references/examples/11-status-report.html +528 -0
  153. package/skills/doc-gist/references/examples/12-incident-report.html +596 -0
  154. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +395 -0
  155. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +381 -0
  156. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +368 -0
  157. package/skills/doc-gist/references/examples/16-implementation-plan.html +702 -0
  158. package/skills/doc-gist/references/examples/17-pr-writeup.html +595 -0
  159. package/skills/doc-gist/references/examples/18-editor-triage-board.html +573 -0
  160. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +663 -0
  161. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +722 -0
  162. package/skills/doc-gist/references/examples/README.md +5 -0
  163. package/skills/doc-gist/scripts/config/__init__.py +0 -0
  164. package/skills/doc-gist/scripts/config/gist_upload_constants.py +16 -0
  165. package/skills/doc-gist/scripts/gist_upload.py +177 -0
  166. package/skills/doc-gist/scripts/test_gist_upload.py +51 -0
  167. package/skills/findbugs/SKILL.md +68 -2
  168. package/skills/monitor-open-prs/SKILL.md +13 -32
  169. package/skills/monitor-open-prs/test_skill_contract.py +0 -11
  170. package/skills/pr-consistency-audit/SKILL.md +112 -0
  171. package/skills/pr-consistency-audit/reference/detection-rules.md +96 -0
  172. package/skills/pr-consistency-audit/reference/illustrations.md +78 -0
  173. package/skills/pr-converge/SKILL.md +227 -23
  174. package/skills/pr-converge/config/__init__.py +0 -0
  175. package/skills/pr-converge/config/constants.py +62 -0
  176. package/skills/pr-converge/reference/convergence-gates.md +138 -44
  177. package/skills/pr-converge/reference/examples.md +43 -11
  178. package/skills/pr-converge/reference/fix-protocol.md +6 -5
  179. package/skills/pr-converge/reference/ground-rules.md +5 -3
  180. package/skills/pr-converge/reference/multi-pr-orchestration.md +44 -19
  181. package/skills/pr-converge/reference/obstacles/fix-post-replies.md +13 -0
  182. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +13 -0
  183. package/skills/pr-converge/reference/obstacles/fix-push.md +13 -0
  184. package/skills/pr-converge/reference/obstacles/fix-read-filelines.md +13 -0
  185. package/skills/pr-converge/reference/obstacles/fix-reset-state.md +13 -0
  186. package/skills/pr-converge/reference/obstacles/fix-resolve-threads.md +13 -0
  187. package/skills/pr-converge/reference/obstacles/fix-spawn-clean-coder.md +13 -0
  188. package/skills/pr-converge/reference/obstacles/fix-stage-commit.md +13 -0
  189. package/skills/pr-converge/reference/obstacles/fix-trigger-bugbot.md +13 -0
  190. package/skills/pr-converge/reference/obstacles/fix-write-test.md +13 -0
  191. package/skills/pr-converge/reference/per-tick.md +90 -31
  192. package/skills/pr-converge/reference/state-schema.md +22 -1
  193. package/skills/pr-converge/reference/stop-conditions.md +9 -7
  194. package/skills/pr-converge/scripts/README.md +34 -46
  195. package/skills/pr-converge/scripts/check_bugbot_ci.py +174 -0
  196. package/skills/pr-converge/scripts/check_convergence.py +497 -0
  197. package/skills/pr-converge/scripts/check_pending_reviews.py +154 -0
  198. package/skills/pr-converge/scripts/config/pr_converge_constants.py +118 -0
  199. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +134 -0
  200. package/skills/pr-converge/scripts/post_fix_reply.py +168 -0
  201. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -12
  202. package/skills/qbug/SKILL.md +132 -27
  203. package/skills/session-log/SKILL.md +216 -114
  204. package/skills/session-tidy/SKILL.md +1 -1
  205. package/skills/skill-builder/SKILL.md +138 -56
  206. package/skills/skill-builder/references/delegation-map.md +72 -113
  207. package/skills/skill-builder/references/progressive-disclosure.md +122 -0
  208. package/skills/skill-builder/references/self-audit-checklist.md +92 -0
  209. package/skills/skill-builder/references/skill-types.md +228 -0
  210. package/skills/skill-builder/references/thariq-x-post-skills.json +33 -0
  211. package/skills/skill-builder/templates/gap-analysis.md +15 -8
  212. package/skills/skill-builder/workflows/improve-skill.md +86 -57
  213. package/skills/skill-builder/workflows/new-skill.md +80 -168
  214. package/skills/skill-builder/workflows/polish-skill.md +78 -54
  215. package/skills/structure-prompt/SKILL.md +50 -0
  216. package/skills/structure-prompt/reference/adversarial-tuning.md +62 -0
  217. package/skills/structure-prompt/reference/block-classification.md +27 -0
  218. package/skills/structure-prompt/reference/canonical-case.md +48 -0
  219. package/skills/structure-prompt/reference/citation-depth.md +70 -0
  220. package/skills/structure-prompt/reference/cleanup.md +33 -0
  221. package/skills/structure-prompt/reference/constraints.md +33 -0
  222. package/skills/structure-prompt/reference/directives.md +37 -0
  223. package/skills/structure-prompt/reference/examples.md +72 -0
  224. package/skills/structure-prompt/reference/instantiation.md +51 -0
  225. package/skills/structure-prompt/reference/output-contract.md +72 -0
  226. package/skills/structure-prompt/reference/per-category.md +23 -0
  227. package/skills/structure-prompt/reference/persona.md +38 -0
  228. package/skills/structure-prompt/reference/research.md +33 -0
  229. package/skills/structure-prompt/reference/structure.md +28 -0
  230. package/agents/code-standards-agent.md +0 -93
  231. package/agents/groq-coder.md +0 -113
  232. package/agents/plan-executor.md +0 -226
  233. package/agents/project-docs-analyzer.md +0 -53
  234. package/agents/project-structure-organizer-agent.md +0 -72
  235. package/agents/skill-to-agent-converter.md +0 -370
  236. package/agents/skill-writer-agent.md +0 -470
  237. package/agents/user-docs-writer.md +0 -67
  238. package/agents/workflow-visual-documenter.md +0 -82
  239. package/commands/readability-review.md +0 -20
  240. package/hooks/mypy.ini +0 -2
  241. package/hooks/notification/attention_needed_notify.py +0 -71
  242. package/hooks/notification/claude_notification_handler.py +0 -67
  243. package/hooks/notification/notification_utils.py +0 -267
  244. package/hooks/notification/subagent_complete_notify.py +0 -381
  245. package/hooks/notification/test_attention_needed_notify.py +0 -47
  246. package/hooks/notification/test_claude_notification_handler.py +0 -54
  247. package/hooks/notification/test_notification_utils.py +0 -91
  248. package/hooks/notification/test_subagent_complete_notify.py +0 -79
  249. package/scripts/config/groq_bugteam_config.py +0 -230
  250. package/scripts/config/test_groq_bugteam_config.py +0 -83
  251. package/scripts/config/test_spec_implementer_prompt.py +0 -32
  252. package/scripts/groq_bugteam.README.md +0 -131
  253. package/scripts/groq_bugteam.py +0 -647
  254. package/scripts/groq_bugteam_dotenv.py +0 -40
  255. package/scripts/groq_bugteam_spec.py +0 -226
  256. package/scripts/test_groq_bugteam.py +0 -529
  257. package/scripts/test_groq_bugteam_apply_fix_from_spec.py +0 -426
  258. package/scripts/test_groq_bugteam_dotenv.py +0 -66
  259. package/scripts/test_groq_bugteam_spec.py +0 -338
  260. package/skills/bugteam/SKILL_EVALS.md +0 -309
  261. package/skills/dream/SKILL.md +0 -118
  262. package/skills/ingest/SKILL.md +0 -40
  263. package/skills/npm-creator/SKILL.md +0 -187
  264. package/skills/readability-review/SKILL.md +0 -127
  265. package/skills/resume-review/SKILL.md +0 -261
  266. package/skills/rule-audit/SKILL.md +0 -307
  267. package/skills/rule-creator/SKILL.md +0 -150
  268. package/skills/searching-obsidian-vault/SKILL.md +0 -131
  269. package/skills/skill-writer/REFERENCE.md +0 -284
  270. package/skills/skill-writer/SKILL.md +0 -222
  271. package/skills/tdd-team/SKILL.md +0 -128
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env python3
2
+ """PreToolUse hook: block gh pr ready until convergence pre-conditions pass.
3
+
4
+ Runs check_convergence.py against the PR and denies the gh pr ready
5
+ call if any condition fails. The agent sees exactly which conditions
6
+ failed and can address them before retrying.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import re
13
+ import subprocess
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ def _resolve_pr_number(command: str, cwd: str | None) -> int | None:
18
+ direct_match = re.search(r"\bgh\s+pr\s+ready\s+(\d+)", command)
19
+ if direct_match:
20
+ return int(direct_match.group(1))
21
+ try:
22
+ completed_process = subprocess.run(
23
+ ["gh", "pr", "view", "--json", "number", "--jq", ".number"],
24
+ capture_output=True,
25
+ text=True,
26
+ cwd=cwd or None,
27
+ check=False,
28
+ )
29
+ except OSError:
30
+ return None
31
+ if completed_process.returncode != 0:
32
+ return None
33
+ try:
34
+ return int(completed_process.stdout.strip())
35
+ except (ValueError, TypeError):
36
+ return None
37
+
38
+
39
+ def _resolve_owner_repo(cwd: str | None) -> tuple[str, str] | None:
40
+ try:
41
+ completed_process = subprocess.run(
42
+ ["gh", "repo", "view", "--json", "owner,name", "--jq", ".owner.login,.name"],
43
+ capture_output=True,
44
+ text=True,
45
+ cwd=cwd or None,
46
+ check=False,
47
+ )
48
+ except OSError:
49
+ return None
50
+ if completed_process.returncode != 0:
51
+ return None
52
+ parts = completed_process.stdout.strip().splitlines()
53
+ if len(parts) <= 1:
54
+ match = re.match(
55
+ r"https://github\.com/([^/]+)/([^/]+?)(?:\.git)?$",
56
+ completed_process.stdout.strip(),
57
+ )
58
+ if match:
59
+ return match.group(1), match.group(2)
60
+ return None
61
+ return parts[0], parts[1]
62
+
63
+
64
+ def main() -> None:
65
+ check_convergence_script = str(
66
+ Path.home() / ".claude/skills/pr-converge/scripts/check_convergence.py"
67
+ )
68
+
69
+ if not Path(check_convergence_script).is_file():
70
+ sys.exit(0)
71
+
72
+ try:
73
+ hook_input = json.load(sys.stdin)
74
+ except json.JSONDecodeError:
75
+ sys.exit(0)
76
+
77
+ tool_name = hook_input.get("tool_name", "")
78
+ if tool_name != "Bash":
79
+ sys.exit(0)
80
+
81
+ command = hook_input.get("tool_input", {}).get("command", "")
82
+ gh_pr_ready_pattern = re.compile(r"\bgh\s+pr\s+ready\b(?![^&|;\n]*--undo)")
83
+ if not gh_pr_ready_pattern.search(command):
84
+ sys.exit(0)
85
+
86
+ cwd = hook_input.get("tool_input", {}).get("cwd")
87
+ pr_number = _resolve_pr_number(command, cwd)
88
+ if pr_number is None:
89
+ sys.exit(0)
90
+
91
+ owner_repo = _resolve_owner_repo(cwd)
92
+ if owner_repo is None:
93
+ sys.exit(0)
94
+ owner, repo = owner_repo
95
+
96
+ completed_process = subprocess.run(
97
+ [
98
+ sys.executable,
99
+ check_convergence_script,
100
+ "--owner",
101
+ owner,
102
+ "--repo",
103
+ repo,
104
+ "--pr-number",
105
+ str(pr_number),
106
+ ],
107
+ capture_output=True,
108
+ text=True,
109
+ check=False,
110
+ )
111
+
112
+ if completed_process.returncode in (0, 2):
113
+ sys.exit(0)
114
+
115
+ deny_payload = {
116
+ "hookSpecificOutput": {
117
+ "hookEventName": "PreToolUse",
118
+ "permissionDecision": "deny",
119
+ "permissionDecisionReason": (
120
+ "Convergence check failed — PR is not ready to mark ready:\n\n" + completed_process.stdout
121
+ ),
122
+ }
123
+ }
124
+ print(json.dumps(deny_payload))
125
+ sys.stdout.flush()
126
+ sys.exit(0)
127
+
128
+
129
+ if __name__ == "__main__":
130
+ main()
@@ -9,6 +9,16 @@ import sys
9
9
  import tempfile
10
10
  from pathlib import Path
11
11
 
12
+ _hooks_dir = str(Path(__file__).resolve().parent.parent)
13
+ if _hooks_dir not in sys.path:
14
+ sys.path.insert(0, _hooks_dir)
15
+
16
+ from config.convergence_branch_constants import ( # noqa: E402
17
+ ALL_CONVERGENCE_BRANCH_PREFIXES,
18
+ CONVERGENCE_BRANCH_SUFFIX_PATTERN,
19
+ CONVERGENCE_FORCE_PUSH_DETECTION_PATTERN,
20
+ )
21
+
12
22
  CLAUDE_DIRECTORY_PATH = os.path.normpath(os.path.expanduser("~/.claude"))
13
23
  GH_REDIRECT_ACTIVE_ENV_VAR = "CLAUDE_GH_REDIRECT_ACTIVE"
14
24
  GH_REDIRECT_ACTIVE_TRUTHY_VALUES = frozenset({"1", "true", "yes", "on"})
@@ -86,6 +96,9 @@ DESTRUCTIVE_BASH_PATTERNS = [
86
96
  (re.compile(r'\bDROP\s+TABLE\b', re.IGNORECASE), "DROP TABLE (destroys database table)"),
87
97
  (re.compile(r'\bDROP\s+DATABASE\b', re.IGNORECASE), "DROP DATABASE (destroys entire database)"),
88
98
  (re.compile(r'\bTRUNCATE\s+TABLE\b', re.IGNORECASE), "TRUNCATE TABLE (removes all table rows)"),
99
+ (re.compile(r'\bgit\s+(?:[^\s]+\s+)*--no-verify\b', re.IGNORECASE), "git --no-verify (skips pre-commit / pre-push hooks; NON-NEGOTIABLE per git-workflow.md)"),
100
+ (re.compile(r'\bgit\s+(?:[^\s]+\s+)*--no-gpg-sign\b', re.IGNORECASE), "git --no-gpg-sign (bypasses commit signing; NON-NEGOTIABLE per git-workflow.md)"),
101
+ (re.compile(r"\bgit\s+-c\s+['\"]?commit\.gpgsign=['\"]?false['\"]?(?!\w)", re.IGNORECASE), "git -c commit.gpgsign=false (bypasses commit signing; NON-NEGOTIABLE per git-workflow.md)"),
89
102
  ]
90
103
 
91
104
  def find_destructive_pattern(command: str) -> str | None:
@@ -483,6 +496,52 @@ def _git_reset_hard_allowed_for_command(command: str, current_working_directory:
483
496
  return False
484
497
 
485
498
 
499
+ def _is_convergence_branch(branch: str) -> bool:
500
+ all_convergence_branch_prefixes = ALL_CONVERGENCE_BRANCH_PREFIXES
501
+ for each_prefix in all_convergence_branch_prefixes:
502
+ if branch.startswith(each_prefix):
503
+ return True
504
+ return bool(re.match(CONVERGENCE_BRANCH_SUFFIX_PATTERN, branch))
505
+
506
+
507
+ def _all_refspecs_are_convergence_branches(post_remote_text: str) -> bool:
508
+ if not post_remote_text.strip():
509
+ return False
510
+ is_any_refspec_checked = False
511
+ for each_token in post_remote_text.split():
512
+ if each_token.startswith("-"):
513
+ continue
514
+ is_any_refspec_checked = True
515
+ destination_branch = each_token.split(":")[-1]
516
+ if not _is_convergence_branch(destination_branch):
517
+ return False
518
+ return is_any_refspec_checked
519
+
520
+
521
+ def _force_push_targets_convergence_branch(command: str) -> bool:
522
+ convergence_force_push_detection_pattern = (
523
+ CONVERGENCE_FORCE_PUSH_DETECTION_PATTERN
524
+ )
525
+ is_force_push_found = False
526
+ for each_match in re.finditer(
527
+ convergence_force_push_detection_pattern, command, re.IGNORECASE
528
+ ):
529
+ is_force_push_found = True
530
+ post_push_text = each_match.group(1).strip()
531
+ all_tokens = post_push_text.split()
532
+ remote_index = 1 if all_tokens and all_tokens[0] in ("--force", "-f") else 0
533
+ all_refspec_tokens = [
534
+ token for token in all_tokens[remote_index + 1 :]
535
+ if token not in ("--force", "-f")
536
+ ]
537
+ post_remote_text = " ".join(all_refspec_tokens)
538
+ if not post_remote_text:
539
+ return False
540
+ if not _all_refspecs_are_convergence_branches(post_remote_text):
541
+ return False
542
+ return is_force_push_found
543
+
544
+
486
545
  def main() -> None:
487
546
  try:
488
547
  hook_input = json.load(sys.stdin)
@@ -524,6 +583,21 @@ def main() -> None:
524
583
  if _git_reset_hard_allowed_for_command(command, os.getcwd()):
525
584
  sys.exit(0)
526
585
 
586
+ if (
587
+ matched_description is not None
588
+ and "git push" in matched_description
589
+ and ("force" in matched_description or "-f" in matched_description)
590
+ and _force_push_targets_convergence_branch(command)
591
+ ):
592
+ for each_pattern, each_description in DESTRUCTIVE_BASH_PATTERNS:
593
+ if "git push" in each_description and ("force" in each_description or "-f" in each_description):
594
+ continue
595
+ if each_pattern.search(command):
596
+ matched_description = each_description
597
+ break
598
+ else:
599
+ sys.exit(0)
600
+
527
601
  if matched_description is not None:
528
602
  ask_response = {
529
603
  "hookSpecificOutput": {
@@ -24,6 +24,7 @@ import re
24
24
  import sys
25
25
 
26
26
  from _gh_body_arg_utils import (
27
+ _is_bash_continuation,
27
28
  all_body_flags,
28
29
  all_body_flag_prefixes,
29
30
  get_logical_first_line,
@@ -69,6 +70,32 @@ def _logical_line_has_bare_body_token(logical_line: str) -> bool:
69
70
  return bool(_BARE_BODY_TOKEN_PATTERN.search(logical_line))
70
71
 
71
72
 
73
+ def _has_backtick(command: str) -> bool:
74
+ """Return True if command contains a backtick that is not a bash continuation.
75
+
76
+ Joins all bash `` \\ `` continuation lines so backticks in multi-line body
77
+ values on later non-continuation lines are not missed. Only strips bash
78
+ continuations — this hook runs on the Bash tool, and PowerShell-style
79
+ backtick continuations are not continuation markers in bash.
80
+
81
+ Scans the entire command string for backtick characters, not just --body
82
+ argument content. This is intentionally conservative — any command
83
+ containing backticks should use --body-file regardless of where they
84
+ appear. False positives (backticks in non-body flags) are safe
85
+ over-blocks.
86
+ """
87
+ continuation_separator = " "
88
+ all_joined_lines: list[str] = []
89
+ for each_line in command.splitlines():
90
+ stripped_line = each_line.rstrip()
91
+ if _is_bash_continuation(stripped_line):
92
+ all_joined_lines.append(stripped_line[:-1].rstrip() + continuation_separator)
93
+ continue
94
+ all_joined_lines.append(each_line)
95
+ full_logical_command = "".join(all_joined_lines)
96
+ return "`" in full_logical_command
97
+
98
+
72
99
  def _uses_body_string_arg(command: str) -> bool:
73
100
  """Return True if command calls an affected gh subcommand with --body <string>.
74
101
 
@@ -114,6 +141,9 @@ def main() -> None:
114
141
  if not _uses_body_string_arg(command):
115
142
  sys.exit(0)
116
143
 
144
+ if not _has_backtick(command):
145
+ sys.exit(0)
146
+
117
147
  deny_payload = {
118
148
  "hookSpecificOutput": {
119
149
  "hookEventName": "PreToolUse",
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env python3
2
+ """PreToolUse hook: blocks Write/Edit targeting .md files, redirecting to .html.
3
+
4
+ HTML preserves spatial structure (diffs, timelines, comparisons, diagrams)
5
+ that markdown flattens. See https://thariqs.github.io/html-effectiveness/
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import sys
11
+ from typing import TextIO
12
+
13
+
14
+ _markdown_extension = ".md"
15
+ _html_effectiveness_url = "https://thariqs.github.io/html-effectiveness/"
16
+ _exempt_root_filenames = ("readme.md", "changelog.md")
17
+
18
+
19
+ def _is_exempt_path(file_path: str) -> bool:
20
+ normalized = os.path.normpath(file_path).replace("\\", "/")
21
+ lower_normalized = normalized.lower()
22
+ if "/.claude/" in lower_normalized or lower_normalized.startswith(".claude/"):
23
+ return True
24
+ basename = os.path.basename(normalized)
25
+ if basename.lower() in _exempt_root_filenames:
26
+ directory = os.path.dirname(normalized)
27
+ if directory in ("", "."):
28
+ return True
29
+ return False
30
+
31
+
32
+ def _block_reason(file_path: str) -> str:
33
+ return (
34
+ f"BLOCKED: Write/Edit to .md file '{file_path}' is not permitted. "
35
+ "Use .html files instead for documentation. "
36
+ f"See {_html_effectiveness_url} for why HTML "
37
+ "is more effective than Markdown for structured information."
38
+ )
39
+
40
+
41
+ def _block_context() -> str:
42
+ return (
43
+ "Generate a self-contained .html file instead of .md. "
44
+ "Design freely — HTML can express spatial structure, interactivity, "
45
+ "and visual hierarchy that markdown cannot.\n\n"
46
+ "Reference for HTML effectiveness patterns:\n"
47
+ f"{_html_effectiveness_url}\n"
48
+ "Exceptions (.md still allowed):\n"
49
+ "- Files inside .claude/ directories\n"
50
+ "- README.md and CHANGELOG.md at repo root"
51
+ )
52
+
53
+
54
+ def _block_system_message() -> str:
55
+ return (
56
+ ".md files are blocked in this project — generate a self-contained .html "
57
+ f"file instead. See {_html_effectiveness_url} for "
58
+ "design patterns and examples. Exemptions: .claude/ infrastructure, "
59
+ "README.md, CHANGELOG.md at repo root."
60
+ )
61
+
62
+
63
+ def main() -> None:
64
+ """Read hook input JSON from stdin, deny .md writes or pass through silently.
65
+
66
+ Returns:
67
+ None (exits process).
68
+ """
69
+ try:
70
+ input_data = json.load(sys.stdin)
71
+ except json.JSONDecodeError:
72
+ sys.exit(0)
73
+
74
+ if not isinstance(input_data, dict):
75
+ sys.exit(0)
76
+
77
+ tool_name = input_data.get("tool_name", "")
78
+ if not isinstance(tool_name, str):
79
+ sys.exit(0)
80
+
81
+ tool_input = input_data.get("tool_input", {})
82
+ if not isinstance(tool_input, dict):
83
+ sys.exit(0)
84
+
85
+ if tool_name not in ("Write", "Edit"):
86
+ sys.exit(0)
87
+
88
+ file_path = tool_input.get("file_path", "")
89
+ if not file_path:
90
+ sys.exit(0)
91
+
92
+ if not file_path.lower().endswith(_markdown_extension):
93
+ sys.exit(0)
94
+
95
+ if _is_exempt_path(file_path):
96
+ sys.exit(0)
97
+
98
+ block_payload = {
99
+ "hookSpecificOutput": {
100
+ "hookEventName": "PreToolUse",
101
+ "permissionDecision": "deny",
102
+ "permissionDecisionReason": _block_reason(file_path),
103
+ "additionalContext": _block_context(),
104
+ },
105
+ "systemMessage": _block_system_message(),
106
+ "suppressOutput": True,
107
+ }
108
+
109
+ _emit_hook_result(block_payload, sys.stdout)
110
+ sys.exit(0)
111
+
112
+
113
+ def _emit_hook_result(all_hook_data: dict, output_stream: TextIO) -> None:
114
+ output_stream.write(json.dumps(all_hook_data) + "\n")
115
+ output_stream.flush()
116
+
117
+
118
+ if __name__ == "__main__":
119
+ main()
@@ -0,0 +1,131 @@
1
+ """Unit tests for bot-mention-comment-blocker PreToolUse hook."""
2
+
3
+ import importlib.util
4
+ import io
5
+ import json
6
+ import pathlib
7
+ import sys
8
+
9
+ _HOOK_DIR = pathlib.Path(__file__).parent
10
+ if str(_HOOK_DIR) not in sys.path:
11
+ sys.path.insert(0, str(_HOOK_DIR))
12
+
13
+ hook_spec = importlib.util.spec_from_file_location(
14
+ "bot_mention_comment_blocker",
15
+ _HOOK_DIR / "bot_mention_comment_blocker.py",
16
+ )
17
+ assert hook_spec is not None
18
+ assert hook_spec.loader is not None
19
+ hook_module = importlib.util.module_from_spec(hook_spec)
20
+ hook_spec.loader.exec_module(hook_module)
21
+
22
+ _detect_bot_mention = hook_module._detect_bot_mention
23
+ _body_contains_token = hook_module._body_contains_token
24
+
25
+ from config.bot_mention_comment_blocker_constants import (
26
+ CORRECTIVE_MESSAGE_COPILOT,
27
+ CORRECTIVE_MESSAGE_CURSOR,
28
+ CURSOR_MENTION_TOKEN,
29
+ )
30
+
31
+
32
+ def test_passes_clean_body() -> None:
33
+ assert _detect_bot_mention("bugbot run") is None
34
+
35
+
36
+ def test_passes_empty_body() -> None:
37
+ assert _detect_bot_mention("") is None
38
+
39
+
40
+ def test_passes_unrelated_body() -> None:
41
+ assert _detect_bot_mention("please review this PR") is None
42
+
43
+
44
+ def test_blocks_cursor_mention() -> None:
45
+ reason = _detect_bot_mention("@cursor bugbot run")
46
+ assert reason is not None
47
+ assert "bugbot run" in reason
48
+
49
+
50
+ def test_blocks_cursor_bracket_mention() -> None:
51
+ reason = _detect_bot_mention("@cursor[bot] bugbot run")
52
+ assert reason is not None
53
+ assert "bugbot run" in reason
54
+
55
+
56
+ def test_blocks_copilot_mention() -> None:
57
+ reason = _detect_bot_mention("@copilot review this")
58
+ assert reason is not None
59
+ assert "copilot-pull-request-reviewer" in reason
60
+
61
+
62
+ def test_returns_cursor_message_for_cursor() -> None:
63
+ assert _detect_bot_mention("@cursor run") == CORRECTIVE_MESSAGE_CURSOR
64
+
65
+
66
+ def test_returns_copilot_message_for_copilot() -> None:
67
+ assert _detect_bot_mention("@copilot help") == CORRECTIVE_MESSAGE_COPILOT
68
+
69
+
70
+ def test_copilot_wins_when_both_present() -> None:
71
+ assert _detect_bot_mention("@cursor and @copilot") == CORRECTIVE_MESSAGE_COPILOT
72
+
73
+
74
+ def test_body_contains_token_case_insensitive() -> None:
75
+ assert _body_contains_token("Hello @CURSOR world", "@cursor")
76
+ assert _body_contains_token("Hello @CoPilot world", "@copilot")
77
+
78
+
79
+ def test_body_contains_token_no_at_sign() -> None:
80
+ assert not _body_contains_token("cursor without at-sign", CURSOR_MENTION_TOKEN)
81
+
82
+
83
+ from unittest import mock
84
+
85
+
86
+ def _run_main_with_io(input_text: str) -> str:
87
+ with mock.patch("sys.stdin", io.StringIO(input_text)):
88
+ with mock.patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
89
+ try:
90
+ hook_module.main()
91
+ except SystemExit:
92
+ pass
93
+ return mock_stdout.getvalue()
94
+
95
+
96
+ def test_main_blocks_matching_cursor_comment() -> None:
97
+ hook_input = {
98
+ "tool_name": "mcp__plugin_github_github__add_issue_comment",
99
+ "tool_input": {"body": "@cursor bugbot run"},
100
+ }
101
+ output_text = _run_main_with_io(json.dumps(hook_input))
102
+ output = json.loads(output_text)
103
+ assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
104
+
105
+
106
+ def test_main_passes_wrong_tool_name() -> None:
107
+ hook_input = {
108
+ "tool_name": "some_other_tool",
109
+ "tool_input": {"body": "@cursor bugbot run"},
110
+ }
111
+ assert _run_main_with_io(json.dumps(hook_input)) == ""
112
+
113
+
114
+ def test_main_passes_empty_body() -> None:
115
+ hook_input = {
116
+ "tool_name": "mcp__plugin_github_github__add_issue_comment",
117
+ "tool_input": {"body": ""},
118
+ }
119
+ assert _run_main_with_io(json.dumps(hook_input)) == ""
120
+
121
+
122
+ def test_main_passes_non_matching_body() -> None:
123
+ hook_input = {
124
+ "tool_name": "mcp__plugin_github_github__add_issue_comment",
125
+ "tool_input": {"body": "please review this PR"},
126
+ }
127
+ assert _run_main_with_io(json.dumps(hook_input)) == ""
128
+
129
+
130
+ def test_main_passes_malformed_json() -> None:
131
+ assert _run_main_with_io("not valid json {{{") == ""
@@ -1192,3 +1192,24 @@ def test_sys_path_insert_should_skip_hook_infrastructure_files() -> None:
1192
1192
  assert issues == [], (
1193
1193
  f"Hook infrastructure files are exempt from this rule, got: {issues}"
1194
1194
  )
1195
+
1196
+
1197
+ def test_validate_content_honors_empty_full_file_content_for_thin_wrapper_check() -> None:
1198
+ """An empty `full_file_content` must not be silently replaced with the pre-edit fragment.
1199
+
1200
+ Regression for loop1-8: the `or` short-circuit at the thin-wrapper call
1201
+ site treated `""` identically to `None`, so an Edit collapsing a file to
1202
+ empty was scanned against the pre-edit fragment instead of the empty
1203
+ post-edit content. Mirror the canonical idiom at line 3438.
1204
+ """
1205
+ pre_edit_fragment_with_imports_only = (
1206
+ "from real_module import do_thing\n__all__ = ['do_thing']\n"
1207
+ )
1208
+ issues = code_rules_enforcer.validate_content(
1209
+ pre_edit_fragment_with_imports_only,
1210
+ "/project/src/aliases.py",
1211
+ full_file_content="",
1212
+ )
1213
+ assert not any("thin wrapper" in each.lower() for each in issues), (
1214
+ f"empty post-edit file must not be flagged as a thin wrapper, got: {issues!r}"
1215
+ )
@@ -0,0 +1,70 @@
1
+ """Tests for ANY_ALLOWED_PATTERNS exemption from check_type_escape_hatches.
2
+
3
+ Per Plan 1b: __init__.py, protocols.py, types.py, conftest.py are exempt
4
+ from the Any/cast checks because their primary purpose is type re-export
5
+ or runtime protocol declaration.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib.util
11
+ from pathlib import Path
12
+ from types import ModuleType
13
+
14
+
15
+ def _load_enforcer_module() -> ModuleType:
16
+ module_path = Path(__file__).parent / "code_rules_enforcer.py"
17
+ spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
18
+ assert spec is not None
19
+ assert spec.loader is not None
20
+ module = importlib.util.module_from_spec(spec)
21
+ spec.loader.exec_module(module)
22
+ return module
23
+
24
+
25
+ code_rules_enforcer = _load_enforcer_module()
26
+ check_type_escape_hatches = code_rules_enforcer.check_type_escape_hatches
27
+
28
+ ANY_USING_SOURCE = "from typing import Any\nx: Any = 1\n"
29
+
30
+
31
+ def test_should_exempt_init_py() -> None:
32
+ issues = check_type_escape_hatches(ANY_USING_SOURCE, "/project/src/foo/__init__.py")
33
+ assert issues == [], f"__init__.py must be exempt, got: {issues!r}"
34
+
35
+
36
+ def test_should_exempt_protocols_py() -> None:
37
+ issues = check_type_escape_hatches(ANY_USING_SOURCE, "/project/src/protocols.py")
38
+ assert issues == [], f"protocols.py must be exempt, got: {issues!r}"
39
+
40
+
41
+ def test_should_exempt_types_py() -> None:
42
+ issues = check_type_escape_hatches(ANY_USING_SOURCE, "/project/src/types.py")
43
+ assert issues == [], f"types.py must be exempt, got: {issues!r}"
44
+
45
+
46
+ def test_should_exempt_conftest_py() -> None:
47
+ issues = check_type_escape_hatches(ANY_USING_SOURCE, "/project/src/conftest.py")
48
+ assert issues == [], f"conftest.py must be exempt, got: {issues!r}"
49
+
50
+
51
+ def test_should_still_flag_in_regular_module() -> None:
52
+ issues = check_type_escape_hatches(ANY_USING_SOURCE, "/project/src/models.py")
53
+ assert issues != [], (
54
+ f"Regular .py files must still be flagged for Any usage, got: {issues!r}"
55
+ )
56
+
57
+
58
+ def test_any_type_config_module_exists_and_exposes_constant() -> None:
59
+ config_module_path = Path(__file__).parent.parent / "config" / "any_type_config.py"
60
+ assert config_module_path.is_file(), f"Missing: {config_module_path}"
61
+ spec = importlib.util.spec_from_file_location("any_type_config_under_test", config_module_path)
62
+ assert spec is not None
63
+ assert spec.loader is not None
64
+ loaded_module = importlib.util.module_from_spec(spec)
65
+ spec.loader.exec_module(loaded_module)
66
+ allowed_patterns = loaded_module.ALL_ANY_ALLOWED_PATTERNS
67
+ assert isinstance(allowed_patterns, tuple)
68
+ assert "__init__.py" in allowed_patterns
69
+ assert "protocols.py" in allowed_patterns
70
+ assert "types.py" in allowed_patterns