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,343 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PreToolUse:Bash hook that validates prerequisites before running test commands.
4
+
5
+ Intercepts playwright/pytest commands and checks:
6
+ 1. Target server is reachable and healthy
7
+ 2. Database file exists (for Django projects)
8
+ 3. (Playwright only) Django server has --test-db flag
9
+ 4. (Playwright only) Frontend builds successfully before e2e tests
10
+
11
+ Blocks doomed test runs early instead of letting them hang for minutes.
12
+ """
13
+ import json
14
+ import os
15
+ import re
16
+ import subprocess
17
+ import sys
18
+ from urllib.parse import urlparse
19
+
20
+ try:
21
+ import psutil
22
+ except Exception:
23
+ psutil = None
24
+
25
+ TEST_COMMAND_PATTERNS = [
26
+ re.compile(r'\bplaywright\s+test\b'),
27
+ re.compile(r'\bnpx\s+playwright\b'),
28
+ re.compile(r'\bpytest\b'),
29
+ re.compile(r'\bpython\s+-m\s+pytest\b'),
30
+ ]
31
+
32
+ SERVER_URL_PATTERN = re.compile(r'https?://[^\s"\']+')
33
+
34
+ DEFAULT_PLAYWRIGHT_URL = "http://localhost:3000"
35
+ DEFAULT_DJANGO_URL = "http://localhost:8000"
36
+
37
+ CURL_TIMEOUT_SECONDS = 2
38
+ DJANGO_DB_FILENAME = "db.sqlite3"
39
+
40
+ BLOCKED_STATUS_CODES = {500, 502, 503, 504}
41
+ HEALTH_CHECK_ERROR_TEMPLATE = "BLOCKED: Server at {} is not healthy ({}). Fix the server before running tests."
42
+ UNREACHABLE_ERROR_TEMPLATE = "BLOCKED: Server at {} is unreachable. Start the server before running tests."
43
+ MISSING_DB_ERROR_TEMPLATE = "BLOCKED: No database file ({}) found in {}. Run migrations before running tests."
44
+ FRONTEND_BUILD_FAILED_MESSAGE = "BLOCKED: Frontend build failed. Fix build errors before running e2e tests."
45
+ MISSING_TEST_DB_FLAG_TEMPLATE = "BLOCKED: Django server on port {} is not running with --test-db. Restart with: python manage.py runserver --test-db 0.0.0.0:{}"
46
+ PORT_CONFLICT_ERROR_TEMPLATE = "BLOCKED: Multiple Django runserver processes are bound to port {} across worktrees: {}. Stop stale servers first."
47
+ FRONTEND_DIRECTORY_NAME = "frontend"
48
+ NPM_BUILD_COMMAND = "npm run build"
49
+ COLLECTSTATIC_COMMAND = "python manage.py collectstatic --noinput"
50
+ BUILD_TIMEOUT_SECONDS = 120
51
+ PLAYWRIGHT_COMMAND_PATTERNS = [
52
+ re.compile(r'\bplaywright\s+test\b'),
53
+ re.compile(r'\bnpx\s+playwright\b'),
54
+ ]
55
+
56
+
57
+ def is_test_command(command: str) -> bool:
58
+ for each_pattern in TEST_COMMAND_PATTERNS:
59
+ if each_pattern.search(command):
60
+ return True
61
+ return False
62
+
63
+
64
+ def is_playwright_command(command: str) -> bool:
65
+ for each_pattern in PLAYWRIGHT_COMMAND_PATTERNS:
66
+ if each_pattern.search(command):
67
+ return True
68
+ return False
69
+
70
+
71
+ def extract_target_url(command: str) -> str:
72
+ url_match = SERVER_URL_PATTERN.search(command)
73
+ if url_match:
74
+ return url_match.group(0)
75
+
76
+ is_playwright = "playwright" in command
77
+ if is_playwright:
78
+ return DEFAULT_PLAYWRIGHT_URL
79
+
80
+ is_pytest = "pytest" in command
81
+ if is_pytest:
82
+ return DEFAULT_DJANGO_URL
83
+
84
+ return DEFAULT_PLAYWRIGHT_URL
85
+
86
+
87
+ def check_server_health(target_url: str) -> str | None:
88
+ try:
89
+ curl_result = subprocess.run(
90
+ ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", str(CURL_TIMEOUT_SECONDS), target_url],
91
+ capture_output=True,
92
+ text=True,
93
+ timeout=CURL_TIMEOUT_SECONDS + 1,
94
+ )
95
+ http_status_code = int(curl_result.stdout.strip())
96
+ except (subprocess.TimeoutExpired, ValueError, OSError):
97
+ return UNREACHABLE_ERROR_TEMPLATE.format(target_url)
98
+
99
+ if http_status_code == 0:
100
+ return UNREACHABLE_ERROR_TEMPLATE.format(target_url)
101
+
102
+ if http_status_code in BLOCKED_STATUS_CODES:
103
+ return HEALTH_CHECK_ERROR_TEMPLATE.format(target_url, f"HTTP {http_status_code}")
104
+
105
+ return None
106
+
107
+
108
+ def find_project_root(command: str) -> str | None:
109
+ working_directory = os.environ.get("PWD", os.getcwd())
110
+
111
+ directory_match = re.search(r'--project[= ](\S+)', command)
112
+ if directory_match:
113
+ return os.path.abspath(directory_match.group(1))
114
+
115
+ cd_match = re.search(r'cd\s+"([^"]+)"', command) or re.search(r"cd\s+'([^']+)'", command) or re.search(r'cd\s+(\S+)', command)
116
+ if cd_match:
117
+ cd_target = cd_match.group(1)
118
+ if os.path.isabs(cd_target):
119
+ return cd_target
120
+ return os.path.join(working_directory, cd_target)
121
+
122
+ return working_directory
123
+
124
+
125
+ def check_django_database(command: str) -> str | None:
126
+ is_django_test = "pytest" in command or "manage.py" in command
127
+ if not is_django_test:
128
+ return None
129
+
130
+ project_root = find_project_root(command)
131
+ if not project_root:
132
+ return None
133
+
134
+ manage_py_path = os.path.join(project_root, "manage.py")
135
+ if not os.path.exists(manage_py_path):
136
+ return None
137
+
138
+ database_path = os.path.join(project_root, DJANGO_DB_FILENAME)
139
+ if os.path.exists(database_path):
140
+ return None
141
+
142
+ return MISSING_DB_ERROR_TEMPLATE.format(DJANGO_DB_FILENAME, project_root)
143
+
144
+
145
+ def find_frontend_directory(command: str) -> str | None:
146
+ project_root = find_project_root(command)
147
+ if not project_root:
148
+ return None
149
+
150
+ frontend_path = os.path.join(project_root, FRONTEND_DIRECTORY_NAME)
151
+ if os.path.isdir(frontend_path):
152
+ return frontend_path
153
+
154
+ return None
155
+
156
+
157
+ def build_frontend(command: str) -> str | None:
158
+ frontend_path = find_frontend_directory(command)
159
+ if not frontend_path:
160
+ return None
161
+
162
+ project_root = find_project_root(command)
163
+
164
+ npm_build_result = subprocess.run(
165
+ NPM_BUILD_COMMAND.split(),
166
+ cwd=frontend_path,
167
+ capture_output=True,
168
+ text=True,
169
+ timeout=BUILD_TIMEOUT_SECONDS,
170
+ )
171
+ if npm_build_result.returncode != 0:
172
+ return FRONTEND_BUILD_FAILED_MESSAGE
173
+
174
+ collectstatic_result = subprocess.run(
175
+ COLLECTSTATIC_COMMAND.split(),
176
+ cwd=project_root,
177
+ capture_output=True,
178
+ text=True,
179
+ timeout=BUILD_TIMEOUT_SECONDS,
180
+ )
181
+ if collectstatic_result.returncode != 0:
182
+ return FRONTEND_BUILD_FAILED_MESSAGE
183
+
184
+ return None
185
+
186
+
187
+ def extract_port_from_url(target_url: str) -> str:
188
+ parsed_url = urlparse(target_url)
189
+ if parsed_url.port:
190
+ return str(parsed_url.port)
191
+ return "8000"
192
+
193
+
194
+ def check_test_db_flag(target_url: str) -> str | None:
195
+ port = extract_port_from_url(target_url)
196
+
197
+ try:
198
+ ps_result = subprocess.run(
199
+ ["ps", "aux"],
200
+ capture_output=True,
201
+ text=True,
202
+ timeout=CURL_TIMEOUT_SECONDS,
203
+ )
204
+ except (subprocess.TimeoutExpired, OSError):
205
+ return None
206
+
207
+ is_runserver_found = False
208
+ for each_line in ps_result.stdout.splitlines():
209
+ if "runserver" not in each_line:
210
+ continue
211
+ if "grep" in each_line:
212
+ continue
213
+ is_runserver_found = True
214
+ if "--test-db" in each_line:
215
+ return None
216
+
217
+ if not is_runserver_found:
218
+ return None
219
+
220
+ return MISSING_TEST_DB_FLAG_TEMPLATE.format(port, port)
221
+
222
+
223
+ def _get_runserver_processes_on_port(target_port: str) -> list[tuple[int, str]]:
224
+ if psutil is None:
225
+ return []
226
+
227
+ runserver_processes: list[tuple[int, str]] = []
228
+ port_token = f":{target_port}"
229
+
230
+ for each_process in psutil.process_iter(["pid", "cmdline", "cwd"]):
231
+ try:
232
+ commandline_parts = each_process.info.get("cmdline") or []
233
+ if len(commandline_parts) < 3:
234
+ continue
235
+ if commandline_parts[1] != "manage.py" or commandline_parts[2] != "runserver":
236
+ continue
237
+
238
+ full_commandline = " ".join(commandline_parts)
239
+ if port_token not in full_commandline:
240
+ continue
241
+
242
+ process_working_directory = each_process.info.get("cwd") or ""
243
+ runserver_processes.append((each_process.info["pid"], process_working_directory))
244
+ except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess, KeyError):
245
+ continue
246
+
247
+ return runserver_processes
248
+
249
+
250
+ def check_runserver_port_conflicts(target_url: str, project_root: str | None) -> str | None:
251
+ parsed_target_url = urlparse(target_url)
252
+ target_host = parsed_target_url.hostname or ""
253
+ if target_host not in {"localhost", "127.0.0.1", "0.0.0.0"}:
254
+ return None
255
+
256
+ target_port = str(parsed_target_url.port or 8000)
257
+ runserver_processes = _get_runserver_processes_on_port(target_port)
258
+ if len(runserver_processes) <= 1:
259
+ return None
260
+
261
+ project_root_realpath = os.path.realpath(project_root) if project_root else None
262
+ unique_directories: set[str] = set()
263
+ for _, directory_path in runserver_processes:
264
+ if not directory_path:
265
+ continue
266
+ unique_directories.add(os.path.realpath(directory_path))
267
+
268
+ if len(unique_directories) <= 1:
269
+ return None
270
+
271
+ if project_root_realpath and project_root_realpath in unique_directories:
272
+ other_worktrees = sorted(path for path in unique_directories if path != project_root_realpath)
273
+ if not other_worktrees:
274
+ return None
275
+ return PORT_CONFLICT_ERROR_TEMPLATE.format(target_port, ", ".join(other_worktrees))
276
+
277
+ return PORT_CONFLICT_ERROR_TEMPLATE.format(target_port, ", ".join(sorted(unique_directories)))
278
+
279
+
280
+ def build_deny_response(reason: str) -> dict:
281
+ return {
282
+ "hookSpecificOutput": {
283
+ "hookEventName": "PreToolUse",
284
+ "permissionDecision": "deny",
285
+ "permissionDecisionReason": reason,
286
+ }
287
+ }
288
+
289
+
290
+ def main() -> None:
291
+ try:
292
+ hook_input = json.load(sys.stdin)
293
+ except json.JSONDecodeError:
294
+ sys.exit(0)
295
+
296
+ tool_name = hook_input.get("tool_name", "")
297
+ if tool_name != "Bash":
298
+ sys.exit(0)
299
+
300
+ command = hook_input.get("tool_input", {}).get("command", "")
301
+ if not is_test_command(command):
302
+ sys.exit(0)
303
+
304
+ project_root = find_project_root(command)
305
+ is_django_project = project_root and os.path.exists(os.path.join(project_root, "manage.py"))
306
+
307
+ if not is_django_project and not is_playwright_command(command):
308
+ sys.exit(0)
309
+
310
+ database_error = check_django_database(command)
311
+ if database_error:
312
+ print(json.dumps(build_deny_response(database_error)))
313
+ sys.exit(0)
314
+
315
+ target_url = extract_target_url(command)
316
+
317
+ is_e2e_test = is_playwright_command(command)
318
+ if is_e2e_test:
319
+ conflict_error = check_runserver_port_conflicts(target_url, project_root)
320
+ if conflict_error:
321
+ print(json.dumps(build_deny_response(conflict_error)))
322
+ sys.exit(0)
323
+
324
+ test_db_error = check_test_db_flag(target_url)
325
+ if test_db_error:
326
+ print(json.dumps(build_deny_response(test_db_error)))
327
+ sys.exit(0)
328
+
329
+ frontend_build_error = build_frontend(command)
330
+ if frontend_build_error:
331
+ print(json.dumps(build_deny_response(frontend_build_error)))
332
+ sys.exit(0)
333
+
334
+ server_error = check_server_health(target_url)
335
+ if server_error:
336
+ print(json.dumps(build_deny_response(server_error)))
337
+ sys.exit(0)
338
+
339
+ sys.exit(0)
340
+
341
+
342
+ if __name__ == "__main__":
343
+ main()
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env python3
2
+ """PreToolUse:Write hook — blocks Write tool when the target file already exists.
3
+
4
+ Agents should use Edit for modifying existing files. Write is only for new file creation.
5
+ Exemptions: Jupyter notebooks (.ipynb) and files in ~/.claude/hooks/ (standalone scripts).
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import sys
11
+
12
+ JUPYTER_EXTENSION = ".ipynb"
13
+ HOOKS_DIRECTORY = os.path.normpath(os.path.expanduser("~/.claude/hooks"))
14
+
15
+
16
+ def is_jupyter_notebook(file_path: str) -> bool:
17
+ return file_path.lower().endswith(JUPYTER_EXTENSION)
18
+
19
+
20
+ def is_inside_hooks_directory(file_path: str) -> bool:
21
+ normalized_path = os.path.normpath(file_path)
22
+ return normalized_path.startswith(HOOKS_DIRECTORY)
23
+
24
+
25
+ def main() -> None:
26
+ try:
27
+ input_payload = json.load(sys.stdin)
28
+ except json.JSONDecodeError:
29
+ sys.exit(0)
30
+
31
+ tool_name = input_payload.get("tool_name", "")
32
+ tool_input = input_payload.get("tool_input", {})
33
+
34
+ if tool_name != "Write":
35
+ sys.exit(0)
36
+
37
+ target_file_path = tool_input.get("file_path", "")
38
+
39
+ if not target_file_path:
40
+ sys.exit(0)
41
+
42
+ if is_jupyter_notebook(target_file_path):
43
+ sys.exit(0)
44
+
45
+ if is_inside_hooks_directory(target_file_path):
46
+ sys.exit(0)
47
+
48
+ if not os.path.exists(target_file_path):
49
+ sys.exit(0)
50
+
51
+ denial = {
52
+ "hookSpecificOutput": {
53
+ "hookEventName": "PreToolUse",
54
+ "permissionDecision": "deny",
55
+ "permissionDecisionReason": f"BLOCKED: Write on existing file {target_file_path}. Use Edit tool instead.",
56
+ }
57
+ }
58
+ print(json.dumps(denial))
59
+ sys.exit(0)
60
+
61
+
62
+ if __name__ == "__main__":
63
+ main()
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Git post-commit hook: Auto-update parent repos when committing in a submodule.
4
+
5
+ When you commit in a submodule, this hook:
6
+ 1. Detects if current repo is a submodule of a parent
7
+ 2. Stages the submodule update in the parent
8
+ 3. Creates a commit in the parent pointing to the new submodule commit
9
+
10
+ This prevents the "lost work" issue where submodule commits aren't tracked by parent.
11
+ """
12
+
13
+ import os
14
+ import subprocess
15
+ import sys
16
+ from pathlib import Path
17
+
18
+
19
+ def run_git(*args: str, cwd: Path | None = None) -> str:
20
+ """Run a git command and return output."""
21
+ result = subprocess.run(
22
+ ["git"] + list(args),
23
+ cwd=cwd,
24
+ capture_output=True,
25
+ text=True,
26
+ )
27
+ return result.stdout.strip()
28
+
29
+
30
+ def find_parent_repo(repo_dir: Path) -> Path | None:
31
+ """Find parent repo that has this as a submodule."""
32
+ repo_name = repo_dir.name
33
+ parent_dir = repo_dir.parent
34
+
35
+ while parent_dir != parent_dir.parent:
36
+ git_path = parent_dir / ".git"
37
+ gitmodules_path = parent_dir / ".gitmodules"
38
+
39
+ if git_path.exists() and gitmodules_path.exists():
40
+ try:
41
+ content = gitmodules_path.read_text()
42
+ if f"path = {repo_name}" in content:
43
+ return parent_dir
44
+ except Exception:
45
+ pass
46
+
47
+ parent_dir = parent_dir.parent
48
+
49
+ return None
50
+
51
+
52
+ def main() -> int:
53
+ """Main hook logic."""
54
+ try:
55
+ repo_dir = Path(run_git("rev-parse", "--show-toplevel"))
56
+ except Exception:
57
+ return 0
58
+
59
+ repo_name = repo_dir.name
60
+ parent_repo = find_parent_repo(repo_dir)
61
+
62
+ if not parent_repo:
63
+ return 0
64
+
65
+ commit_msg = run_git("log", "-1", "--pretty=%s", cwd=repo_dir)
66
+ commit_hash = run_git("rev-parse", "--short", "HEAD", cwd=repo_dir)
67
+
68
+ print()
69
+ print("=== Submodule Parent Update ===")
70
+ print(f"Submodule: {repo_name} @ {commit_hash}")
71
+ print(f"Parent: {parent_repo}")
72
+
73
+ run_git("add", repo_name, cwd=parent_repo)
74
+
75
+ diff_result = subprocess.run(
76
+ ["git", "diff", "--cached", "--quiet"],
77
+ cwd=parent_repo,
78
+ )
79
+
80
+ if diff_result.returncode == 0:
81
+ print("Parent already up to date.")
82
+ return 0
83
+
84
+ full_commit_msg = f"""chore: update {repo_name} submodule to {commit_hash}
85
+
86
+ Submodule commit: {commit_msg}
87
+
88
+ Co-Authored-By: Claude <noreply@anthropic.com>"""
89
+
90
+ subprocess.run(
91
+ ["git", "commit", "-m", full_commit_msg],
92
+ cwd=parent_repo,
93
+ )
94
+
95
+ print("Parent updated successfully.")
96
+ print("================================")
97
+ print()
98
+
99
+ return 0
100
+
101
+
102
+ if __name__ == "__main__":
103
+ sys.exit(main())
@@ -0,0 +1,33 @@
1
+ """Tests for GitHub Action workflow YAML validity."""
2
+
3
+ from pathlib import Path
4
+
5
+ import yaml
6
+
7
+
8
+ def test_workflow_is_valid_yaml() -> None:
9
+ """Test that the workflow file is valid YAML."""
10
+ workflow_path = Path(__file__).parent / "pre-push-review.yml"
11
+ assert workflow_path.exists(), "Workflow file must exist"
12
+
13
+ with open(workflow_path) as f:
14
+ data = yaml.safe_load(f)
15
+
16
+ assert "name" in data
17
+ assert "on" in data or True in data
18
+ assert "jobs" in data
19
+
20
+
21
+ def test_workflow_has_validate_job() -> None:
22
+ """Test that workflow has a validate job with required steps."""
23
+ workflow_path = Path(__file__).parent / "pre-push-review.yml"
24
+
25
+ with open(workflow_path) as f:
26
+ data = yaml.safe_load(f)
27
+
28
+ assert "validate" in data["jobs"]
29
+ job = data["jobs"]["validate"]
30
+ assert "steps" in job
31
+ step_names = [s.get("name", "") for s in job["steps"]]
32
+ assert "Checkout code" in step_names
33
+ assert "Set up Python" in step_names