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,141 @@
1
+ """Tests for check_typed_dict_encode_decode — flags TypedDicts missing companion encoders.
2
+
3
+ Per Plan 1c.typed_dict_validator / Phase B2: every TypedDict declaration in
4
+ production code must have a companion `_encode_<snake_name>` and
5
+ `_decode_<snake_name>` function so untyped dicts cannot leak across module
6
+ boundaries without explicit validation.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib.util
12
+ from pathlib import Path
13
+ from types import ModuleType
14
+
15
+
16
+ def _load_enforcer_module() -> ModuleType:
17
+ module_path = Path(__file__).parent / "code_rules_enforcer.py"
18
+ spec = importlib.util.spec_from_file_location("code_rules_enforcer", module_path)
19
+ assert spec is not None
20
+ assert spec.loader is not None
21
+ module = importlib.util.module_from_spec(spec)
22
+ spec.loader.exec_module(module)
23
+ return module
24
+
25
+
26
+ code_rules_enforcer = _load_enforcer_module()
27
+
28
+
29
+ def check_typed_dict_encode_decode(content: str, file_path: str) -> list[str]:
30
+ return code_rules_enforcer.check_typed_dict_encode_decode(content, file_path)
31
+
32
+
33
+ PRODUCTION_FILE_PATH = "/project/src/contracts.py"
34
+ TEST_FILE_PATH = "/project/src/test_contracts.py"
35
+ HOOK_INFRASTRUCTURE_PATH = "/home/user/.claude/hooks/blocking/example.py"
36
+
37
+
38
+ def test_should_flag_typed_dict_without_encode_or_decode() -> None:
39
+ source = (
40
+ "from typing import TypedDict\n"
41
+ "class InvoicePayload(TypedDict):\n"
42
+ " amount: int\n"
43
+ )
44
+ issues = check_typed_dict_encode_decode(source, PRODUCTION_FILE_PATH)
45
+ assert any("InvoicePayload" in each for each in issues), (
46
+ f"Expected InvoicePayload to be flagged, got: {issues!r}"
47
+ )
48
+
49
+
50
+ def test_should_flag_typed_dict_with_only_encode() -> None:
51
+ source = (
52
+ "from typing import TypedDict\n"
53
+ "class InvoicePayload(TypedDict):\n"
54
+ " amount: int\n"
55
+ "def _encode_invoice_payload(value: InvoicePayload) -> bytes:\n"
56
+ " return b''\n"
57
+ )
58
+ issues = check_typed_dict_encode_decode(source, PRODUCTION_FILE_PATH)
59
+ assert any(
60
+ "InvoicePayload" in each and "decode" in each.lower() for each in issues
61
+ ), f"Expected missing _decode_ to be flagged, got: {issues!r}"
62
+
63
+
64
+ def test_should_flag_typed_dict_with_only_decode() -> None:
65
+ source = (
66
+ "from typing import TypedDict\n"
67
+ "class InvoicePayload(TypedDict):\n"
68
+ " amount: int\n"
69
+ "def _decode_invoice_payload(raw: bytes) -> InvoicePayload:\n"
70
+ " return {'amount': 0}\n"
71
+ )
72
+ issues = check_typed_dict_encode_decode(source, PRODUCTION_FILE_PATH)
73
+ assert any(
74
+ "InvoicePayload" in each and "encode" in each.lower() for each in issues
75
+ ), f"Expected missing _encode_ to be flagged, got: {issues!r}"
76
+
77
+
78
+ def test_should_not_flag_typed_dict_with_both_companions() -> None:
79
+ source = (
80
+ "from typing import TypedDict\n"
81
+ "class InvoicePayload(TypedDict):\n"
82
+ " amount: int\n"
83
+ "def _encode_invoice_payload(value: InvoicePayload) -> bytes:\n"
84
+ " return b''\n"
85
+ "def _decode_invoice_payload(raw: bytes) -> InvoicePayload:\n"
86
+ " return {'amount': 0}\n"
87
+ )
88
+ issues = check_typed_dict_encode_decode(source, PRODUCTION_FILE_PATH)
89
+ assert issues == [], f"Both companions present, got: {issues!r}"
90
+
91
+
92
+ def test_should_handle_pascal_to_snake_conversion() -> None:
93
+ source = (
94
+ "from typing import TypedDict\n"
95
+ "class TypedAuthRequest(TypedDict):\n"
96
+ " token: str\n"
97
+ )
98
+ issues = check_typed_dict_encode_decode(source, PRODUCTION_FILE_PATH)
99
+ assert any("TypedAuthRequest" in each for each in issues), (
100
+ f"PascalCase conversion expected; got: {issues!r}"
101
+ )
102
+
103
+
104
+ def test_should_skip_test_file() -> None:
105
+ source = "from typing import TypedDict\nclass MockPayload(TypedDict):\n x: int\n"
106
+ issues = check_typed_dict_encode_decode(source, TEST_FILE_PATH)
107
+ assert issues == [], f"Test files exempt, got: {issues!r}"
108
+
109
+
110
+ def test_should_skip_hook_infrastructure() -> None:
111
+ source = "from typing import TypedDict\nclass HookPayload(TypedDict):\n x: int\n"
112
+ issues = check_typed_dict_encode_decode(source, HOOK_INFRASTRUCTURE_PATH)
113
+ assert issues == [], f"Hook infrastructure exempt, got: {issues!r}"
114
+
115
+
116
+ def test_should_handle_syntax_error_gracefully() -> None:
117
+ source = "class InvoicePayload(TypedDict\n"
118
+ issues = check_typed_dict_encode_decode(source, PRODUCTION_FILE_PATH)
119
+ assert issues == [], f"Syntax error must yield no issues, got: {issues!r}"
120
+
121
+
122
+ def test_should_not_flag_typed_dict_nested_inside_class() -> None:
123
+ source = (
124
+ "from typing import TypedDict\n"
125
+ "class Service:\n"
126
+ " class RequestPayload(TypedDict):\n"
127
+ " token: str\n"
128
+ )
129
+ issues = check_typed_dict_encode_decode(source, PRODUCTION_FILE_PATH)
130
+ assert issues == [], f"Nested TypedDict must not be flagged, got: {issues!r}"
131
+
132
+
133
+ def test_should_not_flag_non_typed_dict_class() -> None:
134
+ source = (
135
+ "from dataclasses import dataclass\n"
136
+ "@dataclass\n"
137
+ "class Invoice:\n"
138
+ " amount: int\n"
139
+ )
140
+ issues = check_typed_dict_encode_decode(source, PRODUCTION_FILE_PATH)
141
+ assert issues == [], f"Regular dataclass must not be flagged, got: {issues!r}"
@@ -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
+