claude-dev-env 1.0.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 (215) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/agents/agent-writer.md +157 -0
  4. package/agents/clasp-deployment-orchestrator.md +609 -0
  5. package/agents/clean-coder.md +295 -0
  6. package/agents/code-quality-agent.md +40 -0
  7. package/agents/code-standards-agent.md +93 -0
  8. package/agents/config-centralizer.md +686 -0
  9. package/agents/config-extraction-agent.md +225 -0
  10. package/agents/doc-orchestrator.md +47 -0
  11. package/agents/docs-agent.md +112 -0
  12. package/agents/docx-agent.md +211 -0
  13. package/agents/git-commit-crafter.md +100 -0
  14. package/agents/magic-value-eliminator-agent.md +72 -0
  15. package/agents/mandatory-agent-workflow-agent.md +88 -0
  16. package/agents/parallel-workflow-coordinator.md +779 -0
  17. package/agents/pdf-agent.md +302 -0
  18. package/agents/plan-executor.md +226 -0
  19. package/agents/pr-description-writer.md +87 -0
  20. package/agents/project-context-loader.md +238 -0
  21. package/agents/project-docs-analyzer.md +54 -0
  22. package/agents/project-structure-organizer-agent.md +72 -0
  23. package/agents/readability-review-agent.md +76 -0
  24. package/agents/refactoring-specialist.md +69 -0
  25. package/agents/right-sized-engineer.md +129 -0
  26. package/agents/session-continuity-manager.md +53 -0
  27. package/agents/skill-to-agent-converter.md +371 -0
  28. package/agents/skill-writer-agent.md +470 -0
  29. package/agents/stub-detector-agent.md +140 -0
  30. package/agents/tdd-test-writer.md +62 -0
  31. package/agents/test-data-builder.md +68 -0
  32. package/agents/tooling-builder.md +78 -0
  33. package/agents/user-docs-writer.md +67 -0
  34. package/agents/validation-expert.md +71 -0
  35. package/agents/workflow-visual-documenter.md +82 -0
  36. package/agents/xlsx-agent.md +169 -0
  37. package/bin/install.mjs +256 -0
  38. package/commands/commit.md +28 -0
  39. package/commands/docupdate.md +322 -0
  40. package/commands/implement.md +102 -0
  41. package/commands/initialize.md +91 -0
  42. package/commands/plan.md +63 -0
  43. package/commands/pr-comments.md +47 -0
  44. package/commands/readability-review.md +20 -0
  45. package/commands/review-plan.md +7 -0
  46. package/commands/right-size.md +15 -0
  47. package/commands/stubcheck.md +89 -0
  48. package/commands/sum.md +30 -0
  49. package/docs/CODE_RULES.md +186 -0
  50. package/docs/DJANGO_PATTERNS.md +80 -0
  51. package/docs/REACT_PATTERNS.md +185 -0
  52. package/docs/TEST_QUALITY.md +104 -0
  53. package/hooks/advisory/migration-safety-advisor.py +49 -0
  54. package/hooks/advisory/refactor-guard.py +205 -0
  55. package/hooks/blocking/block-main-commit.py +168 -0
  56. package/hooks/blocking/code-rules-enforcer.py +549 -0
  57. package/hooks/blocking/destructive-command-blocker.py +107 -0
  58. package/hooks/blocking/docker-settings-guard.py +44 -0
  59. package/hooks/blocking/hedging-language-blocker.py +130 -0
  60. package/hooks/blocking/parallel-task-blocker.py +69 -0
  61. package/hooks/blocking/pr-description-enforcer.py +87 -0
  62. package/hooks/blocking/pyautogui-scroll-blocker.py +74 -0
  63. package/hooks/blocking/sensitive-file-protector.py +70 -0
  64. package/hooks/blocking/tdd-enforcer.py +62 -0
  65. package/hooks/blocking/test-preflight-check.py +343 -0
  66. package/hooks/blocking/write-existing-file-blocker.py +63 -0
  67. package/hooks/git-hooks/post-commit.py +103 -0
  68. package/hooks/github-action/test_workflow.py +33 -0
  69. package/hooks/hooks.json +246 -0
  70. package/hooks/lifecycle/config-change-guard.py +84 -0
  71. package/hooks/lifecycle/session-end-cleanup.py +59 -0
  72. package/hooks/notification/attention-needed-notify.py +63 -0
  73. package/hooks/notification/claude-notification-handler.py +59 -0
  74. package/hooks/notification/notification_utils.py +206 -0
  75. package/hooks/rewrite-plugin-paths.py +116 -0
  76. package/hooks/session/bulk-edit-reminder.py +30 -0
  77. package/hooks/session/code-rules-reminder.py +97 -0
  78. package/hooks/session/compact-context-reinject.py +39 -0
  79. package/hooks/session/hook-structure-context.py +140 -0
  80. package/hooks/session/plugin-data-dir-cleanup.py +39 -0
  81. package/hooks/validation/code-style-validator.py +145 -0
  82. package/hooks/validation/e2e-test-validator.py +142 -0
  83. package/hooks/validation/hook-format-validator.py +66 -0
  84. package/hooks/validation/mypy_validator.py +180 -0
  85. package/hooks/validators/README.md +125 -0
  86. package/hooks/validators/VALIDATION_REPORT.md +287 -0
  87. package/hooks/validators/__init__.py +19 -0
  88. package/hooks/validators/abbreviation_checks.py +82 -0
  89. package/hooks/validators/code_quality_checks.py +133 -0
  90. package/hooks/validators/comment_checks.py +188 -0
  91. package/hooks/validators/file_structure_checks.py +182 -0
  92. package/hooks/validators/git_checks.py +107 -0
  93. package/hooks/validators/health_check.py +214 -0
  94. package/hooks/validators/magic_value_checks.py +81 -0
  95. package/hooks/validators/mypy_integration.py +52 -0
  96. package/hooks/validators/output_formatter.py +266 -0
  97. package/hooks/validators/pr_reference_checks.py +72 -0
  98. package/hooks/validators/python_antipattern_checks.py +110 -0
  99. package/hooks/validators/python_style_checks.py +364 -0
  100. package/hooks/validators/react_checks.py +90 -0
  101. package/hooks/validators/ruff_integration.py +80 -0
  102. package/hooks/validators/run_all_validators.py +772 -0
  103. package/hooks/validators/security_checks.py +135 -0
  104. package/hooks/validators/test_abbreviation_checks.py +76 -0
  105. package/hooks/validators/test_bad.tsx +7 -0
  106. package/hooks/validators/test_code_quality_checks.py +129 -0
  107. package/hooks/validators/test_file_structure_checks.py +307 -0
  108. package/hooks/validators/test_files/01_basic_component.tsx +10 -0
  109. package/hooks/validators/test_files/02_component_without_react.tsx +10 -0
  110. package/hooks/validators/test_files/03_pure_component.tsx +10 -0
  111. package/hooks/validators/test_files/04_pure_component_import.tsx +10 -0
  112. package/hooks/validators/test_files/05_typescript_generics.tsx +14 -0
  113. package/hooks/validators/test_files/06_typescript_two_generics.tsx +18 -0
  114. package/hooks/validators/test_files/07_multiline_declaration.tsx +11 -0
  115. package/hooks/validators/test_files/08_error_boundary_valid.tsx +14 -0
  116. package/hooks/validators/test_files/09_error_boundary_with_other_class.tsx +20 -0
  117. package/hooks/validators/test_files/10_inheritance_chain.tsx +16 -0
  118. package/hooks/validators/test_files/11_ts_file.ts +10 -0
  119. package/hooks/validators/test_files/12_non_react_class.tsx +14 -0
  120. package/hooks/validators/test_files/13_functional_component.tsx +8 -0
  121. package/hooks/validators/test_files/14_indented_class.tsx +13 -0
  122. package/hooks/validators/test_files/15_getDerivedStateFromError.tsx +14 -0
  123. package/hooks/validators/test_files/16_mixed_components.tsx +20 -0
  124. package/hooks/validators/test_files/EXECUTIVE_SUMMARY.md +175 -0
  125. package/hooks/validators/test_files/TEST_RESULTS_TABLE.txt +60 -0
  126. package/hooks/validators/test_files/VALIDATION_REPORT.md +201 -0
  127. package/hooks/validators/test_files/async_views.py +23 -0
  128. package/hooks/validators/test_files/async_with_imports.py +14 -0
  129. package/hooks/validators/test_files/bad_inline_imports.py +37 -0
  130. package/hooks/validators/test_files/management/commands/cmd_01_no_debug_check.py +10 -0
  131. package/hooks/validators/test_files/management/commands/cmd_02_proper_debug_check.py +14 -0
  132. package/hooks/validators/test_files/management/commands/cmd_03_debug_check_with_return.py +14 -0
  133. package/hooks/validators/test_files/management/commands/cmd_04_imported_DEBUG.py +14 -0
  134. package/hooks/validators/test_files/management/commands/cmd_05_debug_check_in_helper.py +16 -0
  135. package/hooks/validators/test_files/management/commands/cmd_06_debug_check_late.py +22 -0
  136. package/hooks/validators/test_files/management/commands/cmd_07_positive_debug_check.py +15 -0
  137. package/hooks/validators/test_files/management/commands/cmd_08_debug_with_and.py +14 -0
  138. package/hooks/validators/test_files/not_management_command.py +10 -0
  139. package/hooks/validators/test_files/skip_decorators/test_01_simple_skip.py +8 -0
  140. package/hooks/validators/test_files/skip_decorators/test_02_pytest_skipif.py +8 -0
  141. package/hooks/validators/test_files/skip_decorators/test_03_unittest_skipIf.py +8 -0
  142. package/hooks/validators/test_files/skip_decorators/test_04_skip_with_parens.py +8 -0
  143. package/hooks/validators/test_files/skip_decorators/test_05_xfail.py +7 -0
  144. package/hooks/validators/test_files/skip_decorators/test_06_custom_skip.py +11 -0
  145. package/hooks/validators/test_files/skip_decorators/test_07_capital_Skip.py +8 -0
  146. package/hooks/validators/test_files/skip_decorators/test_08_skipUnless.py +7 -0
  147. package/hooks/validators/test_files/skip_decorators/test_09_pytest_mark_skip_simple.py +7 -0
  148. package/hooks/validators/test_files/test_async_functions.py +45 -0
  149. package/hooks/validators/test_files/test_purecomponent/PureComponentExample.tsx +7 -0
  150. package/hooks/validators/test_files/test_purecomponent/ReactPureComponentExample.tsx +7 -0
  151. package/hooks/validators/test_git_checks.py +295 -0
  152. package/hooks/validators/test_good.tsx +5 -0
  153. package/hooks/validators/test_health_check.py +57 -0
  154. package/hooks/validators/test_magic_value_checks.py +63 -0
  155. package/hooks/validators/test_mypy_integration.py +27 -0
  156. package/hooks/validators/test_output_formatter.py +150 -0
  157. package/hooks/validators/test_pr_reference_checks.py +41 -0
  158. package/hooks/validators/test_python_antipattern_checks.py +113 -0
  159. package/hooks/validators/test_python_style_checks.py +439 -0
  160. package/hooks/validators/test_react_checks.py +213 -0
  161. package/hooks/validators/test_results.txt +25 -0
  162. package/hooks/validators/test_ruff_integration.py +27 -0
  163. package/hooks/validators/test_run_all_validators.py +228 -0
  164. package/hooks/validators/test_run_all_validators_integration.py +48 -0
  165. package/hooks/validators/test_safety_checks.py +243 -0
  166. package/hooks/validators/test_security_checks.py +105 -0
  167. package/hooks/validators/test_test_safety_checks.py +321 -0
  168. package/hooks/validators/test_todo_checks.py +39 -0
  169. package/hooks/validators/test_type_safety_checks.py +85 -0
  170. package/hooks/validators/test_useless_test_checks.py +55 -0
  171. package/hooks/validators/test_validator_base.py +26 -0
  172. package/hooks/validators/test_verify_paths.py +34 -0
  173. package/hooks/validators/todo_checks.py +59 -0
  174. package/hooks/validators/type_safety_checks.py +101 -0
  175. package/hooks/validators/useless_test_checks.py +92 -0
  176. package/hooks/validators/validator_base.py +19 -0
  177. package/hooks/validators/verify_paths.py +57 -0
  178. package/hooks/workflow/auto-formatter.py +114 -0
  179. package/hooks/workflow/investigation-tracker-reset.py +46 -0
  180. package/package.json +30 -0
  181. package/rules/agent-spawn-protocol.md +47 -0
  182. package/rules/cleanup-temp-files.md +27 -0
  183. package/rules/code-reviews.md +11 -0
  184. package/rules/code-standards.md +43 -0
  185. package/rules/conservative-action.md +20 -0
  186. package/rules/context7.md +12 -0
  187. package/rules/explore-thoroughly.md +27 -0
  188. package/rules/git-workflow.md +42 -0
  189. package/rules/parallel-tools.md +23 -0
  190. package/rules/research-mode.md +23 -0
  191. package/rules/right-sized-engineering.md +28 -0
  192. package/rules/tdd.md +7 -0
  193. package/rules/testing.md +12 -0
  194. package/skills/agent-prompt/SKILL.md +102 -0
  195. package/skills/anthropic-plan/SKILL.md +107 -0
  196. package/skills/everything-search/SKILL.md +144 -0
  197. package/skills/ingest/SKILL.md +40 -0
  198. package/skills/npm-creator/SKILL.md +183 -0
  199. package/skills/pr-review-responder/EXAMPLES.md +590 -0
  200. package/skills/pr-review-responder/PRINCIPLES.md +539 -0
  201. package/skills/pr-review-responder/README.md +209 -0
  202. package/skills/pr-review-responder/SKILL.md +202 -0
  203. package/skills/pr-review-responder/TESTING.md +407 -0
  204. package/skills/pr-review-responder/scripts/respond_to_reviews.py +376 -0
  205. package/skills/pr-review-responder/update_skill.py +297 -0
  206. package/skills/prompt-generator/REFERENCE.md +150 -0
  207. package/skills/prompt-generator/SKILL.md +154 -0
  208. package/skills/readability-review/SKILL.md +127 -0
  209. package/skills/recall/SKILL.md +27 -0
  210. package/skills/remember/SKILL.md +63 -0
  211. package/skills/rule-audit/SKILL.md +307 -0
  212. package/skills/rule-creator/SKILL.md +150 -0
  213. package/skills/skill-writer/REFERENCE.md +246 -0
  214. package/skills/skill-writer/SKILL.md +270 -0
  215. package/skills/tdd-team/SKILL.md +128 -0
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Code style validator - checks for common style issues.
4
+
5
+ - 4-space indentation (not tabs, not 2 spaces)
6
+ - Single newlines between functions (not double)
7
+ - Single newlines between class methods
8
+ """
9
+ import json
10
+ import re
11
+ import sys
12
+
13
+
14
+ def check_indentation(content: str) -> list[str]:
15
+ """Check for non-4-space indentation."""
16
+ issues = []
17
+ lines = content.split('\n')
18
+
19
+ for line_num, line in enumerate(lines, 1):
20
+ if not line or not line[0].isspace():
21
+ continue
22
+
23
+ # Check for tabs
24
+ if '\t' in line:
25
+ issues.append(f"Line {line_num}: Tab indentation - use 4 spaces")
26
+ continue
27
+
28
+ # Get leading spaces
29
+ stripped = line.lstrip(' ')
30
+ indent = len(line) - len(stripped)
31
+
32
+ # Check if indent is multiple of 4
33
+ if indent > 0 and indent % 4 != 0:
34
+ issues.append(f"Line {line_num}: {indent}-space indent - use 4 spaces")
35
+
36
+ return issues[:5] # Limit to first 5
37
+
38
+
39
+ def check_function_spacing(content: str) -> list[str]:
40
+ """Check for excessive blank lines between code blocks.
41
+
42
+ Detects 2+ consecutive blank lines anywhere in the file, plus validates
43
+ correct spacing before function/method/class definitions.
44
+ """
45
+ issues = []
46
+ lines = content.split('\n')
47
+
48
+ func_pattern = re.compile(r'^(\s*)(async\s+)?def\s+\w+')
49
+ class_pattern = re.compile(r'^class\s+\w+')
50
+
51
+ consecutive_blank_count = 0
52
+ blank_run_start_line = 0
53
+ prev_was_code = False
54
+
55
+ for line_num, line in enumerate(lines, 1):
56
+ stripped = line.strip()
57
+
58
+ if not stripped:
59
+ if consecutive_blank_count == 0:
60
+ blank_run_start_line = line_num
61
+ consecutive_blank_count += 1
62
+ continue
63
+
64
+ if consecutive_blank_count >= 3:
65
+ issues.append(f"Line {blank_run_start_line}: {consecutive_blank_count} consecutive blank lines - max 2 allowed")
66
+
67
+ func_match = func_pattern.match(line)
68
+ class_match = class_pattern.match(line)
69
+
70
+ if func_match and prev_was_code:
71
+ indent = len(func_match.group(1)) if func_match.group(1) else 0
72
+
73
+ if indent == 0:
74
+ if consecutive_blank_count != 2:
75
+ issues.append(f"Line {line_num}: Top-level function needs 2 blank lines above (has {consecutive_blank_count})")
76
+ else:
77
+ if consecutive_blank_count != 1:
78
+ issues.append(f"Line {line_num}: Method needs 1 blank line above (has {consecutive_blank_count})")
79
+
80
+ elif class_match and prev_was_code:
81
+ if consecutive_blank_count != 2:
82
+ issues.append(f"Line {line_num}: Class needs 2 blank lines above (has {consecutive_blank_count})")
83
+
84
+ consecutive_blank_count = 0
85
+ prev_was_code = not stripped.startswith('#') and not stripped.startswith('@')
86
+
87
+ return issues[:5]
88
+
89
+
90
+ def main() -> None:
91
+ try:
92
+ input_data = json.load(sys.stdin)
93
+ except json.JSONDecodeError:
94
+ sys.exit(0)
95
+
96
+ tool_input = input_data.get("tool_input", {})
97
+ file_path = tool_input.get("file_path", "")
98
+
99
+ if not file_path:
100
+ sys.exit(0)
101
+
102
+ # Only check Python files
103
+ if not file_path.endswith('.py'):
104
+ sys.exit(0)
105
+
106
+ # Skip test files (more lenient)
107
+ if 'test' in file_path.lower() or 'conftest' in file_path.lower():
108
+ sys.exit(0)
109
+
110
+ tool_name = input_data.get("tool_name", "")
111
+ content = tool_input.get("content", "") or tool_input.get("new_string", "")
112
+
113
+ if not content:
114
+ sys.exit(0)
115
+
116
+ if tool_name == "Write":
117
+ try:
118
+ with open(file_path, "r", encoding="utf-8") as existing_file:
119
+ existing_content = existing_file.read()
120
+ if existing_content:
121
+ sys.exit(0)
122
+ except (FileNotFoundError, OSError, UnicodeDecodeError):
123
+ pass
124
+
125
+ issues = []
126
+ issues.extend(check_indentation(content))
127
+ issues.extend(check_function_spacing(content))
128
+
129
+ if issues:
130
+ issue_list = "; ".join(issues)
131
+ result = {
132
+ "hookSpecificOutput": {
133
+ "hookEventName": "PreToolUse",
134
+ "permissionDecision": "ask",
135
+ "permissionDecisionReason": f"[Code Style] {len(issues)} issue(s): {issue_list}. Fix or proceed?"
136
+ }
137
+ }
138
+ print(json.dumps(result))
139
+ sys.stdout.flush()
140
+
141
+ sys.exit(0)
142
+
143
+
144
+ if __name__ == "__main__":
145
+ main()
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Validate E2E test consistency between online/offline specs.
4
+
5
+ Two checks:
6
+ 1. Naming: offline tests must mirror online test names with " offline" suffix.
7
+ 2. Coverage: when a new online e2e test file is written, a corresponding
8
+ offline equivalent must exist. Blocks if missing.
9
+
10
+ Triggered as PostToolUse hook when editing spec files.
11
+ """
12
+
13
+ import json
14
+ import os
15
+ import re
16
+ import sys
17
+ from pathlib import Path
18
+
19
+
20
+ E2E_TEST_DIRECTORY = "frontend/tests/e2e"
21
+
22
+
23
+ def extract_test_names(file_path: Path) -> set[str]:
24
+ """Extract test names from spec file."""
25
+ content = file_path.read_text()
26
+ pattern = r"test\(['\"]([^'\"]+)['\"]"
27
+ return set(re.findall(pattern, content))
28
+
29
+
30
+ def validate_e2e_naming(project_root: Path) -> list[str]:
31
+ """Return list of naming violations.
32
+
33
+ Only validates tests that follow the naming convention (end with " offline").
34
+ Legacy tests without the suffix are ignored - they may intentionally differ.
35
+ """
36
+ online = project_root / E2E_TEST_DIRECTORY / "online.spec.ts"
37
+ offline = project_root / E2E_TEST_DIRECTORY / "offline.spec.ts"
38
+
39
+ if not online.exists() or not offline.exists():
40
+ return []
41
+
42
+ online_tests = extract_test_names(online)
43
+ offline_tests = extract_test_names(offline)
44
+
45
+ violations = []
46
+
47
+ for test in offline_tests:
48
+ if not test.endswith(" offline"):
49
+ continue
50
+
51
+ online_name = test.removesuffix(" offline")
52
+ if online_name not in online_tests:
53
+ violations.append(f"No online pair for: '{test}'")
54
+
55
+ return violations
56
+
57
+
58
+ def validate_offline_coverage(file_path: str, project_root: Path) -> list[str]:
59
+ """Check that online e2e test files have a corresponding offline file.
60
+
61
+ When a new online spec file is written, the offline equivalent must exist.
62
+ Returns blocking violations if offline file is missing.
63
+ """
64
+ e2e_directory = project_root / E2E_TEST_DIRECTORY
65
+ file_name = Path(file_path).name
66
+
67
+ if "offline" in file_name:
68
+ return []
69
+
70
+ if not file_name.endswith(".spec.ts"):
71
+ return []
72
+
73
+ offline_name = file_name.replace(".spec.ts", ".offline.spec.ts")
74
+ if file_name == "online.spec.ts":
75
+ offline_name = "offline.spec.ts"
76
+
77
+ offline_path = e2e_directory / offline_name
78
+ if not offline_path.exists():
79
+ return [f"Missing offline equivalent: {offline_name} required for {file_name}"]
80
+
81
+ return []
82
+
83
+
84
+ def main() -> None:
85
+ """Hook entry point - reads tool input from stdin."""
86
+ try:
87
+ input_data = json.load(sys.stdin)
88
+ except json.JSONDecodeError:
89
+ sys.exit(0)
90
+
91
+ tool_input = input_data.get("tool_input", {})
92
+ file_path = tool_input.get("file_path", "")
93
+
94
+ if not file_path:
95
+ sys.exit(0)
96
+
97
+ if ".spec.ts" not in file_path:
98
+ sys.exit(0)
99
+
100
+ path_object = Path(file_path)
101
+ project_root = path_object.parent
102
+ while project_root != project_root.parent:
103
+ if (project_root / E2E_TEST_DIRECTORY).exists():
104
+ break
105
+ project_root = project_root.parent
106
+ else:
107
+ sys.exit(0)
108
+
109
+ if not (project_root / E2E_TEST_DIRECTORY).exists():
110
+ sys.exit(0)
111
+
112
+ naming_violations = validate_e2e_naming(project_root)
113
+ coverage_violations = validate_offline_coverage(file_path, project_root)
114
+
115
+ if coverage_violations:
116
+ violation_list = "; ".join(coverage_violations)
117
+ result = {
118
+ "hookSpecificOutput": {
119
+ "hookEventName": "PostToolUse",
120
+ "additionalContext": f"[E2E COVERAGE] Offline test required: {violation_list}"
121
+ }
122
+ }
123
+ print(json.dumps(result))
124
+ sys.stdout.flush()
125
+ sys.exit(0)
126
+
127
+ if naming_violations:
128
+ violation_list = "; ".join(naming_violations)
129
+ result = {
130
+ "hookSpecificOutput": {
131
+ "hookEventName": "PostToolUse",
132
+ "additionalContext": f"[E2E NAMING] {violation_list}. Offline tests must mirror online names with ' offline' suffix."
133
+ }
134
+ }
135
+ print(json.dumps(result))
136
+ sys.stdout.flush()
137
+
138
+ sys.exit(0)
139
+
140
+
141
+ if __name__ == "__main__":
142
+ main()
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PostToolUse hook that validates hook commands in settings.json use cross-platform format.
4
+ Blocks if hooks use simple 'python3 ~/.claude/...' instead of the exec(open(...)) pattern.
5
+ """
6
+
7
+ import json
8
+ import re
9
+ import sys
10
+
11
+
12
+ SIMPLE_PATTERN = re.compile(
13
+ r'python3?\s+~/\.claude/hooks/'
14
+ )
15
+
16
+
17
+ def main() -> None:
18
+ try:
19
+ hook_input = json.load(sys.stdin)
20
+ except json.JSONDecodeError:
21
+ sys.exit(0)
22
+
23
+ tool_input = hook_input.get("tool_input", {})
24
+ file_path = tool_input.get("file_path", "")
25
+
26
+ if not file_path.endswith("settings.json"):
27
+ sys.exit(0)
28
+
29
+ if "/.claude/" not in file_path and "\\.claude\\" not in file_path:
30
+ sys.exit(0)
31
+
32
+ tool_name = hook_input.get("tool_name", "")
33
+ content = tool_input.get("content", "")
34
+ if not content:
35
+ new_string = tool_input.get("new_string", "")
36
+ content = new_string
37
+
38
+ if tool_name == "Write" and content:
39
+ try:
40
+ with open(file_path, "r", encoding="utf-8") as existing_file:
41
+ existing_content = existing_file.read()
42
+ if existing_content:
43
+ sys.exit(0)
44
+ except (FileNotFoundError, OSError, UnicodeDecodeError):
45
+ pass
46
+
47
+ if not content:
48
+ sys.exit(0)
49
+
50
+ if SIMPLE_PATTERN.search(content):
51
+ message = "BLOCKED: Hook uses python3 ~/.claude/hooks/... format which breaks cross-platform. Use this pattern: node -e \"process.argv.splice(1,0,'_');require(require('os').homedir()+'/.claude/hooks/run-hook-wrapper.js')\" \"subfolder/your-hook.py\""
52
+ result = {
53
+ "hookSpecificOutput": {
54
+ "hookEventName": "PreToolUse",
55
+ "permissionDecision": "deny",
56
+ "permissionDecisionReason": message
57
+ }
58
+ }
59
+ print(json.dumps(result))
60
+ sys.exit(0)
61
+
62
+ sys.exit(0)
63
+
64
+
65
+ if __name__ == "__main__":
66
+ main()
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Mypy validation hook - blocks Write/Edit if mypy finds type errors.
4
+
5
+ This catches:
6
+ - Missing attributes (e.g., HumanActions has no attribute 'press_key')
7
+ - Wrong function signatures
8
+ - Type mismatches
9
+ - Import errors
10
+
11
+ Works in both WSL and Windows for any Python project with a git root.
12
+ Project root is discovered via CLAUDE_PROJECT_ROOT env var or git rev-parse.
13
+ """
14
+ import json
15
+ import os
16
+ import platform
17
+ import subprocess
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ IS_WINDOWS = platform.system() == "Windows"
22
+
23
+ GIT_COMMAND_TIMEOUT_SECONDS = 5
24
+ MYPY_TIMEOUT_SECONDS = 60
25
+ MAXIMUM_DISPLAYED_ERRORS = 5
26
+
27
+ SKIP_PATTERNS = {"test_", "_test.", "conftest", "/tests/", "\\tests\\", "fixture", "mock"}
28
+
29
+
30
+ def discover_project_root(target_file: str) -> Path | None:
31
+ if env_root := os.environ.get("CLAUDE_PROJECT_ROOT"):
32
+ return Path(env_root)
33
+
34
+ try:
35
+ completed_process = subprocess.run(
36
+ ["git", "rev-parse", "--show-toplevel"],
37
+ capture_output=True,
38
+ text=True,
39
+ timeout=GIT_COMMAND_TIMEOUT_SECONDS,
40
+ cwd=str(Path(target_file).parent),
41
+ )
42
+ if completed_process.returncode != 0:
43
+ return None
44
+ return Path(completed_process.stdout.strip())
45
+ except (subprocess.TimeoutExpired, FileNotFoundError):
46
+ return None
47
+
48
+
49
+ def is_file_within_project(target_file: str, project_root: Path) -> bool:
50
+ try:
51
+ Path(target_file).resolve().relative_to(project_root.resolve())
52
+ return True
53
+ except ValueError:
54
+ return False
55
+
56
+
57
+ def build_mypy_command(relative_file_path: str) -> list[str]:
58
+ if IS_WINDOWS:
59
+ base_command = [sys.executable, "-m", "mypy"]
60
+ else:
61
+ base_command = ["mypy"]
62
+
63
+ return base_command + [
64
+ "--no-error-summary",
65
+ "--show-error-codes",
66
+ "--no-color",
67
+ relative_file_path,
68
+ ]
69
+
70
+
71
+ def run_mypy(target_file: str, project_root: str) -> tuple[int, str]:
72
+ relative_file_path = os.path.relpath(target_file, project_root)
73
+ mypy_command = build_mypy_command(relative_file_path)
74
+
75
+ completed_process = subprocess.run(
76
+ mypy_command,
77
+ capture_output=True,
78
+ text=True,
79
+ env=os.environ.copy(),
80
+ timeout=MYPY_TIMEOUT_SECONDS,
81
+ cwd=project_root,
82
+ )
83
+
84
+ stdout_output = completed_process.stdout.strip()
85
+ stderr_output = completed_process.stderr.strip()
86
+ combined_output = f"{stdout_output}\n{stderr_output}".strip() if stderr_output else stdout_output
87
+
88
+ return completed_process.returncode, combined_output
89
+
90
+
91
+ def extract_error_lines(mypy_output: str) -> list[str]:
92
+ all_lines = mypy_output.strip().split("\n")
93
+ return [each_line for each_line in all_lines if ": error:" in each_line]
94
+
95
+
96
+ def format_error_summary(all_error_lines: list[str]) -> str:
97
+ displayed_errors = all_error_lines[:MAXIMUM_DISPLAYED_ERRORS]
98
+ error_summary = "\n".join(f" {each_line}" for each_line in displayed_errors)
99
+
100
+ remaining_error_count = len(all_error_lines) - MAXIMUM_DISPLAYED_ERRORS
101
+ if remaining_error_count > 0:
102
+ error_summary += f"\n ... and {remaining_error_count} more"
103
+
104
+ return error_summary
105
+
106
+
107
+ def build_block_response(error_summary: str) -> dict[str, str | dict[str, str]]:
108
+ return {
109
+ "decision": "block",
110
+ "reason": f"[MYPY] Type errors: {error_summary}",
111
+ "hookSpecificOutput": {
112
+ "hookEventName": "PostToolUse",
113
+ },
114
+ }
115
+
116
+
117
+ def parse_file_path_from_stdin() -> str:
118
+ try:
119
+ hook_event = json.load(sys.stdin)
120
+ except json.JSONDecodeError:
121
+ return ""
122
+
123
+ return hook_event.get("tool_input", {}).get("file_path", "")
124
+
125
+
126
+ def is_test_file(python_file: Path) -> bool:
127
+ name_lower = python_file.name.lower()
128
+ path_lower = str(python_file).lower()
129
+
130
+ return any(
131
+ each_pattern in name_lower or each_pattern in path_lower
132
+ for each_pattern in SKIP_PATTERNS
133
+ )
134
+
135
+
136
+ def main() -> None:
137
+ target_file_path = parse_file_path_from_stdin()
138
+
139
+ if not target_file_path:
140
+ sys.exit(0)
141
+
142
+ target_file = Path(target_file_path)
143
+
144
+ if target_file.suffix.lower() != ".py":
145
+ sys.exit(0)
146
+
147
+ if is_test_file(target_file):
148
+ sys.exit(0)
149
+
150
+ if not target_file.exists():
151
+ sys.exit(0)
152
+
153
+ project_root = discover_project_root(target_file_path)
154
+ if project_root is None:
155
+ sys.exit(0)
156
+
157
+ if not is_file_within_project(target_file_path, project_root):
158
+ sys.exit(0)
159
+
160
+ try:
161
+ mypy_exit_code, mypy_output = run_mypy(target_file_path, str(project_root))
162
+ except (subprocess.TimeoutExpired, FileNotFoundError):
163
+ sys.exit(0)
164
+
165
+ if mypy_exit_code == 0:
166
+ sys.exit(0)
167
+
168
+ all_error_lines = extract_error_lines(mypy_output)
169
+
170
+ if not all_error_lines:
171
+ sys.exit(0)
172
+
173
+ error_summary = format_error_summary(all_error_lines)
174
+ block_response = build_block_response(error_summary)
175
+ print(json.dumps(block_response))
176
+ sys.exit(0)
177
+
178
+
179
+ if __name__ == "__main__":
180
+ main()
@@ -0,0 +1,125 @@
1
+ # Python Style Validators
2
+
3
+ AST-based Python style checks for code quality enforcement.
4
+
5
+ ## Checks Implemented
6
+
7
+ 1. **Imports at top** - All import statements must be at the top of the file
8
+ 2. **No empty line after decorators** - Decorators must be directly above functions (no blank lines)
9
+ 3. **Single empty line between functions** - Exactly one blank line between top-level functions
10
+ 4. **View function naming** - Functions in `views.py` with `request` parameter must end with `_view`
11
+
12
+ ## Usage
13
+
14
+ ### Command Line
15
+
16
+ ```bash
17
+ python python_style_checks.py file1.py file2.py ...
18
+ ```
19
+
20
+ Exit codes:
21
+ - `0` - All files pass
22
+ - `1` - Violations found or error
23
+
24
+ ### Python API
25
+
26
+ ```python
27
+ from python_style_checks import validate_file, Violation
28
+ from pathlib import Path
29
+
30
+ violations = validate_file(Path("myfile.py"))
31
+ for v in violations:
32
+ print(v) # Prints: file:line: message
33
+ ```
34
+
35
+ ### Individual Checks
36
+
37
+ ```python
38
+ import ast
39
+ from python_style_checks import (
40
+ check_imports_at_top,
41
+ check_no_empty_line_after_decorators,
42
+ check_single_empty_line_between_functions,
43
+ check_view_function_naming,
44
+ )
45
+
46
+ source = Path("myfile.py").read_text()
47
+ tree = ast.parse(source)
48
+
49
+ # Run individual checks
50
+ violations = check_imports_at_top(tree, "myfile.py")
51
+ violations = check_no_empty_line_after_decorators(source, "myfile.py")
52
+ violations = check_single_empty_line_between_functions(source, "myfile.py")
53
+ violations = check_view_function_naming(tree, "views.py")
54
+ ```
55
+
56
+ ## Testing
57
+
58
+ ```bash
59
+ pytest test_python_style_checks.py -v
60
+ ```
61
+
62
+ ## Examples
63
+
64
+ ### Valid Code
65
+
66
+ ```python
67
+ """Module docstring."""
68
+
69
+ import os
70
+ import sys
71
+ from typing import List
72
+
73
+
74
+ def foo() -> None:
75
+ """Do something."""
76
+ pass
77
+
78
+ @decorator
79
+ def bar() -> None:
80
+ """Another function."""
81
+ pass
82
+ ```
83
+
84
+ ### Invalid Code
85
+
86
+ ```python
87
+ # Import not at top
88
+ def foo() -> None:
89
+ pass
90
+
91
+ import os # VIOLATION: Import must be at top
92
+
93
+ # Empty line after decorator
94
+ @decorator
95
+
96
+ def bar() -> None: # VIOLATION: No empty line after decorator
97
+ pass
98
+
99
+ # Wrong spacing between functions
100
+ def baz() -> None:
101
+ pass
102
+
103
+
104
+ def qux() -> None: # VIOLATION: Expected 1 empty line, found 2
105
+ pass
106
+
107
+ # View naming (in views.py)
108
+ def user_profile(request): # VIOLATION: Must end with _view
109
+ pass
110
+ ```
111
+
112
+ ## Integration with Pre-Commit Hooks
113
+
114
+ Example `.pre-commit-config.yaml`:
115
+
116
+ ```yaml
117
+ repos:
118
+ - repo: local
119
+ hooks:
120
+ - id: python-style-checks
121
+ name: Python Style Checks
122
+ entry: python hooks/validators/python_style_checks.py
123
+ language: system
124
+ types: [python]
125
+ ```