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.
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/agents/agent-writer.md +157 -0
- package/agents/clasp-deployment-orchestrator.md +609 -0
- package/agents/clean-coder.md +295 -0
- package/agents/code-quality-agent.md +40 -0
- package/agents/code-standards-agent.md +93 -0
- package/agents/config-centralizer.md +686 -0
- package/agents/config-extraction-agent.md +225 -0
- package/agents/doc-orchestrator.md +47 -0
- package/agents/docs-agent.md +112 -0
- package/agents/docx-agent.md +211 -0
- package/agents/git-commit-crafter.md +100 -0
- package/agents/magic-value-eliminator-agent.md +72 -0
- package/agents/mandatory-agent-workflow-agent.md +88 -0
- package/agents/parallel-workflow-coordinator.md +779 -0
- package/agents/pdf-agent.md +302 -0
- package/agents/plan-executor.md +226 -0
- package/agents/pr-description-writer.md +87 -0
- package/agents/project-context-loader.md +238 -0
- package/agents/project-docs-analyzer.md +54 -0
- package/agents/project-structure-organizer-agent.md +72 -0
- package/agents/readability-review-agent.md +76 -0
- package/agents/refactoring-specialist.md +69 -0
- package/agents/right-sized-engineer.md +129 -0
- package/agents/session-continuity-manager.md +53 -0
- package/agents/skill-to-agent-converter.md +371 -0
- package/agents/skill-writer-agent.md +470 -0
- package/agents/stub-detector-agent.md +140 -0
- package/agents/tdd-test-writer.md +62 -0
- package/agents/test-data-builder.md +68 -0
- package/agents/tooling-builder.md +78 -0
- package/agents/user-docs-writer.md +67 -0
- package/agents/validation-expert.md +71 -0
- package/agents/workflow-visual-documenter.md +82 -0
- package/agents/xlsx-agent.md +169 -0
- package/bin/install.mjs +256 -0
- package/commands/commit.md +28 -0
- package/commands/docupdate.md +322 -0
- package/commands/implement.md +102 -0
- package/commands/initialize.md +91 -0
- package/commands/plan.md +63 -0
- package/commands/pr-comments.md +47 -0
- package/commands/readability-review.md +20 -0
- package/commands/review-plan.md +7 -0
- package/commands/right-size.md +15 -0
- package/commands/stubcheck.md +89 -0
- package/commands/sum.md +30 -0
- package/docs/CODE_RULES.md +186 -0
- package/docs/DJANGO_PATTERNS.md +80 -0
- package/docs/REACT_PATTERNS.md +185 -0
- package/docs/TEST_QUALITY.md +104 -0
- package/hooks/advisory/migration-safety-advisor.py +49 -0
- package/hooks/advisory/refactor-guard.py +205 -0
- package/hooks/blocking/block-main-commit.py +168 -0
- package/hooks/blocking/code-rules-enforcer.py +549 -0
- package/hooks/blocking/destructive-command-blocker.py +107 -0
- package/hooks/blocking/docker-settings-guard.py +44 -0
- package/hooks/blocking/hedging-language-blocker.py +130 -0
- package/hooks/blocking/parallel-task-blocker.py +69 -0
- package/hooks/blocking/pr-description-enforcer.py +87 -0
- package/hooks/blocking/pyautogui-scroll-blocker.py +74 -0
- package/hooks/blocking/sensitive-file-protector.py +70 -0
- package/hooks/blocking/tdd-enforcer.py +62 -0
- package/hooks/blocking/test-preflight-check.py +343 -0
- package/hooks/blocking/write-existing-file-blocker.py +63 -0
- package/hooks/git-hooks/post-commit.py +103 -0
- package/hooks/github-action/test_workflow.py +33 -0
- package/hooks/hooks.json +246 -0
- package/hooks/lifecycle/config-change-guard.py +84 -0
- package/hooks/lifecycle/session-end-cleanup.py +59 -0
- package/hooks/notification/attention-needed-notify.py +63 -0
- package/hooks/notification/claude-notification-handler.py +59 -0
- package/hooks/notification/notification_utils.py +206 -0
- package/hooks/rewrite-plugin-paths.py +116 -0
- package/hooks/session/bulk-edit-reminder.py +30 -0
- package/hooks/session/code-rules-reminder.py +97 -0
- package/hooks/session/compact-context-reinject.py +39 -0
- package/hooks/session/hook-structure-context.py +140 -0
- package/hooks/session/plugin-data-dir-cleanup.py +39 -0
- package/hooks/validation/code-style-validator.py +145 -0
- package/hooks/validation/e2e-test-validator.py +142 -0
- package/hooks/validation/hook-format-validator.py +66 -0
- package/hooks/validation/mypy_validator.py +180 -0
- package/hooks/validators/README.md +125 -0
- package/hooks/validators/VALIDATION_REPORT.md +287 -0
- package/hooks/validators/__init__.py +19 -0
- package/hooks/validators/abbreviation_checks.py +82 -0
- package/hooks/validators/code_quality_checks.py +133 -0
- package/hooks/validators/comment_checks.py +188 -0
- package/hooks/validators/file_structure_checks.py +182 -0
- package/hooks/validators/git_checks.py +107 -0
- package/hooks/validators/health_check.py +214 -0
- package/hooks/validators/magic_value_checks.py +81 -0
- package/hooks/validators/mypy_integration.py +52 -0
- package/hooks/validators/output_formatter.py +266 -0
- package/hooks/validators/pr_reference_checks.py +72 -0
- package/hooks/validators/python_antipattern_checks.py +110 -0
- package/hooks/validators/python_style_checks.py +364 -0
- package/hooks/validators/react_checks.py +90 -0
- package/hooks/validators/ruff_integration.py +80 -0
- package/hooks/validators/run_all_validators.py +772 -0
- package/hooks/validators/security_checks.py +135 -0
- package/hooks/validators/test_abbreviation_checks.py +76 -0
- package/hooks/validators/test_bad.tsx +7 -0
- package/hooks/validators/test_code_quality_checks.py +129 -0
- package/hooks/validators/test_file_structure_checks.py +307 -0
- package/hooks/validators/test_files/01_basic_component.tsx +10 -0
- package/hooks/validators/test_files/02_component_without_react.tsx +10 -0
- package/hooks/validators/test_files/03_pure_component.tsx +10 -0
- package/hooks/validators/test_files/04_pure_component_import.tsx +10 -0
- package/hooks/validators/test_files/05_typescript_generics.tsx +14 -0
- package/hooks/validators/test_files/06_typescript_two_generics.tsx +18 -0
- package/hooks/validators/test_files/07_multiline_declaration.tsx +11 -0
- package/hooks/validators/test_files/08_error_boundary_valid.tsx +14 -0
- package/hooks/validators/test_files/09_error_boundary_with_other_class.tsx +20 -0
- package/hooks/validators/test_files/10_inheritance_chain.tsx +16 -0
- package/hooks/validators/test_files/11_ts_file.ts +10 -0
- package/hooks/validators/test_files/12_non_react_class.tsx +14 -0
- package/hooks/validators/test_files/13_functional_component.tsx +8 -0
- package/hooks/validators/test_files/14_indented_class.tsx +13 -0
- package/hooks/validators/test_files/15_getDerivedStateFromError.tsx +14 -0
- package/hooks/validators/test_files/16_mixed_components.tsx +20 -0
- package/hooks/validators/test_files/EXECUTIVE_SUMMARY.md +175 -0
- package/hooks/validators/test_files/TEST_RESULTS_TABLE.txt +60 -0
- package/hooks/validators/test_files/VALIDATION_REPORT.md +201 -0
- package/hooks/validators/test_files/async_views.py +23 -0
- package/hooks/validators/test_files/async_with_imports.py +14 -0
- package/hooks/validators/test_files/bad_inline_imports.py +37 -0
- package/hooks/validators/test_files/management/commands/cmd_01_no_debug_check.py +10 -0
- package/hooks/validators/test_files/management/commands/cmd_02_proper_debug_check.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_03_debug_check_with_return.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_04_imported_DEBUG.py +14 -0
- package/hooks/validators/test_files/management/commands/cmd_05_debug_check_in_helper.py +16 -0
- package/hooks/validators/test_files/management/commands/cmd_06_debug_check_late.py +22 -0
- package/hooks/validators/test_files/management/commands/cmd_07_positive_debug_check.py +15 -0
- package/hooks/validators/test_files/management/commands/cmd_08_debug_with_and.py +14 -0
- package/hooks/validators/test_files/not_management_command.py +10 -0
- package/hooks/validators/test_files/skip_decorators/test_01_simple_skip.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_02_pytest_skipif.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_03_unittest_skipIf.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_04_skip_with_parens.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_05_xfail.py +7 -0
- package/hooks/validators/test_files/skip_decorators/test_06_custom_skip.py +11 -0
- package/hooks/validators/test_files/skip_decorators/test_07_capital_Skip.py +8 -0
- package/hooks/validators/test_files/skip_decorators/test_08_skipUnless.py +7 -0
- package/hooks/validators/test_files/skip_decorators/test_09_pytest_mark_skip_simple.py +7 -0
- package/hooks/validators/test_files/test_async_functions.py +45 -0
- package/hooks/validators/test_files/test_purecomponent/PureComponentExample.tsx +7 -0
- package/hooks/validators/test_files/test_purecomponent/ReactPureComponentExample.tsx +7 -0
- package/hooks/validators/test_git_checks.py +295 -0
- package/hooks/validators/test_good.tsx +5 -0
- package/hooks/validators/test_health_check.py +57 -0
- package/hooks/validators/test_magic_value_checks.py +63 -0
- package/hooks/validators/test_mypy_integration.py +27 -0
- package/hooks/validators/test_output_formatter.py +150 -0
- package/hooks/validators/test_pr_reference_checks.py +41 -0
- package/hooks/validators/test_python_antipattern_checks.py +113 -0
- package/hooks/validators/test_python_style_checks.py +439 -0
- package/hooks/validators/test_react_checks.py +213 -0
- package/hooks/validators/test_results.txt +25 -0
- package/hooks/validators/test_ruff_integration.py +27 -0
- package/hooks/validators/test_run_all_validators.py +228 -0
- package/hooks/validators/test_run_all_validators_integration.py +48 -0
- package/hooks/validators/test_safety_checks.py +243 -0
- package/hooks/validators/test_security_checks.py +105 -0
- package/hooks/validators/test_test_safety_checks.py +321 -0
- package/hooks/validators/test_todo_checks.py +39 -0
- package/hooks/validators/test_type_safety_checks.py +85 -0
- package/hooks/validators/test_useless_test_checks.py +55 -0
- package/hooks/validators/test_validator_base.py +26 -0
- package/hooks/validators/test_verify_paths.py +34 -0
- package/hooks/validators/todo_checks.py +59 -0
- package/hooks/validators/type_safety_checks.py +101 -0
- package/hooks/validators/useless_test_checks.py +92 -0
- package/hooks/validators/validator_base.py +19 -0
- package/hooks/validators/verify_paths.py +57 -0
- package/hooks/workflow/auto-formatter.py +114 -0
- package/hooks/workflow/investigation-tracker-reset.py +46 -0
- package/package.json +30 -0
- package/rules/agent-spawn-protocol.md +47 -0
- package/rules/cleanup-temp-files.md +27 -0
- package/rules/code-reviews.md +11 -0
- package/rules/code-standards.md +43 -0
- package/rules/conservative-action.md +20 -0
- package/rules/context7.md +12 -0
- package/rules/explore-thoroughly.md +27 -0
- package/rules/git-workflow.md +42 -0
- package/rules/parallel-tools.md +23 -0
- package/rules/research-mode.md +23 -0
- package/rules/right-sized-engineering.md +28 -0
- package/rules/tdd.md +7 -0
- package/rules/testing.md +12 -0
- package/skills/agent-prompt/SKILL.md +102 -0
- package/skills/anthropic-plan/SKILL.md +107 -0
- package/skills/everything-search/SKILL.md +144 -0
- package/skills/ingest/SKILL.md +40 -0
- package/skills/npm-creator/SKILL.md +183 -0
- package/skills/pr-review-responder/EXAMPLES.md +590 -0
- package/skills/pr-review-responder/PRINCIPLES.md +539 -0
- package/skills/pr-review-responder/README.md +209 -0
- package/skills/pr-review-responder/SKILL.md +202 -0
- package/skills/pr-review-responder/TESTING.md +407 -0
- package/skills/pr-review-responder/scripts/respond_to_reviews.py +376 -0
- package/skills/pr-review-responder/update_skill.py +297 -0
- package/skills/prompt-generator/REFERENCE.md +150 -0
- package/skills/prompt-generator/SKILL.md +154 -0
- package/skills/readability-review/SKILL.md +127 -0
- package/skills/recall/SKILL.md +27 -0
- package/skills/remember/SKILL.md +63 -0
- package/skills/rule-audit/SKILL.md +307 -0
- package/skills/rule-creator/SKILL.md +150 -0
- package/skills/skill-writer/REFERENCE.md +246 -0
- package/skills/skill-writer/SKILL.md +270 -0
- 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
|
+
```
|