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
@@ -30,6 +30,7 @@ assert _hook_spec.loader is not None
30
30
  _hook_module = importlib.util.module_from_spec(_hook_spec)
31
31
  _hook_spec.loader.exec_module(_hook_module)
32
32
  check_unused_module_level_imports = _hook_module.check_unused_module_level_imports
33
+ _reconstruct_post_edit_file_content = _hook_module._reconstruct_post_edit_file_content
33
34
 
34
35
 
35
36
  PRODUCTION_FILE_PATH = "packages/app/services/loader.py"
@@ -297,6 +298,163 @@ def test_should_skip_when_noqa_is_bare() -> None:
297
298
  assert issues == [], f"Bare noqa must suppress unused import, got: {issues}"
298
299
 
299
300
 
301
+ def test_should_not_flag_imports_referenced_only_in_full_file_content() -> None:
302
+ fragment = "from config.constants import NEW_NAME\n"
303
+ full_file = (
304
+ "from config.constants import NEW_NAME\n"
305
+ "\n"
306
+ "def existing_function() -> str:\n"
307
+ " return NEW_NAME\n"
308
+ )
309
+ issues = check_unused_module_level_imports(
310
+ fragment, PRODUCTION_FILE_PATH, full_file_content=full_file,
311
+ )
312
+ assert issues == [], (
313
+ f"When the post-edit file references the import, it must not flag, got: {issues}"
314
+ )
315
+
316
+
317
+ def test_should_flag_imports_unused_in_full_file_content() -> None:
318
+ fragment = "from config.constants import TRULY_UNUSED\n"
319
+ full_file = (
320
+ "from config.constants import TRULY_UNUSED\n"
321
+ "\n"
322
+ "def existing_function() -> None:\n"
323
+ " return None\n"
324
+ )
325
+ issues = check_unused_module_level_imports(
326
+ fragment, PRODUCTION_FILE_PATH, full_file_content=full_file,
327
+ )
328
+ assert any("TRULY_UNUSED" in each_issue for each_issue in issues), (
329
+ f"Imports unused in the post-edit file must still flag, got: {issues}"
330
+ )
331
+
332
+
333
+ def test_should_only_flag_imports_in_fragment_not_full_file() -> None:
334
+ fragment = "from config.constants import FRAGMENT_IMPORT\n"
335
+ full_file = (
336
+ "from config.other import PRE_EXISTING_UNUSED\n"
337
+ "from config.constants import FRAGMENT_IMPORT\n"
338
+ "\n"
339
+ "def existing_function() -> str:\n"
340
+ " return FRAGMENT_IMPORT\n"
341
+ )
342
+ issues = check_unused_module_level_imports(
343
+ fragment, PRODUCTION_FILE_PATH, full_file_content=full_file,
344
+ )
345
+ assert issues == [], (
346
+ "Pre-existing imports outside the fragment must not be flagged on Edit; "
347
+ f"got: {issues}"
348
+ )
349
+
350
+
351
+ def test_should_skip_when_full_file_declares_dunder_all() -> None:
352
+ fragment = "from config.constants import NEW_NAME\n"
353
+ full_file = (
354
+ "from config.constants import NEW_NAME\n"
355
+ "\n"
356
+ "__all__ = ['NEW_NAME']\n"
357
+ )
358
+ issues = check_unused_module_level_imports(
359
+ fragment, PRODUCTION_FILE_PATH, full_file_content=full_file,
360
+ )
361
+ assert issues == [], (
362
+ "__all__ in the post-edit file must skip the scan even when the fragment "
363
+ f"itself does not contain __all__, got: {issues}"
364
+ )
365
+
366
+
367
+ def test_should_skip_when_full_file_uses_type_checking_gate() -> None:
368
+ fragment = "from config.constants import NEW_NAME\n"
369
+ full_file = (
370
+ "from typing import TYPE_CHECKING\n"
371
+ "from config.constants import NEW_NAME\n"
372
+ "\n"
373
+ "if TYPE_CHECKING:\n"
374
+ " from somewhere import OtherName\n"
375
+ "\n"
376
+ "def run() -> None:\n"
377
+ " return None\n"
378
+ )
379
+ issues = check_unused_module_level_imports(
380
+ fragment, PRODUCTION_FILE_PATH, full_file_content=full_file,
381
+ )
382
+ assert issues == [], (
383
+ "TYPE_CHECKING gate in the post-edit file must skip the scan, "
384
+ f"got: {issues}"
385
+ )
386
+
387
+
388
+ def test_should_fall_back_to_content_when_full_file_content_is_none() -> None:
389
+ source = (
390
+ "from config.constants import VENV_DIRECTORY_NAME\n"
391
+ "\n"
392
+ "def run() -> None:\n"
393
+ " return None\n"
394
+ )
395
+ issues = check_unused_module_level_imports(source, PRODUCTION_FILE_PATH, full_file_content=None)
396
+ assert any("VENV_DIRECTORY_NAME" in each_issue for each_issue in issues), (
397
+ f"Backward-compat: with full_file_content=None, behavior must match "
398
+ f"the existing single-argument scan, got: {issues}"
399
+ )
400
+
401
+
402
+ def test_should_fall_back_when_full_file_content_has_syntax_error() -> None:
403
+ fragment = "from config.constants import NEW_NAME\n"
404
+ full_file_with_syntax_error = "from config import (\n not python\n"
405
+ issues = check_unused_module_level_imports(
406
+ fragment, PRODUCTION_FILE_PATH, full_file_content=full_file_with_syntax_error,
407
+ )
408
+ assert issues == [], (
409
+ "When the reconstructed post-edit content cannot be parsed, return empty "
410
+ f"rather than raising, got: {issues}"
411
+ )
412
+
413
+
414
+ def test_reconstruct_post_edit_returns_replaced_content(tmp_path: pathlib.Path) -> None:
415
+ target_file = tmp_path / "module.py"
416
+ target_file.write_text("ALPHA = 1\nBETA = 2\n", encoding="utf-8")
417
+ post_edit = _reconstruct_post_edit_file_content(
418
+ str(target_file), "BETA = 2", "BETA = 22\nGAMMA = 3",
419
+ )
420
+ assert post_edit == "ALPHA = 1\nBETA = 22\nGAMMA = 3\n", (
421
+ f"Helper must return the file body with the first occurrence replaced, got: {post_edit!r}"
422
+ )
423
+
424
+
425
+ def test_reconstruct_post_edit_returns_none_when_file_missing(tmp_path: pathlib.Path) -> None:
426
+ missing_file = tmp_path / "does_not_exist.py"
427
+ post_edit = _reconstruct_post_edit_file_content(
428
+ str(missing_file), "any_old", "any_new",
429
+ )
430
+ assert post_edit is None, (
431
+ f"Missing file must yield None so the caller treats it as 'no full-file context', got: {post_edit!r}"
432
+ )
433
+
434
+
435
+ def test_reconstruct_post_edit_returns_none_when_old_string_absent(tmp_path: pathlib.Path) -> None:
436
+ target_file = tmp_path / "module.py"
437
+ target_file.write_text("ALPHA = 1\n", encoding="utf-8")
438
+ post_edit = _reconstruct_post_edit_file_content(
439
+ str(target_file), "ZETA = 9", "OMEGA = 0",
440
+ )
441
+ assert post_edit is None, (
442
+ f"Absent old_string means the Edit will not apply cleanly — return None, got: {post_edit!r}"
443
+ )
444
+
445
+
446
+ def test_reconstruct_post_edit_replaces_only_first_occurrence(tmp_path: pathlib.Path) -> None:
447
+ target_file = tmp_path / "module.py"
448
+ target_file.write_text("X = 1\nX = 1\n", encoding="utf-8")
449
+ post_edit = _reconstruct_post_edit_file_content(
450
+ str(target_file), "X = 1", "X = 2",
451
+ )
452
+ assert post_edit == "X = 2\nX = 1\n", (
453
+ "Edit replaces only the first occurrence; helper must mirror that, got: "
454
+ f"{post_edit!r}"
455
+ )
456
+
457
+
300
458
  def test_should_flag_import_when_only_shadowed_local_name_is_loaded() -> None:
301
459
  source = (
302
460
  "import json\n"
@@ -0,0 +1,63 @@
1
+ """Unit tests for convergence-gate-blocker PreToolUse hook."""
2
+
3
+ import importlib.util
4
+ import pathlib
5
+ import sys
6
+
7
+ _HOOK_DIR = pathlib.Path(__file__).parent
8
+ if str(_HOOK_DIR) not in sys.path:
9
+ sys.path.insert(0, str(_HOOK_DIR))
10
+
11
+ import re
12
+
13
+ _GH_PR_READY_PATTERN = re.compile(r"\bgh\s+pr\s+ready\b(?![^&|;\n]*--undo)")
14
+
15
+ hook_spec = importlib.util.spec_from_file_location(
16
+ "convergence_gate_blocker",
17
+ _HOOK_DIR / "convergence_gate_blocker.py",
18
+ )
19
+ assert hook_spec is not None
20
+ assert hook_spec.loader is not None
21
+ hook_module = importlib.util.module_from_spec(hook_spec)
22
+ hook_spec.loader.exec_module(hook_module)
23
+ _resolve_pr_number = hook_module._resolve_pr_number
24
+
25
+
26
+ def test_matches_gh_pr_ready_with_number() -> None:
27
+ assert _GH_PR_READY_PATTERN.search("gh pr ready 418")
28
+
29
+
30
+ def test_matches_gh_pr_ready_without_number() -> None:
31
+ assert _GH_PR_READY_PATTERN.search("gh pr ready")
32
+
33
+
34
+ def test_matches_gh_pr_ready_with_flags() -> None:
35
+ assert not _GH_PR_READY_PATTERN.search("gh pr ready --undo")
36
+
37
+
38
+ def test_does_not_match_gh_pr_create() -> None:
39
+ assert not _GH_PR_READY_PATTERN.search("gh pr create --title T")
40
+
41
+
42
+ def test_does_not_match_gh_pr_view() -> None:
43
+ assert not _GH_PR_READY_PATTERN.search("gh pr view 418")
44
+
45
+
46
+ def test_does_not_match_gh_issue_close() -> None:
47
+ assert not _GH_PR_READY_PATTERN.search("gh issue close 42")
48
+
49
+
50
+ def test_extracts_pr_number_from_command() -> None:
51
+ assert _resolve_pr_number("gh pr ready 418", None) == 418
52
+
53
+
54
+ def test_extracts_pr_number_with_flags() -> None:
55
+ assert _resolve_pr_number("gh pr ready 99 --undo", None) == 99
56
+
57
+
58
+ def test_returns_none_when_no_number_and_no_repo() -> None:
59
+ assert _resolve_pr_number("gh pr ready", "/nonexistent/path") is None
60
+
61
+
62
+ def test_matches_gh_pr_ready_in_compound_command() -> None:
63
+ assert not _GH_PR_READY_PATTERN.search("gh pr ready --undo && gh pr create")
@@ -1008,3 +1008,149 @@ def test_git_reset_hard_asks_when_settings_file_is_invalid_json(tmp_path: Path)
1008
1008
 
1009
1009
  response = json.loads(result.stdout)
1010
1010
  assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
1011
+
1012
+
1013
+ # --- convergence branch exemption unit tests ---
1014
+
1015
+ import importlib.util
1016
+
1017
+ _HOOK_DIR = Path(__file__).parent
1018
+ _hook_spec = importlib.util.spec_from_file_location(
1019
+ "destructive_command_blocker",
1020
+ _HOOK_DIR / "destructive_command_blocker.py",
1021
+ )
1022
+ assert _hook_spec is not None
1023
+ assert _hook_spec.loader is not None
1024
+ _hook_module = importlib.util.module_from_spec(_hook_spec)
1025
+ _hook_spec.loader.exec_module(_hook_module)
1026
+ _force_push_targets_convergence_branch = _hook_module._force_push_targets_convergence_branch
1027
+ _is_convergence_branch = _hook_module._is_convergence_branch
1028
+ _all_refspecs_are_convergence_branches = _hook_module._all_refspecs_are_convergence_branches
1029
+
1030
+
1031
+ def test_convergence_branch_claude_prefix_allowed() -> None:
1032
+ assert _force_push_targets_convergence_branch(
1033
+ "git push --force origin claude/fix-123"
1034
+ )
1035
+
1036
+
1037
+ def test_convergence_branch_worktree_prefix_allowed() -> None:
1038
+ assert _force_push_targets_convergence_branch(
1039
+ "git push --force origin worktree-pr-converge-418"
1040
+ )
1041
+
1042
+
1043
+ def test_convergence_branch_pr_converge_allowed() -> None:
1044
+ assert _force_push_targets_convergence_branch(
1045
+ "git push --force origin pr-423-converge"
1046
+ )
1047
+
1048
+
1049
+ def test_convergence_branch_f_variant_allowed() -> None:
1050
+ assert _force_push_targets_convergence_branch(
1051
+ "git push -f origin claude/fix-123"
1052
+ )
1053
+
1054
+
1055
+ def test_convergence_branch_main_blocked() -> None:
1056
+ assert not _force_push_targets_convergence_branch(
1057
+ "git push --force origin main"
1058
+ )
1059
+
1060
+
1061
+ def test_convergence_branch_refspec_destination_checked() -> None:
1062
+ assert not _force_push_targets_convergence_branch(
1063
+ "git push --force origin claude/fix:main"
1064
+ )
1065
+
1066
+
1067
+ def test_convergence_branch_multi_refspec_main_blocked() -> None:
1068
+ assert not _force_push_targets_convergence_branch(
1069
+ "git push --force origin claude/fix-123 main"
1070
+ )
1071
+
1072
+
1073
+ def test_convergence_branch_multi_refspec_all_convergence() -> None:
1074
+ assert _force_push_targets_convergence_branch(
1075
+ "git push --force origin claude/fix-123 worktree-other"
1076
+ )
1077
+
1078
+
1079
+ def test_convergence_branch_multi_refspec_mixed_blocked() -> None:
1080
+ assert not _force_push_targets_convergence_branch(
1081
+ "git push --force origin claude/fix-123 main worktree-other"
1082
+ )
1083
+
1084
+
1085
+ def test_convergence_branch_compound_main_piggyback_blocked() -> None:
1086
+ assert not _force_push_targets_convergence_branch(
1087
+ "git push --force origin claude/foo && git push --force origin main"
1088
+ )
1089
+
1090
+
1091
+ def test_is_convergence_branch_claude_prefix() -> None:
1092
+ assert _is_convergence_branch("claude/fix-123")
1093
+
1094
+
1095
+ def test_is_convergence_branch_worktree_prefix() -> None:
1096
+ assert _is_convergence_branch("worktree-pr-418")
1097
+
1098
+
1099
+ def test_is_convergence_branch_pr_converge() -> None:
1100
+ assert _is_convergence_branch("pr-423-converge")
1101
+
1102
+
1103
+ def test_is_convergence_branch_main_rejected() -> None:
1104
+ assert not _is_convergence_branch("main")
1105
+
1106
+
1107
+ def test_is_convergence_branch_pr_converge_no_end_anchor() -> None:
1108
+ assert not _is_convergence_branch("pr-423-converge-extra")
1109
+
1110
+
1111
+ def test_all_refspecs_empty_string_returns_false() -> None:
1112
+ assert not _all_refspecs_are_convergence_branches("")
1113
+
1114
+
1115
+ def test_all_refspecs_whitespace_only_returns_false() -> None:
1116
+ assert not _all_refspecs_are_convergence_branches(" ")
1117
+
1118
+
1119
+ def test_all_refspecs_flag_only_returns_false() -> None:
1120
+ assert not _all_refspecs_are_convergence_branches("--no-verify")
1121
+
1122
+
1123
+ def test_all_refspecs_multiple_flags_only_returns_false() -> None:
1124
+ assert not _all_refspecs_are_convergence_branches("--no-verify --force")
1125
+
1126
+
1127
+ def test_all_refspecs_flag_then_branch_checks_branch() -> None:
1128
+ assert not _all_refspecs_are_convergence_branches("--force main")
1129
+
1130
+
1131
+ def test_all_refspecs_convergence_branch_with_flags() -> None:
1132
+ assert _all_refspecs_are_convergence_branches("--force claude/fix-123")
1133
+
1134
+
1135
+ def test_force_push_convergence_with_no_verify_blocked() -> None:
1136
+ payload = _make_bash_payload(
1137
+ "git push --force origin --no-verify claude/fix-123"
1138
+ )
1139
+
1140
+ result = _run_rm_hook(payload)
1141
+
1142
+ response = json.loads(result.stdout)
1143
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
1144
+ assert "--no-verify" in response["hookSpecificOutput"]["permissionDecisionReason"]
1145
+
1146
+
1147
+ def test_force_push_convergence_with_no_gpg_sign_blocked() -> None:
1148
+ payload = _make_bash_payload(
1149
+ "git push --force origin --no-gpg-sign claude/fix-123"
1150
+ )
1151
+
1152
+ result = _run_rm_hook(payload)
1153
+
1154
+ response = json.loads(result.stdout)
1155
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
1156
+ assert "--no-gpg-sign" in response["hookSpecificOutput"]["permissionDecisionReason"]
@@ -0,0 +1,102 @@
1
+ """Tests for --no-verify / --no-gpg-sign blocking in destructive_command_blocker.
2
+
3
+ git-workflow.md:30-33 marks these as NON-NEGOTIABLE to skip — they bypass
4
+ hook signing and verification. The blocker must ASK before allowing them.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+
13
+
14
+ SCRIPT_PATH = Path(__file__).parent / "destructive_command_blocker.py"
15
+
16
+
17
+ def _run_hook(payload: dict) -> subprocess.CompletedProcess[str]:
18
+ child_environment = os.environ.copy()
19
+ return subprocess.run(
20
+ [sys.executable, str(SCRIPT_PATH)],
21
+ input=json.dumps(payload),
22
+ text=True,
23
+ capture_output=True,
24
+ check=False,
25
+ env=child_environment,
26
+ )
27
+
28
+
29
+ def _make_bash_payload(command: str) -> dict:
30
+ return {"tool_name": "Bash", "tool_input": {"command": command}}
31
+
32
+
33
+ def test_asks_on_git_commit_no_verify() -> None:
34
+ payload = _make_bash_payload('git commit -m "wip" --no-verify')
35
+ result = _run_hook(payload)
36
+ response = json.loads(result.stdout)
37
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
38
+ f"Expected ask for git commit --no-verify, got: {response!r}"
39
+ )
40
+ assert "no-verify" in response["hookSpecificOutput"]["permissionDecisionReason"], (
41
+ f"Reason must mention --no-verify, got: {response!r}"
42
+ )
43
+
44
+
45
+ def test_asks_on_git_push_no_verify() -> None:
46
+ payload = _make_bash_payload("git push --no-verify origin main")
47
+ result = _run_hook(payload)
48
+ response = json.loads(result.stdout)
49
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
50
+ f"Expected ask for git push --no-verify, got: {response!r}"
51
+ )
52
+
53
+
54
+ def test_asks_on_git_no_gpg_sign() -> None:
55
+ payload = _make_bash_payload("git commit --no-gpg-sign -m wip")
56
+ result = _run_hook(payload)
57
+ response = json.loads(result.stdout)
58
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
59
+ f"Expected ask for git --no-gpg-sign, got: {response!r}"
60
+ )
61
+ assert (
62
+ "no-gpg-sign" in response["hookSpecificOutput"]["permissionDecisionReason"]
63
+ ), f"Reason must mention --no-gpg-sign, got: {response!r}"
64
+
65
+
66
+ def test_asks_on_git_commit_with_no_gpg_sign_config() -> None:
67
+ payload = _make_bash_payload("git -c commit.gpgsign=false commit -m wip")
68
+ result = _run_hook(payload)
69
+ response = json.loads(result.stdout)
70
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
71
+ f"Expected ask for -c commit.gpgsign=false, got: {response!r}"
72
+ )
73
+
74
+
75
+ def test_asks_on_quoted_gpgsign_config() -> None:
76
+ payload = _make_bash_payload("git -c 'commit.gpgsign=false' commit -m wip")
77
+ result = _run_hook(payload)
78
+ response = json.loads(result.stdout)
79
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
80
+ f"Expected ask for quoted -c commit.gpgsign=false, got: {response!r}"
81
+ )
82
+
83
+
84
+ def test_asks_on_value_quoted_gpgsign_config() -> None:
85
+ payload = _make_bash_payload("git -c commit.gpgsign='false' commit -m wip")
86
+ result = _run_hook(payload)
87
+ response = json.loads(result.stdout)
88
+ assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
89
+ f"Expected ask for value-quoted -c commit.gpgsign='false', got: {response!r}"
90
+ )
91
+
92
+
93
+ def test_normal_git_commit_passes() -> None:
94
+ payload = _make_bash_payload('git commit -m "real commit"')
95
+ result = _run_hook(payload)
96
+ if not result.stdout.strip():
97
+ return
98
+ response = json.loads(result.stdout)
99
+ decision = response.get("hookSpecificOutput", {}).get("permissionDecision", "allow")
100
+ assert decision != "ask", (
101
+ f"Normal git commit must not be flagged as destructive, got: {response!r}"
102
+ )
@@ -17,6 +17,7 @@ assert hook_spec.loader is not None
17
17
  hook_module = importlib.util.module_from_spec(hook_spec)
18
18
  hook_spec.loader.exec_module(hook_module)
19
19
  _uses_body_string_arg = hook_module._uses_body_string_arg
20
+ _has_backtick = hook_module._has_backtick
20
21
 
21
22
  from _gh_body_arg_utils import iter_significant_tokens
22
23
 
@@ -373,3 +374,47 @@ def test_all_body_flag_prefixes_used_for_equals_skip() -> None:
373
374
  for each_prefix in all_body_flag_prefixes:
374
375
  assert each_prefix in _all_equals_prefixes_for_skip
375
376
 
377
+
378
+ def test_has_backtick_with_plain_text_body() -> None:
379
+ assert not _has_backtick('gh issue comment 42 --body "bugbot run"')
380
+
381
+
382
+ def test_has_backtick_with_markdown_body() -> None:
383
+ assert _has_backtick('gh pr create --title "T" --body "Fixes `foo`"')
384
+
385
+
386
+ def test_has_backtick_with_short_b_plain_text() -> None:
387
+ assert not _has_backtick('gh pr comment 10 -b "LGTM"')
388
+
389
+
390
+ def test_has_backtick_with_equals_form_plain_text() -> None:
391
+ assert not _has_backtick('gh pr create --title "T" --body="bugbot run"')
392
+
393
+
394
+ def test_has_backtick_with_empty_body() -> None:
395
+ assert not _has_backtick('gh pr create --title "T" --body=""')
396
+
397
+
398
+ def test_has_backtick_bash_continuation_stripped() -> None:
399
+ """Bash backslash line continuations are stripped before checking."""
400
+ command = 'gh pr create \\\n --title "T" \\\n --body "bugbot run"\n'
401
+ assert not _has_backtick(command)
402
+
403
+
404
+ def test_has_backtick_powershell_continuation_stripped() -> None:
405
+ """PowerShell backtick line continuations are stripped before checking."""
406
+ command = 'gh pr create `\n --title "T" `\n --body "bugbot run"\n'
407
+ assert not _has_backtick(command)
408
+
409
+
410
+ def test_has_backtick_content_backtick_at_line_end() -> None:
411
+ """A backtick at end of line in body content must be detected (not mistaken for continuation)."""
412
+ command = 'gh pr create --title "T" --body "Thanks `\n@user"'
413
+ assert _has_backtick(command)
414
+
415
+
416
+ def test_has_backtick_multi_line_body() -> None:
417
+ """Backtick on a non-first line of a multi-line body must be detected."""
418
+ command = 'gh pr create --title "T" --body "First line.\nSecond with `code`."'
419
+ assert _has_backtick(command)
420
+