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,452 @@
1
+ """Tests for md_to_html_companion hook.
2
+
3
+ This test suite validates that the md-to-html companion hook correctly
4
+ generates HTML from markdown input, handles edge cases, and produces
5
+ valid HTML output.
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import subprocess
11
+ import sys
12
+ import tempfile
13
+
14
+
15
+ HOOK_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "md_to_html_companion.py")
16
+
17
+
18
+ class _RunHook:
19
+ def __call__(self, tool_name: str, tool_input: dict) -> subprocess.CompletedProcess:
20
+ payload = json.dumps({"tool_name": tool_name, "tool_input": tool_input})
21
+ return subprocess.run(
22
+ [sys.executable, HOOK_SCRIPT_PATH],
23
+ input=payload,
24
+ capture_output=True,
25
+ text=True,
26
+ check=False,
27
+ )
28
+
29
+
30
+ _run_hook = _RunHook()
31
+
32
+
33
+ def test_generates_html_companion():
34
+ with tempfile.TemporaryDirectory() as tmp:
35
+ md_path = os.path.join(tmp, "guide.md")
36
+ html_path = os.path.join(tmp, "guide.html")
37
+
38
+
39
+ with open(md_path, "w", encoding="utf-8") as f:
40
+ f.write("# Hello\n\nThis is a test.")
41
+
42
+ result = _run_hook(
43
+ "Write", {"file_path": md_path, "content": "# Hello\n\nThis is a test."}
44
+ )
45
+ assert result.returncode == 0
46
+ assert os.path.exists(html_path)
47
+
48
+
49
+ def test_html_contains_heading():
50
+ with tempfile.TemporaryDirectory() as tmp:
51
+ md_path = os.path.join(tmp, "guide.md")
52
+ with open(md_path, "w", encoding="utf-8") as f:
53
+ f.write("# Hello World")
54
+
55
+ _run_hook("Write", {"file_path": md_path, "content": "# Hello World"})
56
+ html_path = os.path.join(tmp, "guide.html")
57
+ with open(html_path, encoding="utf-8") as f:
58
+ html = f.read()
59
+ assert "<h1>" in html
60
+ assert "Hello World" in html
61
+
62
+
63
+ def test_html_wraps_in_template():
64
+ with tempfile.TemporaryDirectory() as tmp:
65
+ md_path = os.path.join(tmp, "guide.md")
66
+ with open(md_path, "w", encoding="utf-8") as f:
67
+ f.write("plain text")
68
+
69
+ _run_hook("Write", {"file_path": md_path, "content": "plain text"})
70
+ html_path = os.path.join(tmp, "guide.html")
71
+ with open(html_path, encoding="utf-8") as f:
72
+ html = f.read()
73
+ assert "<!DOCTYPE html>" in html
74
+ assert "<style>" in html
75
+
76
+
77
+ def test_skips_non_md_files():
78
+ with tempfile.TemporaryDirectory() as tmp:
79
+ py_path = os.path.join(tmp, "main.py")
80
+ html_path = os.path.join(tmp, "main.html")
81
+
82
+
83
+ with open(py_path, "w", encoding="utf-8") as f:
84
+ f.write("x = 1")
85
+
86
+ result = _run_hook("Write", {"file_path": py_path, "content": "x = 1"})
87
+ assert result.returncode == 0
88
+ assert not os.path.exists(html_path)
89
+
90
+
91
+ def test_skips_claude_dir():
92
+ with tempfile.TemporaryDirectory() as tmp:
93
+ claude_dir = os.path.join(tmp, ".claude")
94
+ md_path = os.path.join(claude_dir, "CLAUDE.md")
95
+ html_path = os.path.join(claude_dir, "CLAUDE.html")
96
+
97
+ os.makedirs(claude_dir, exist_ok=True)
98
+ with open(md_path, "w", encoding="utf-8") as f:
99
+ f.write("# CLAUDE.md")
100
+
101
+ result = _run_hook("Write", {"file_path": md_path, "content": "# CLAUDE.md"})
102
+ assert result.returncode == 0
103
+ assert not os.path.exists(html_path)
104
+
105
+
106
+ def test_unknown_tool_passes():
107
+ result = _run_hook("Grep", {"pattern": "foo"})
108
+ assert result.returncode == 0
109
+ assert result.stdout == ""
110
+
111
+
112
+ def test_empty_file_path_passes():
113
+ result = _run_hook("Write", {"file_path": "", "content": "# Hello"})
114
+ assert result.returncode == 0
115
+ assert result.stdout == ""
116
+
117
+
118
+ def test_nonexistent_md_passes():
119
+ result = _run_hook(
120
+ "Write",
121
+ {"file_path": "/nonexistent/path/guide.md", "content": "# Hello"},
122
+ )
123
+ assert result.returncode == 0
124
+
125
+
126
+ def test_converts_code_fence():
127
+ with tempfile.TemporaryDirectory() as tmp:
128
+ md_path = os.path.join(tmp, "guide.md")
129
+ with open(md_path, "w", encoding="utf-8") as f:
130
+ f.write("```python\nprint('hi')\n```")
131
+
132
+ _run_hook(
133
+ "Write", {"file_path": md_path, "content": "```python\nprint('hi')\n```"}
134
+ )
135
+ html_path = os.path.join(tmp, "guide.html")
136
+ with open(html_path, encoding="utf-8") as f:
137
+ html = f.read()
138
+ assert "<pre>" in html
139
+ assert "<code" in html
140
+ assert "print(&#x27;hi&#x27;)" in html
141
+
142
+
143
+ def test_converts_bold():
144
+ with tempfile.TemporaryDirectory() as tmp:
145
+ md_path = os.path.join(tmp, "guide.md")
146
+ with open(md_path, "w", encoding="utf-8") as f:
147
+ f.write("This is **bold** text.")
148
+
149
+ _run_hook("Write", {"file_path": md_path, "content": "This is **bold** text."})
150
+ html_path = os.path.join(tmp, "guide.html")
151
+ with open(html_path, encoding="utf-8") as f:
152
+ html = f.read()
153
+ assert "<strong>bold</strong>" in html
154
+
155
+
156
+ def test_escapes_html_special_chars():
157
+ with tempfile.TemporaryDirectory() as tmp:
158
+ md_path = os.path.join(tmp, "guide.md")
159
+ with open(md_path, "w", encoding="utf-8") as f:
160
+ f.write("Use <div> for layout & choose \"text\" for quotes.")
161
+
162
+ _run_hook(
163
+ "Write",
164
+ {
165
+ "file_path": md_path,
166
+ "content": "Use <div> for layout & choose \"text\" for quotes.",
167
+ },
168
+ )
169
+ html_path = os.path.join(tmp, "guide.html")
170
+ with open(html_path, encoding="utf-8") as f:
171
+ html = f.read()
172
+ assert "&lt;div&gt;" in html
173
+ assert "&amp;" in html
174
+ assert "<div>" not in html
175
+
176
+
177
+ def test_escapes_code_block_content():
178
+ with tempfile.TemporaryDirectory() as tmp:
179
+ md_path = os.path.join(tmp, "guide.md")
180
+ with open(md_path, "w", encoding="utf-8") as f:
181
+ f.write("```\nif x < 5 and y > 3:\n print('hello')\n```")
182
+
183
+ _run_hook(
184
+ "Write",
185
+ {
186
+ "file_path": md_path,
187
+ "content": "```\nif x < 5 and y > 3:\n print('hello')\n```",
188
+ },
189
+ )
190
+ html_path = os.path.join(tmp, "guide.html")
191
+ with open(html_path, encoding="utf-8") as f:
192
+ html = f.read()
193
+ assert "&lt;" in html
194
+ assert "if x" in html
195
+
196
+
197
+ def test_lists_are_wrapped_in_ul():
198
+ with tempfile.TemporaryDirectory() as tmp:
199
+ md_path = os.path.join(tmp, "guide.md")
200
+ with open(md_path, "w", encoding="utf-8") as f:
201
+ f.write("- item one\n- item two\n- item three")
202
+
203
+ _run_hook(
204
+ "Write",
205
+ {
206
+ "file_path": md_path,
207
+ "content": "- item one\n- item two\n- item three",
208
+ },
209
+ )
210
+ html_path = os.path.join(tmp, "guide.html")
211
+ with open(html_path, encoding="utf-8") as f:
212
+ html = f.read()
213
+ assert "<ul>" in html
214
+ assert "</ul>" in html
215
+ assert html.index("<ul>") < html.index("<li>item one</li>")
216
+ assert html.index("</li>") < html.index("</ul>")
217
+
218
+
219
+ def test_ordered_lists_are_wrapped_in_ol():
220
+ with tempfile.TemporaryDirectory() as tmp:
221
+ md_path = os.path.join(tmp, "guide.md")
222
+ with open(md_path, "w", encoding="utf-8") as f:
223
+ f.write("1. first\n2. second")
224
+
225
+ _run_hook(
226
+ "Write",
227
+ {"file_path": md_path, "content": "1. first\n2. second"},
228
+ )
229
+ html_path = os.path.join(tmp, "guide.html")
230
+ with open(html_path, encoding="utf-8") as f:
231
+ html = f.read()
232
+ assert "<ol>" in html
233
+ assert "</ol>" in html
234
+
235
+
236
+ def test_handles_curly_braces_in_body():
237
+ with tempfile.TemporaryDirectory() as tmp:
238
+ md_path = os.path.join(tmp, "guide.md")
239
+ with open(md_path, "w", encoding="utf-8") as f:
240
+ f.write("# JS Example\n\nUse `{ foo: 1 }` in code.")
241
+
242
+ _run_hook(
243
+ "Write",
244
+ {
245
+ "file_path": md_path,
246
+ "content": "# JS Example\n\nUse `{ foo: 1 }` in code.",
247
+ },
248
+ )
249
+ html_path = os.path.join(tmp, "guide.html")
250
+ with open(html_path, encoding="utf-8") as f:
251
+ html = f.read()
252
+ assert "{ foo: 1 }" in html
253
+ assert "{{" not in html
254
+ assert "JS Example" in html
255
+
256
+
257
+ def test_escapes_title_in_html_output():
258
+ with tempfile.TemporaryDirectory() as tmp:
259
+ md_path = os.path.join(tmp, "guide.md")
260
+ with open(md_path, "w", encoding="utf-8") as f:
261
+ f.write("# Hackers <3 Markdown & <scripts>")
262
+
263
+ _run_hook(
264
+ "Write",
265
+ {
266
+ "file_path": md_path,
267
+ "content": "# Hackers <3 Markdown & <scripts>",
268
+ },
269
+ )
270
+ html_path = os.path.join(tmp, "guide.html")
271
+ with open(html_path, encoding="utf-8") as f:
272
+ html = f.read()
273
+ assert "<title>Hackers &lt;3 Markdown &amp; &lt;scripts&gt;</title>" in html
274
+ assert "<script>" not in html
275
+
276
+
277
+ def test_skips_root_readme():
278
+ with tempfile.TemporaryDirectory() as tmp:
279
+ original_cwd = os.getcwd()
280
+ try:
281
+ os.chdir(tmp)
282
+ for each_name in ("README.md", "readme.md"):
283
+ with open(each_name, "w", encoding="utf-8") as f:
284
+ f.write("# Test")
285
+ result = _run_hook(
286
+ "Write", {"file_path": each_name, "content": "# Test"}
287
+ )
288
+ assert result.returncode == 0
289
+ expected_html = each_name.replace(".md", ".html")
290
+ assert not os.path.exists(expected_html)
291
+ finally:
292
+ os.chdir(original_cwd)
293
+
294
+
295
+ def test_skips_root_changelog():
296
+ with tempfile.TemporaryDirectory() as tmp:
297
+ original_cwd = os.getcwd()
298
+ try:
299
+ os.chdir(tmp)
300
+ for each_name in ("CHANGELOG.md", "changelog.md"):
301
+ with open(each_name, "w", encoding="utf-8") as f:
302
+ f.write("# Test")
303
+ result = _run_hook(
304
+ "Write", {"file_path": each_name, "content": "# Test"}
305
+ )
306
+ assert result.returncode == 0
307
+ expected_html = each_name.replace(".md", ".html")
308
+ assert not os.path.exists(expected_html)
309
+ finally:
310
+ os.chdir(original_cwd)
311
+
312
+
313
+ def test_language_class_valid():
314
+ with tempfile.TemporaryDirectory() as tmp:
315
+ md_path = os.path.join(tmp, "guide.md")
316
+ with open(md_path, "w", encoding="utf-8") as f:
317
+ f.write("```python\nx = 1\n```")
318
+
319
+ _run_hook("Write", {"file_path": md_path, "content": "```python\nx = 1\n```"})
320
+ html_path = os.path.join(tmp, "guide.html")
321
+ with open(html_path, encoding="utf-8") as f:
322
+ html = f.read()
323
+ assert 'class="language-python"' in html
324
+
325
+
326
+ def test_language_class_skips_invalid():
327
+ with tempfile.TemporaryDirectory() as tmp:
328
+ md_path = os.path.join(tmp, "guide.md")
329
+ with open(md_path, "w", encoding="utf-8") as f:
330
+ f.write("```my lang\nx = 1\n```")
331
+
332
+ _run_hook("Write", {"file_path": md_path, "content": "```my lang\nx = 1\n```"})
333
+ html_path = os.path.join(tmp, "guide.html")
334
+ with open(html_path, encoding="utf-8") as f:
335
+ html = f.read()
336
+ assert "<pre><code>" in html
337
+ assert 'class="language-' not in html
338
+
339
+
340
+ def test_language_class_allows_valid_chars():
341
+ with tempfile.TemporaryDirectory() as tmp:
342
+ md_path = os.path.join(tmp, "guide.md")
343
+ with open(md_path, "w", encoding="utf-8") as f:
344
+ f.write("```c++\nint x = 1;\n```")
345
+
346
+ _run_hook("Write", {"file_path": md_path, "content": "```c++\nint x = 1;\n```"})
347
+ html_path = os.path.join(tmp, "guide.html")
348
+ with open(html_path, encoding="utf-8") as f:
349
+ html = f.read()
350
+ assert 'class="language-c++"' in html
351
+
352
+
353
+ def test_link_text_asterisks_remain_literal():
354
+ with tempfile.TemporaryDirectory() as tmp:
355
+ md_path = os.path.join(tmp, "guide.md")
356
+ with open(md_path, "w", encoding="utf-8") as f:
357
+ f.write("See [text *not italic*](url).")
358
+
359
+ _run_hook(
360
+ "Write",
361
+ {"file_path": md_path, "content": "See [text *not italic*](url)."},
362
+ )
363
+ html_path = os.path.join(tmp, "guide.html")
364
+ with open(html_path, encoding="utf-8") as f:
365
+ html = f.read()
366
+ assert '<a href="url">text *not italic*</a>' in html
367
+ assert "<em>" not in html
368
+
369
+
370
+ def test_handles_parentheses_in_links():
371
+ with tempfile.TemporaryDirectory() as tmp:
372
+ md_path = os.path.join(tmp, "guide.md")
373
+ with open(md_path, "w", encoding="utf-8") as f:
374
+ f.write(
375
+ "See [Python]"
376
+ "(https://en.wikipedia.org/wiki/Python_(programming_language))."
377
+ )
378
+
379
+ _run_hook(
380
+ "Write",
381
+ {
382
+ "file_path": md_path,
383
+ "content": "See [Python]"
384
+ "(https://en.wikipedia.org/wiki/Python_(programming_language)).",
385
+ },
386
+ )
387
+ html_path = os.path.join(tmp, "guide.html")
388
+ with open(html_path, encoding="utf-8") as f:
389
+ html = f.read()
390
+ assert (
391
+ 'href="https://en.wikipedia.org/wiki/Python_(programming_language)"'
392
+ in html
393
+ )
394
+
395
+
396
+ def test_does_not_skip_nested_readme():
397
+ with tempfile.TemporaryDirectory() as tmp:
398
+ nested_dir = os.path.join(tmp, "docs")
399
+ os.makedirs(nested_dir)
400
+ md_path = os.path.join(nested_dir, "README.md")
401
+ html_path = os.path.join(nested_dir, "README.html")
402
+
403
+ with open(md_path, "w", encoding="utf-8") as f:
404
+ f.write("# Nested README")
405
+
406
+ result = _run_hook(
407
+ "Write",
408
+ {"file_path": md_path, "content": "# Nested README"},
409
+ )
410
+ assert result.returncode == 0
411
+ assert os.path.exists(html_path)
412
+
413
+
414
+ def test_inline_code_preserves_asterisks():
415
+ with tempfile.TemporaryDirectory() as tmp:
416
+ md_path = os.path.join(tmp, "guide.md")
417
+ with open(md_path, "w", encoding="utf-8") as f:
418
+ f.write("Type `**bold**` in a docstring.")
419
+
420
+ _run_hook(
421
+ "Write",
422
+ {
423
+ "file_path": md_path,
424
+ "content": "Type `**bold**` in a docstring.",
425
+ },
426
+ )
427
+ html_path = os.path.join(tmp, "guide.html")
428
+ with open(html_path, encoding="utf-8") as f:
429
+ html = f.read()
430
+ assert "<code>**bold**</code>" in html
431
+ assert "<strong>" not in html
432
+
433
+
434
+ def test_blocks_javascript_url_scheme():
435
+ with tempfile.TemporaryDirectory() as tmp:
436
+ md_path = os.path.join(tmp, "guide.md")
437
+ with open(md_path, "w", encoding="utf-8") as f:
438
+ f.write("[click me](javascript:alert(1))")
439
+
440
+ _run_hook(
441
+ "Write",
442
+ {
443
+ "file_path": md_path,
444
+ "content": "[click me](javascript:alert(1))",
445
+ },
446
+ )
447
+ html_path = os.path.join(tmp, "guide.html")
448
+ with open(html_path, encoding="utf-8") as f:
449
+ html = f.read()
450
+ assert "javascript:" not in html
451
+ assert "click me" in html
452
+ assert "<a" not in html
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.38.1",
3
+ "version": "1.40.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,7 @@
1
1
  # gh --body-file Rule
2
2
 
3
+ **MCP note:** MCP tools accept `body` as a structured string parameter and are unaffected by shell quoting. This rule applies to `gh` CLI invocations issued through the `Bash` tool.
4
+
3
5
  **Root cause:** In shell-invoked `gh` command contexts used in this repo, passing markdown body text via `--body "..."` can cause backticks to be stored as `\`` literals on GitHub instead of rendering as markdown code formatting. Quoting and escaping rules vary by execution environment (Bash, PowerShell, CMD), but the failure mode is the same: inline code and code fences can be broken in issues, PR descriptions, comments, and reviews written this way.
4
6
 
5
7
  **Rule:** All `gh` commands that include markdown body content **must** use `--body-file <path>` with a temp file. Never pass body text as a string argument to `--body` or its shorthand `-b`.
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env pwsh
2
+ <#
3
+ .SYNOPSIS
4
+ Install or remove the scheduled task that sweeps empty directories.
5
+
6
+ .DESCRIPTION
7
+ Registers a scheduled task that runs sweep_empty_dirs.py --once every N minutes
8
+ against a target directory. Defaults: every 5 minutes, age threshold 120 seconds.
9
+
10
+ Install-SweepEmptyDirs.ps1 -Target "C:\path\to\watch"
11
+ Install-SweepEmptyDirs.ps1 -Target "C:\path\to\watch" -IntervalMinutes 10 -AgeSeconds 300 # custom
12
+ Install-SweepEmptyDirs.ps1 -Remove
13
+ Install-SweepEmptyDirs.ps1 -Status
14
+ #>
15
+
16
+ param(
17
+ [Parameter(ParameterSetName = "install")]
18
+ [string]$Target,
19
+
20
+ [Parameter(ParameterSetName = "install")]
21
+ [ValidateRange(1, [int]::MaxValue)]
22
+ [int]$IntervalMinutes = 5,
23
+
24
+ [Parameter(ParameterSetName = "install")]
25
+ [ValidateRange(1, [int]::MaxValue)]
26
+ [int]$AgeSeconds = 120,
27
+
28
+ [Parameter(ParameterSetName = "install")]
29
+ [DateTime]$StartAt = (Get-Date),
30
+
31
+ [Parameter(ParameterSetName = "remove")]
32
+ [switch]$Remove,
33
+
34
+ [Parameter(ParameterSetName = "status")]
35
+ [switch]$Status
36
+ )
37
+
38
+ $TaskName = "SweepEmptyDirs"
39
+
40
+ if ($Status) {
41
+ $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
42
+ if (-not $task) {
43
+ Write-Host "STATUS: $TaskName is not registered."
44
+ return
45
+ }
46
+ Write-Host "STATUS: $TaskName is registered."
47
+ Write-Host " State: $($task.State)"
48
+ Write-Host " Actions:"
49
+ foreach ($each_action in $task.Actions) {
50
+ Write-Host " $($each_action.Execute) $($each_action.Arguments)"
51
+ }
52
+ Write-Host " Triggers:"
53
+ foreach ($each_trigger in $task.Triggers) {
54
+ Write-Host " $($each_trigger.Repetition.Interval) (starting $($each_trigger.StartBoundary))"
55
+ }
56
+ return
57
+ }
58
+
59
+ if ($Remove) {
60
+ Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
61
+ if (-not $?) {
62
+ Write-Warning "Failed to unregister scheduled task '$TaskName'."
63
+ } else {
64
+ Write-Host "$TaskName removed."
65
+ }
66
+ return
67
+ }
68
+
69
+ $ScriptDir = Split-Path -Parent $PSCommandPath
70
+ $ScriptPath = Join-Path $ScriptDir "sweep_empty_dirs.py"
71
+
72
+ if (-not (Test-Path $ScriptPath)) {
73
+ Write-Error "sweep_empty_dirs.py not found at: $ScriptPath"
74
+ exit 1
75
+ }
76
+
77
+ if (-not $Target) {
78
+ Write-Error "Parameter -Target is required (the directory to watch)."
79
+ exit 1
80
+ }
81
+
82
+ if (-not (Test-Path -PathType Container $Target)) {
83
+ Write-Error "Target directory does not exist: $Target"
84
+ exit 1
85
+ }
86
+
87
+ $_py = Get-Command py -ErrorAction SilentlyContinue
88
+ $PythonPath = if ($_py) { $_py.Source } else { (Get-Command python -ErrorAction SilentlyContinue).Source }
89
+ if (-not $PythonPath) {
90
+ Write-Error "Cannot find Python (py or python) on PATH."
91
+ exit 1
92
+ }
93
+ & $PythonPath --version 2>$null
94
+ if (-not $?) {
95
+ Write-Error "Python found at $PythonPath but failed to run."
96
+ exit 1
97
+ }
98
+
99
+ $Target = (Resolve-Path $Target).Path
100
+ $Target = [System.IO.Path]::TrimEndingDirectorySeparator($Target)
101
+
102
+ $Action = New-ScheduledTaskAction -Execute $PythonPath -Argument """$ScriptPath"" --once --age $AgeSeconds ""$Target"""
103
+ $Trigger = New-ScheduledTaskTrigger -Once -At $StartAt -RepetitionInterval (New-TimeSpan -Minutes $IntervalMinutes) -RepetitionDuration (New-TimeSpan -Days 31)
104
+ $Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
105
+
106
+ $null = Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $Trigger -Settings $Settings -Force
107
+ if (-not $?) {
108
+ Write-Error "Failed to register scheduled task."
109
+ exit 1
110
+ }
111
+ Write-Host "$TaskName registered — runs every ${IntervalMinutes}min against '$Target' (age > ${AgeSeconds}s)."
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env pwsh
2
+ <#
3
+ .SYNOPSIS
4
+ One-shot quality gate for the hooks package — runs ruff, mypy, and the
5
+ blocking pytest suite from a single entry point.
6
+
7
+ .DESCRIPTION
8
+ Resolves paths relative to $PSScriptRoot so the script works from any CWD
9
+ and from both the worktree (packages/claude-dev-env/scripts/check.ps1)
10
+ and the installed runtime (~/.claude/scripts/check.ps1, after install.mjs
11
+ propagates this file). Each tool runs sequentially; the first non-zero
12
+ exit code is preserved as the script's exit code so CI/pre-commit can
13
+ short-circuit on the first failure.
14
+
15
+ .PARAMETER SkipTests
16
+ Skip the pytest run. Useful during local iteration when you want only the
17
+ static-analysis gates.
18
+
19
+ .PARAMETER SkipMypy
20
+ Skip the mypy run.
21
+
22
+ .PARAMETER SkipRuff
23
+ Skip the ruff run.
24
+
25
+ .OUTPUTS
26
+ Per-tool status lines on stdout. Final summary line:
27
+ CHECK: OK
28
+ CHECK: FAILED tools=ruff,mypy,pytest
29
+ #>
30
+ [CmdletBinding()]
31
+ param(
32
+ [switch]$SkipTests,
33
+ [switch]$SkipMypy,
34
+ [switch]$SkipRuff
35
+ )
36
+
37
+ $ErrorActionPreference = 'Stop'
38
+
39
+ $hooksRoot = Resolve-Path (Join-Path $PSScriptRoot '..' 'hooks')
40
+ $blockingRoot = Join-Path $hooksRoot 'blocking'
41
+
42
+ $failedTools = @()
43
+ $firstNonZeroExitCode = 0
44
+
45
+ function Invoke-Tool {
46
+ param(
47
+ [string]$Label,
48
+ [scriptblock]$Action
49
+ )
50
+ Write-Host ""
51
+ Write-Host "==> $Label" -ForegroundColor Cyan
52
+ & $Action
53
+ $exitCode = $LASTEXITCODE
54
+ if ($exitCode -ne 0) {
55
+ $script:failedTools += $Label
56
+ if ($script:firstNonZeroExitCode -eq 0) {
57
+ $script:firstNonZeroExitCode = $exitCode
58
+ }
59
+ Write-Host "$Label FAILED (exit $exitCode)" -ForegroundColor Red
60
+ } else {
61
+ Write-Host "$Label OK" -ForegroundColor Green
62
+ }
63
+ }
64
+
65
+ if (-not $SkipRuff) {
66
+ Invoke-Tool -Label 'ruff' -Action {
67
+ Push-Location $hooksRoot
68
+ try {
69
+ ruff check .
70
+ } finally {
71
+ Pop-Location
72
+ }
73
+ }
74
+ }
75
+
76
+ if (-not $SkipMypy) {
77
+ Invoke-Tool -Label 'mypy' -Action {
78
+ Push-Location $hooksRoot
79
+ try {
80
+ mypy --config-file (Join-Path $hooksRoot 'pyproject.toml') blocking validators
81
+ } finally {
82
+ Pop-Location
83
+ }
84
+ }
85
+ }
86
+
87
+ if (-not $SkipTests) {
88
+ Invoke-Tool -Label 'pytest' -Action {
89
+ Push-Location $blockingRoot
90
+ try {
91
+ python -m pytest (Get-ChildItem test_code_rules_enforcer*.py)
92
+ } finally {
93
+ Pop-Location
94
+ }
95
+ }
96
+ }
97
+
98
+ Write-Host ""
99
+ if ($failedTools.Count -eq 0) {
100
+ Write-Host "CHECK: OK" -ForegroundColor Green
101
+ exit 0
102
+ } else {
103
+ $joined = ($failedTools -join ',')
104
+ Write-Host "CHECK: FAILED tools=$joined" -ForegroundColor Red
105
+ exit $firstNonZeroExitCode
106
+ }
@@ -0,0 +1,11 @@
1
+ """Timing constants for sweep_empty_dirs.
2
+
3
+ Per the project's configuration conventions, timeouts, delays, and retries
4
+ live in config/timing.py.
5
+ """
6
+
7
+ DEFAULT_AGE_SECONDS: int = 120
8
+ """Minimum age before an empty directory is eligible for deletion."""
9
+
10
+ DEFAULT_POLL_INTERVAL: int = 30
11
+ """Seconds between sweep passes in continuous-watch mode."""