start-vibing 2.0.19 → 2.0.21
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/package.json +1 -1
- package/template/.claude/hooks/run-hook.ts +18 -10
- package/template/.claude/hooks/stop-validator.ts +22 -2
- package/template/.claude/hooks/check-documentation.py +0 -268
- package/template/.claude/hooks/stop-validator.py +0 -336
- package/template/.claude/hooks/user-prompt-submit.py +0 -826
package/package.json
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
* Universal Hook Runner
|
|
4
4
|
*
|
|
5
5
|
* Runs hooks with multiple runtime fallbacks:
|
|
6
|
-
* 1.
|
|
7
|
-
* 2.
|
|
8
|
-
* 3.
|
|
6
|
+
* 1. bun (primary - fastest TypeScript execution)
|
|
7
|
+
* 2. npx tsx (TypeScript fallback)
|
|
8
|
+
* 3. python3 (Python fallback)
|
|
9
|
+
* 4. python (Python fallback)
|
|
10
|
+
*
|
|
11
|
+
* IMPORTANT: TypeScript files are the source of truth.
|
|
12
|
+
* Python files are only for environments without Node.js/Bun.
|
|
9
13
|
*
|
|
10
14
|
* Usage: npx tsx run-hook.ts <hook-name>
|
|
11
15
|
* The hook-name should be without extension (e.g., "stop-validator")
|
|
@@ -96,11 +100,10 @@ async function runHook(hookName: string, stdinData: string): Promise<void> {
|
|
|
96
100
|
const tsPath = join(HOOKS_DIR, `${hookName}.ts`);
|
|
97
101
|
const pyPath = join(HOOKS_DIR, `${hookName}.py`);
|
|
98
102
|
|
|
99
|
-
// Runtime detection order -
|
|
103
|
+
// Runtime detection order - TypeScript ONLY (source of truth)
|
|
104
|
+
// Python files are deprecated and should be removed
|
|
100
105
|
const runtimes: Array<{ name: string; cmd: string; ext: string }> = [
|
|
101
|
-
{ name: '
|
|
102
|
-
{ name: 'python', cmd: 'python', ext: '.py' },
|
|
103
|
-
{ name: 'bun-ts', cmd: 'bun', ext: '.ts' },
|
|
106
|
+
{ name: 'bun', cmd: 'bun', ext: '.ts' },
|
|
104
107
|
{ name: 'npx-tsx', cmd: 'npx tsx', ext: '.ts' },
|
|
105
108
|
];
|
|
106
109
|
|
|
@@ -130,7 +133,7 @@ async function runHook(hookName: string, stdinData: string): Promise<void> {
|
|
|
130
133
|
|
|
131
134
|
// No runtime available - return safe default
|
|
132
135
|
console.error(`[run-hook] No runtime available to run hook: ${hookName}`);
|
|
133
|
-
console.error('[run-hook] Please install
|
|
136
|
+
console.error('[run-hook] Please install bun or Node.js (for npx tsx)');
|
|
134
137
|
const safeDefault = JSON.stringify({
|
|
135
138
|
decision: 'approve',
|
|
136
139
|
continue: true,
|
|
@@ -171,17 +174,22 @@ async function readStdinWithTimeout(timeoutMs: number): Promise<string> {
|
|
|
171
174
|
|
|
172
175
|
// Main
|
|
173
176
|
async function main(): Promise<void> {
|
|
177
|
+
// Log hook invocation for debugging (writes to stderr so it doesn't affect JSON output)
|
|
178
|
+
const hookName = process.argv[2];
|
|
179
|
+
const timestamp = new Date().toISOString();
|
|
180
|
+
console.error(`[run-hook] ${timestamp} - Hook invoked: ${hookName || 'none'}`);
|
|
181
|
+
|
|
174
182
|
// Clean up deprecated files on every hook run
|
|
175
183
|
cleanupDeprecatedFiles();
|
|
176
184
|
|
|
177
|
-
const hookName = process.argv[2];
|
|
178
185
|
if (!hookName) {
|
|
179
|
-
console.error('Usage: bun run-hook.ts <hook-name>');
|
|
186
|
+
console.error('[run-hook] Usage: bun run-hook.ts <hook-name>');
|
|
180
187
|
process.exit(1);
|
|
181
188
|
}
|
|
182
189
|
|
|
183
190
|
// Read stdin with timeout to avoid hanging
|
|
184
191
|
const stdinData = await readStdinWithTimeout(2000);
|
|
192
|
+
console.error(`[run-hook] ${hookName} - stdin received, length: ${stdinData.length}`);
|
|
185
193
|
await runHook(hookName, stdinData);
|
|
186
194
|
}
|
|
187
195
|
|
|
@@ -965,11 +965,20 @@ async function readStdinWithTimeout(timeoutMs: number): Promise<string> {
|
|
|
965
965
|
}
|
|
966
966
|
|
|
967
967
|
async function main(): Promise<void> {
|
|
968
|
+
// Debug logging - always output to stderr for visibility
|
|
969
|
+
console.error('[stop-validator] ========================================');
|
|
970
|
+
console.error('[stop-validator] Starting validation...');
|
|
971
|
+
console.error(`[stop-validator] CWD: ${process.cwd()}`);
|
|
972
|
+
console.error(`[stop-validator] PROJECT_DIR: ${PROJECT_DIR}`);
|
|
973
|
+
console.error(`[stop-validator] CLAUDE_MD exists: ${existsSync(CLAUDE_MD_PATH)}`);
|
|
974
|
+
|
|
968
975
|
let hookInput: HookInput = {};
|
|
969
976
|
try {
|
|
970
977
|
const stdin = await readStdinWithTimeout(1000);
|
|
978
|
+
console.error(`[stop-validator] stdin received: ${stdin.length} chars`);
|
|
971
979
|
if (stdin && stdin.trim()) {
|
|
972
980
|
hookInput = JSON.parse(stdin);
|
|
981
|
+
console.error(`[stop-validator] Parsed input keys: ${Object.keys(hookInput).join(', ') || 'none'}`);
|
|
973
982
|
}
|
|
974
983
|
} catch {
|
|
975
984
|
hookInput = {};
|
|
@@ -993,6 +1002,11 @@ async function main(): Promise<void> {
|
|
|
993
1002
|
const isCleanTree = modifiedFiles.length === 0;
|
|
994
1003
|
|
|
995
1004
|
// Run all validations
|
|
1005
|
+
console.error('[stop-validator] Running validations...');
|
|
1006
|
+
console.error(`[stop-validator] Branch: ${currentBranch}, isMain: ${isMainBranch}`);
|
|
1007
|
+
console.error(`[stop-validator] Modified files: ${modifiedFiles.length}`);
|
|
1008
|
+
console.error(`[stop-validator] Source files: ${sourceFiles.length}`);
|
|
1009
|
+
|
|
996
1010
|
const errors: ValidationError[] = [];
|
|
997
1011
|
|
|
998
1012
|
// Validation order matters - most critical first
|
|
@@ -1036,6 +1050,11 @@ async function main(): Promise<void> {
|
|
|
1036
1050
|
// OUTPUT RESULTS
|
|
1037
1051
|
// ============================================================================
|
|
1038
1052
|
|
|
1053
|
+
console.error(`[stop-validator] Validation complete. Errors found: ${errors.length}`);
|
|
1054
|
+
if (errors.length > 0) {
|
|
1055
|
+
console.error(`[stop-validator] Error types: ${errors.map((e) => e.type).join(', ')}`);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1039
1058
|
if (errors.length > 0) {
|
|
1040
1059
|
let output = `
|
|
1041
1060
|
################################################################################
|
|
@@ -1073,9 +1092,10 @@ Before completing, ask yourself:
|
|
|
1073
1092
|
Update CLAUDE.md with any learnings from this session.
|
|
1074
1093
|
`;
|
|
1075
1094
|
|
|
1095
|
+
// IMPORTANT: For blocking, output to STDERR and exit with code 2
|
|
1076
1096
|
const result: HookResult = { decision: 'block', reason: output.trim() };
|
|
1077
|
-
console.
|
|
1078
|
-
process.exit(
|
|
1097
|
+
console.error(JSON.stringify(result));
|
|
1098
|
+
process.exit(2); // Exit code 2 = block and show to Claude
|
|
1079
1099
|
}
|
|
1080
1100
|
|
|
1081
1101
|
// All validations passed
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Check Documentation Hook
|
|
4
|
-
|
|
5
|
-
Verifies that all modified files are properly documented.
|
|
6
|
-
Runs regex search across documentation to ensure file mentions.
|
|
7
|
-
|
|
8
|
-
IGNORE PATTERNS:
|
|
9
|
-
- .next/
|
|
10
|
-
- node_modules/
|
|
11
|
-
- dist/
|
|
12
|
-
- build/
|
|
13
|
-
- coverage/
|
|
14
|
-
- .git/
|
|
15
|
-
- *.lock
|
|
16
|
-
- *.log
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
import json
|
|
20
|
-
import sys
|
|
21
|
-
import os
|
|
22
|
-
import re
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
from typing import List, Set, Tuple
|
|
25
|
-
|
|
26
|
-
# Directories to ignore
|
|
27
|
-
IGNORE_DIRS = {
|
|
28
|
-
'.next',
|
|
29
|
-
'node_modules',
|
|
30
|
-
'dist',
|
|
31
|
-
'build',
|
|
32
|
-
'coverage',
|
|
33
|
-
'.git',
|
|
34
|
-
'__pycache__',
|
|
35
|
-
'.turbo',
|
|
36
|
-
'.cache',
|
|
37
|
-
'.husky/_',
|
|
38
|
-
'.claude', # System config
|
|
39
|
-
'packages' # Monorepo packages
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
# File patterns to ignore
|
|
43
|
-
IGNORE_PATTERNS = [
|
|
44
|
-
r'.*\.lock$',
|
|
45
|
-
r'.*\.log$',
|
|
46
|
-
r'.*\.map$',
|
|
47
|
-
r'.*\.min\.js$',
|
|
48
|
-
r'.*\.min\.css$',
|
|
49
|
-
r'package-lock\.json$',
|
|
50
|
-
r'bun\.lockb$',
|
|
51
|
-
r'\.DS_Store$',
|
|
52
|
-
r'Thumbs\.db$',
|
|
53
|
-
]
|
|
54
|
-
|
|
55
|
-
# Documentation directories to search
|
|
56
|
-
DOC_DIRS = [
|
|
57
|
-
'docs',
|
|
58
|
-
'.claude/skills/codebase-knowledge/domains',
|
|
59
|
-
'.claude/skills/docs-tracker',
|
|
60
|
-
]
|
|
61
|
-
|
|
62
|
-
# Documentation file extensions
|
|
63
|
-
DOC_EXTENSIONS = {'.md', '.mdx', '.txt', '.rst'}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def should_ignore_file(file_path: str) -> bool:
|
|
67
|
-
"""Check if file should be ignored from documentation check"""
|
|
68
|
-
path = Path(file_path)
|
|
69
|
-
|
|
70
|
-
# Check if in ignored directory
|
|
71
|
-
for part in path.parts:
|
|
72
|
-
if part in IGNORE_DIRS:
|
|
73
|
-
return True
|
|
74
|
-
|
|
75
|
-
# Check if matches ignore pattern
|
|
76
|
-
for pattern in IGNORE_PATTERNS:
|
|
77
|
-
if re.match(pattern, file_path):
|
|
78
|
-
return True
|
|
79
|
-
|
|
80
|
-
# Ignore documentation files themselves
|
|
81
|
-
if path.suffix in DOC_EXTENSIONS:
|
|
82
|
-
return True
|
|
83
|
-
|
|
84
|
-
# Ignore config files that don't need documentation
|
|
85
|
-
if path.name in {'.gitignore', '.eslintrc.json', '.prettierrc', 'tsconfig.json'}:
|
|
86
|
-
return True
|
|
87
|
-
|
|
88
|
-
return False
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def get_modified_files(project_dir: str) -> List[str]:
|
|
92
|
-
"""Get list of modified files from git diff"""
|
|
93
|
-
import subprocess
|
|
94
|
-
|
|
95
|
-
try:
|
|
96
|
-
# Get files changed compared to main branch
|
|
97
|
-
result = subprocess.run(
|
|
98
|
-
['git', 'diff', '--name-only', 'main...HEAD'],
|
|
99
|
-
cwd=project_dir,
|
|
100
|
-
capture_output=True,
|
|
101
|
-
text=True
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
if result.returncode != 0:
|
|
105
|
-
# Fallback: get staged + unstaged changes
|
|
106
|
-
result = subprocess.run(
|
|
107
|
-
['git', 'diff', '--name-only', 'HEAD'],
|
|
108
|
-
cwd=project_dir,
|
|
109
|
-
capture_output=True,
|
|
110
|
-
text=True
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
files = [f.strip() for f in result.stdout.strip().split('\n') if f.strip()]
|
|
114
|
-
|
|
115
|
-
# Also get staged files
|
|
116
|
-
staged = subprocess.run(
|
|
117
|
-
['git', 'diff', '--name-only', '--cached'],
|
|
118
|
-
cwd=project_dir,
|
|
119
|
-
capture_output=True,
|
|
120
|
-
text=True
|
|
121
|
-
)
|
|
122
|
-
staged_files = [f.strip() for f in staged.stdout.strip().split('\n') if f.strip()]
|
|
123
|
-
|
|
124
|
-
# Combine and deduplicate
|
|
125
|
-
all_files = list(set(files + staged_files))
|
|
126
|
-
return [f for f in all_files if f and not should_ignore_file(f)]
|
|
127
|
-
|
|
128
|
-
except Exception as e:
|
|
129
|
-
print(f"Error getting modified files: {e}", file=sys.stderr)
|
|
130
|
-
return []
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def search_in_docs(project_dir: str, file_path: str) -> List[str]:
|
|
134
|
-
"""Search for file mentions in documentation"""
|
|
135
|
-
found_in = []
|
|
136
|
-
file_name = Path(file_path).name
|
|
137
|
-
file_stem = Path(file_path).stem
|
|
138
|
-
|
|
139
|
-
# Patterns to search for
|
|
140
|
-
patterns = [
|
|
141
|
-
re.escape(file_path), # Full path
|
|
142
|
-
re.escape(file_name), # Just filename
|
|
143
|
-
re.escape(file_stem), # Filename without extension
|
|
144
|
-
]
|
|
145
|
-
|
|
146
|
-
# Search in documentation directories
|
|
147
|
-
for doc_dir in DOC_DIRS:
|
|
148
|
-
doc_path = Path(project_dir) / doc_dir
|
|
149
|
-
if not doc_path.exists():
|
|
150
|
-
continue
|
|
151
|
-
|
|
152
|
-
for doc_file in doc_path.rglob('*'):
|
|
153
|
-
if doc_file.suffix not in DOC_EXTENSIONS:
|
|
154
|
-
continue
|
|
155
|
-
if not doc_file.is_file():
|
|
156
|
-
continue
|
|
157
|
-
|
|
158
|
-
try:
|
|
159
|
-
content = doc_file.read_text(encoding='utf-8', errors='ignore')
|
|
160
|
-
for pattern in patterns:
|
|
161
|
-
if re.search(pattern, content, re.IGNORECASE):
|
|
162
|
-
found_in.append(str(doc_file.relative_to(project_dir)))
|
|
163
|
-
break
|
|
164
|
-
except Exception:
|
|
165
|
-
continue
|
|
166
|
-
|
|
167
|
-
return found_in
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def check_documentation(project_dir: str) -> Tuple[List[str], List[str]]:
|
|
171
|
-
"""
|
|
172
|
-
Check if modified files are documented.
|
|
173
|
-
|
|
174
|
-
Returns:
|
|
175
|
-
Tuple of (undocumented_files, documented_files)
|
|
176
|
-
"""
|
|
177
|
-
modified_files = get_modified_files(project_dir)
|
|
178
|
-
undocumented = []
|
|
179
|
-
documented = []
|
|
180
|
-
|
|
181
|
-
for file_path in modified_files:
|
|
182
|
-
found_in = search_in_docs(project_dir, file_path)
|
|
183
|
-
if found_in:
|
|
184
|
-
documented.append((file_path, found_in))
|
|
185
|
-
else:
|
|
186
|
-
undocumented.append(file_path)
|
|
187
|
-
|
|
188
|
-
return undocumented, documented
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
def main():
|
|
192
|
-
"""Main entry point for the hook"""
|
|
193
|
-
project_dir = os.environ.get('CLAUDE_PROJECT_DIR', os.getcwd())
|
|
194
|
-
|
|
195
|
-
# Check if this is being called from a hook (stdin has JSON) or directly
|
|
196
|
-
try:
|
|
197
|
-
hook_input = json.load(sys.stdin)
|
|
198
|
-
except (json.JSONDecodeError, Exception):
|
|
199
|
-
hook_input = {}
|
|
200
|
-
|
|
201
|
-
undocumented, documented = check_documentation(project_dir)
|
|
202
|
-
|
|
203
|
-
if undocumented:
|
|
204
|
-
# Build error message
|
|
205
|
-
error_msg = f"""
|
|
206
|
-
================================================================================
|
|
207
|
-
DOCUMENTATION CHECK FAILED
|
|
208
|
-
================================================================================
|
|
209
|
-
|
|
210
|
-
The following {len(undocumented)} file(s) have been modified but are NOT mentioned
|
|
211
|
-
in any documentation:
|
|
212
|
-
|
|
213
|
-
"""
|
|
214
|
-
for f in undocumented:
|
|
215
|
-
error_msg += f" - {f}\n"
|
|
216
|
-
|
|
217
|
-
error_msg += """
|
|
218
|
-
================================================================================
|
|
219
|
-
ACTION REQUIRED
|
|
220
|
-
================================================================================
|
|
221
|
-
|
|
222
|
-
You MUST run the documenter agent to update documentation before completing:
|
|
223
|
-
|
|
224
|
-
Task(subagent_type="documenter", prompt="Update documentation for modified files")
|
|
225
|
-
|
|
226
|
-
The documenter will:
|
|
227
|
-
1. Detect all changed files via git diff
|
|
228
|
-
2. Update relevant domain files in .claude/skills/codebase-knowledge/domains/
|
|
229
|
-
3. Update docs/CHANGELOG.md (if exists)
|
|
230
|
-
4. Ensure all modified files are properly documented
|
|
231
|
-
|
|
232
|
-
DOCUMENTATION IS MANDATORY. This check will BLOCK task completion until all
|
|
233
|
-
modified source files are mentioned in the documentation.
|
|
234
|
-
|
|
235
|
-
================================================================================
|
|
236
|
-
"""
|
|
237
|
-
|
|
238
|
-
# Return as blocking error
|
|
239
|
-
result = {
|
|
240
|
-
"continue": False,
|
|
241
|
-
"error": error_msg.strip()
|
|
242
|
-
}
|
|
243
|
-
print(json.dumps(result))
|
|
244
|
-
sys.exit(1)
|
|
245
|
-
|
|
246
|
-
else:
|
|
247
|
-
# All files documented
|
|
248
|
-
success_msg = f"""
|
|
249
|
-
================================================================================
|
|
250
|
-
DOCUMENTATION CHECK PASSED
|
|
251
|
-
================================================================================
|
|
252
|
-
|
|
253
|
-
All {len(documented)} modified file(s) are properly documented.
|
|
254
|
-
|
|
255
|
-
"""
|
|
256
|
-
for file_path, doc_locations in documented:
|
|
257
|
-
success_msg += f" - {file_path} -> {', '.join(doc_locations)}\n"
|
|
258
|
-
|
|
259
|
-
result = {
|
|
260
|
-
"continue": True,
|
|
261
|
-
"systemMessage": success_msg.strip()
|
|
262
|
-
}
|
|
263
|
-
print(json.dumps(result))
|
|
264
|
-
sys.exit(0)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if __name__ == '__main__':
|
|
268
|
-
main()
|