pumuki-ast-hooks 5.5.65 → 5.6.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/README.md +152 -6
- package/bin/__tests__/check-version.spec.js +32 -57
- package/package.json +1 -1
- package/scripts/hooks-system/.audit_tmp/hook-metrics.jsonl +4 -0
- package/scripts/hooks-system/bin/__tests__/check-version.spec.js +37 -57
- package/scripts/hooks-system/bin/cli.js +109 -0
- package/scripts/hooks-system/infrastructure/ast/ast-core.js +124 -0
- package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +3 -1
- package/scripts/hooks-system/infrastructure/ast/frontend/analyzers/__tests__/FrontendArchitectureDetector.spec.js +4 -1
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/__tests__/iOSASTIntelligentAnalyzer.spec.js +3 -1
- package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-ast-intelligent-strategies.js +1 -1
- package/scripts/hooks-system/infrastructure/cascade-hooks/README.md +114 -0
- package/scripts/hooks-system/infrastructure/cascade-hooks/cascade-hooks-config.json +20 -0
- package/scripts/hooks-system/infrastructure/cascade-hooks/claude-code-hook.sh +127 -0
- package/scripts/hooks-system/infrastructure/cascade-hooks/post-write-code-hook.js +72 -0
- package/scripts/hooks-system/infrastructure/cascade-hooks/pre-write-code-hook.js +167 -0
- package/scripts/hooks-system/infrastructure/cascade-hooks/universal-hook-adapter.js +186 -0
- package/scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js +739 -24
- package/scripts/hooks-system/infrastructure/observability/MetricsCollector.js +221 -0
- package/scripts/hooks-system/infrastructure/observability/index.js +23 -0
- package/scripts/hooks-system/infrastructure/orchestration/__tests__/intelligent-audit.spec.js +177 -0
- package/scripts/hooks-system/infrastructure/resilience/CircuitBreaker.js +229 -0
- package/scripts/hooks-system/infrastructure/resilience/RetryPolicy.js +141 -0
- package/scripts/hooks-system/infrastructure/resilience/index.js +34 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* =============================================================================
|
|
4
|
+
* 🚀 REVOLUTIONARY: Cascade Hook - pre_write_code
|
|
5
|
+
* =============================================================================
|
|
6
|
+
*
|
|
7
|
+
* This hook is executed by Windsurf BEFORE any code is written.
|
|
8
|
+
* It uses AST Intelligence to validate the proposed code and BLOCKS
|
|
9
|
+
* the write if critical violations are detected.
|
|
10
|
+
*
|
|
11
|
+
* Exit codes:
|
|
12
|
+
* - 0: Allow the write
|
|
13
|
+
* - 2: BLOCK the write (critical violations)
|
|
14
|
+
* - 1: Error (allow write, log error)
|
|
15
|
+
*
|
|
16
|
+
* Author: Pumuki Team®
|
|
17
|
+
* =============================================================================
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
|
|
23
|
+
function getRepoRoot(filePath) {
|
|
24
|
+
if (filePath) {
|
|
25
|
+
const { execSync } = require('child_process');
|
|
26
|
+
try {
|
|
27
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
28
|
+
encoding: 'utf-8',
|
|
29
|
+
cwd: path.dirname(filePath)
|
|
30
|
+
}).trim();
|
|
31
|
+
} catch (e) {
|
|
32
|
+
if (process.env.DEBUG) {
|
|
33
|
+
process.stderr.write(`[pre-write-hook] Git root lookup failed: ${e.message}\n`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Fallback: this script is in scripts/hooks-system/infrastructure/cascade-hooks/
|
|
38
|
+
return path.resolve(__dirname, '..', '..', '..', '..');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
|
42
|
+
|
|
43
|
+
const LOG_FILE = path.join(REPO_ROOT, '.audit_tmp', 'cascade-hook.log');
|
|
44
|
+
|
|
45
|
+
function log(message) {
|
|
46
|
+
const timestamp = new Date().toISOString();
|
|
47
|
+
const logLine = `[${timestamp}] ${message}\n`;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
|
|
51
|
+
fs.appendFileSync(LOG_FILE, logLine);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (process.env.DEBUG) {
|
|
54
|
+
process.stderr.write(`[pre-write-hook] Log write failed: ${error.message}\n`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (process.env.DEBUG) {
|
|
59
|
+
process.stderr.write(logLine);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function main() {
|
|
64
|
+
let inputData = '';
|
|
65
|
+
|
|
66
|
+
// Read JSON from stdin
|
|
67
|
+
for await (const chunk of process.stdin) {
|
|
68
|
+
inputData += chunk;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let hookInput;
|
|
72
|
+
try {
|
|
73
|
+
hookInput = JSON.parse(inputData);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
log(`ERROR: Failed to parse hook input: ${error.message}`);
|
|
76
|
+
process.exit(1); // Error, allow write
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { agent_action_name, tool_info } = hookInput;
|
|
80
|
+
|
|
81
|
+
if (agent_action_name !== 'pre_write_code') {
|
|
82
|
+
log(`SKIP: Not a pre_write_code event: ${agent_action_name}`);
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const filePath = tool_info?.file_path;
|
|
87
|
+
const edits = tool_info?.edits || [];
|
|
88
|
+
|
|
89
|
+
if (!filePath) {
|
|
90
|
+
log('WARN: No file_path in tool_info');
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Update REPO_ROOT based on the file being edited
|
|
95
|
+
REPO_ROOT = getRepoRoot(filePath);
|
|
96
|
+
|
|
97
|
+
log(`ANALYZING: ${filePath} (${edits.length} edits) [REPO: ${REPO_ROOT}]`);
|
|
98
|
+
|
|
99
|
+
// Skip test files from blocking (allow TDD flow)
|
|
100
|
+
if (/\.(spec|test)\.(js|ts|swift|kt)$/.test(filePath)) {
|
|
101
|
+
log(`ALLOW: Test file detected - TDD cycle allowed: ${filePath}`);
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Load AST analyzer
|
|
106
|
+
let analyzeCodeInMemory;
|
|
107
|
+
try {
|
|
108
|
+
const astCore = require(path.join(REPO_ROOT, 'scripts/hooks-system/infrastructure/ast/ast-core'));
|
|
109
|
+
analyzeCodeInMemory = astCore.analyzeCodeInMemory;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
log(`ERROR: Failed to load AST analyzer: ${error.message}`);
|
|
112
|
+
process.exit(1); // Error, allow write (graceful degradation)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Analyze each edit
|
|
116
|
+
const allViolations = [];
|
|
117
|
+
|
|
118
|
+
for (const edit of edits) {
|
|
119
|
+
const newCode = edit.new_string || '';
|
|
120
|
+
|
|
121
|
+
if (!newCode || newCode.trim().length === 0) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const analysis = analyzeCodeInMemory(newCode, filePath);
|
|
127
|
+
|
|
128
|
+
if (analysis.hasCritical) {
|
|
129
|
+
allViolations.push(...analysis.violations.filter(v => v.severity === 'CRITICAL'));
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
log(`WARN: AST analysis failed for edit: ${error.message}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (allViolations.length > 0) {
|
|
137
|
+
const errorMessage = [
|
|
138
|
+
'',
|
|
139
|
+
'🚫 ═══════════════════════════════════════════════════════════════',
|
|
140
|
+
'🚫 AST INTELLIGENCE BLOCKED THIS WRITE',
|
|
141
|
+
'🚫 ═══════════════════════════════════════════════════════════════',
|
|
142
|
+
'',
|
|
143
|
+
`📁 File: ${filePath}`,
|
|
144
|
+
`❌ Critical Violations: ${allViolations.length}`,
|
|
145
|
+
'',
|
|
146
|
+
...allViolations.map((v, i) => ` ${i + 1}. [${v.ruleId}] ${v.message}`),
|
|
147
|
+
'',
|
|
148
|
+
'🔧 FIX THESE VIOLATIONS BEFORE WRITING:',
|
|
149
|
+
...allViolations.slice(0, 3).map(v => ` → ${v.message}`),
|
|
150
|
+
'',
|
|
151
|
+
'═══════════════════════════════════════════════════════════════',
|
|
152
|
+
''
|
|
153
|
+
].join('\n');
|
|
154
|
+
|
|
155
|
+
log(`BLOCKED: ${allViolations.length} critical violations in ${filePath}`);
|
|
156
|
+
process.stderr.write(errorMessage);
|
|
157
|
+
process.exit(2); // EXIT CODE 2 = BLOCK THE WRITE
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
log(`ALLOWED: No critical violations in ${filePath}`);
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
main().catch(error => {
|
|
165
|
+
log(`FATAL ERROR: ${error.message}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* =============================================================================
|
|
4
|
+
* 🌐 UNIVERSAL: IDE-Agnostic Hook Adapter
|
|
5
|
+
* =============================================================================
|
|
6
|
+
*
|
|
7
|
+
* This adapter works with ANY IDE that supports hooks:
|
|
8
|
+
* - Windsurf: pre_write_code, post_write_code
|
|
9
|
+
* - Cursor: afterFileEdit
|
|
10
|
+
* - Claude Code: afterFileEdit
|
|
11
|
+
*
|
|
12
|
+
* For IDEs without pre-write hooks, the Git pre-commit is the 100% fallback.
|
|
13
|
+
*
|
|
14
|
+
* Author: Pumuki Team®
|
|
15
|
+
* =============================================================================
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
|
|
21
|
+
const REPO_ROOT = (() => {
|
|
22
|
+
try {
|
|
23
|
+
const { execSync } = require('child_process');
|
|
24
|
+
return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return process.cwd();
|
|
27
|
+
}
|
|
28
|
+
})();
|
|
29
|
+
|
|
30
|
+
const LOG_FILE = path.join(REPO_ROOT, '.audit_tmp', 'universal-hook.log');
|
|
31
|
+
|
|
32
|
+
function log(level, message) {
|
|
33
|
+
const timestamp = new Date().toISOString();
|
|
34
|
+
const logLine = `[${timestamp}] [${level}] ${message}\n`;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
|
|
38
|
+
fs.appendFileSync(LOG_FILE, logLine);
|
|
39
|
+
} catch (writeError) {
|
|
40
|
+
if (process.env.DEBUG) {
|
|
41
|
+
process.stderr.write(`[universal-hook] Log failed: ${writeError.message}\n`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (process.env.DEBUG || level === 'ERROR') {
|
|
46
|
+
process.stderr.write(logLine);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function detectIDE() {
|
|
51
|
+
if (process.env.WINDSURF_SESSION_ID) return 'windsurf';
|
|
52
|
+
if (process.env.CURSOR_SESSION_ID) return 'cursor';
|
|
53
|
+
if (process.env.CLAUDE_CODE_SESSION) return 'claude-code';
|
|
54
|
+
if (process.env.KILO_CODE_SESSION) return 'kilo-code';
|
|
55
|
+
|
|
56
|
+
const parentProcess = process.env._ || '';
|
|
57
|
+
if (parentProcess.includes('windsurf')) return 'windsurf';
|
|
58
|
+
if (parentProcess.includes('cursor')) return 'cursor';
|
|
59
|
+
if (parentProcess.includes('claude')) return 'claude-code';
|
|
60
|
+
|
|
61
|
+
return 'unknown';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function analyzeCode(code, filePath) {
|
|
65
|
+
try {
|
|
66
|
+
const { analyzeCodeInMemory } = require(path.join(
|
|
67
|
+
REPO_ROOT,
|
|
68
|
+
'scripts/hooks-system/infrastructure/ast/ast-core'
|
|
69
|
+
));
|
|
70
|
+
return analyzeCodeInMemory(code, filePath);
|
|
71
|
+
} catch (loadError) {
|
|
72
|
+
log('ERROR', `AST load failed: ${loadError.message}`);
|
|
73
|
+
return { success: false, violations: [], hasCritical: false };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function handleWindsurfPreWrite(hookInput) {
|
|
78
|
+
const { tool_info } = hookInput;
|
|
79
|
+
const filePath = tool_info?.file_path;
|
|
80
|
+
const edits = tool_info?.edits || [];
|
|
81
|
+
|
|
82
|
+
if (!filePath) {
|
|
83
|
+
log('WARN', 'No file_path in Windsurf hook');
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (/\.(spec|test)\.(js|ts|swift|kt)$/.test(filePath)) {
|
|
88
|
+
log('INFO', `TDD: Test file allowed: ${filePath}`);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const edit of edits) {
|
|
93
|
+
const newCode = edit.new_string || '';
|
|
94
|
+
if (!newCode.trim()) continue;
|
|
95
|
+
|
|
96
|
+
const analysis = analyzeCode(newCode, filePath);
|
|
97
|
+
|
|
98
|
+
if (analysis.hasCritical) {
|
|
99
|
+
const violations = analysis.violations.filter(v => v.severity === 'CRITICAL');
|
|
100
|
+
log('BLOCKED', `${violations.length} critical violations in ${filePath}`);
|
|
101
|
+
|
|
102
|
+
process.stderr.write(`\n🚫 AST INTELLIGENCE BLOCKED\n`);
|
|
103
|
+
process.stderr.write(`File: ${filePath}\n`);
|
|
104
|
+
violations.forEach(v => {
|
|
105
|
+
process.stderr.write(` ❌ [${v.ruleId}] ${v.message}\n`);
|
|
106
|
+
});
|
|
107
|
+
process.stderr.write(`\n`);
|
|
108
|
+
|
|
109
|
+
return 2; // BLOCK
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
log('ALLOWED', `No violations in ${filePath}`);
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function handleCursorAfterEdit(hookInput) {
|
|
118
|
+
const { tool_info } = hookInput;
|
|
119
|
+
const filePath = tool_info?.file_path;
|
|
120
|
+
|
|
121
|
+
log('INFO', `Cursor afterFileEdit: ${filePath}`);
|
|
122
|
+
|
|
123
|
+
// For Cursor, we can only log - blocking happens at Git pre-commit
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function handleClaudeCodeAfterEdit(hookInput) {
|
|
128
|
+
const { tool_info } = hookInput;
|
|
129
|
+
const filePath = tool_info?.file_path;
|
|
130
|
+
|
|
131
|
+
log('INFO', `Claude Code afterFileEdit: ${filePath}`);
|
|
132
|
+
|
|
133
|
+
// For Claude Code, we can only log - blocking happens at Git pre-commit
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function main() {
|
|
138
|
+
let inputData = '';
|
|
139
|
+
|
|
140
|
+
for await (const chunk of process.stdin) {
|
|
141
|
+
inputData += chunk;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!inputData.trim()) {
|
|
145
|
+
log('WARN', 'Empty input received');
|
|
146
|
+
process.exit(0);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let hookInput;
|
|
150
|
+
try {
|
|
151
|
+
hookInput = JSON.parse(inputData);
|
|
152
|
+
} catch (parseError) {
|
|
153
|
+
log('ERROR', `JSON parse failed: ${parseError.message}`);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const ide = detectIDE();
|
|
158
|
+
const eventName = hookInput.agent_action_name || hookInput.event || 'unknown';
|
|
159
|
+
|
|
160
|
+
log('INFO', `IDE: ${ide}, Event: ${eventName}`);
|
|
161
|
+
|
|
162
|
+
let exitCode = 0;
|
|
163
|
+
|
|
164
|
+
switch (eventName) {
|
|
165
|
+
case 'pre_write_code':
|
|
166
|
+
exitCode = await handleWindsurfPreWrite(hookInput);
|
|
167
|
+
break;
|
|
168
|
+
case 'afterFileEdit':
|
|
169
|
+
if (ide === 'cursor') {
|
|
170
|
+
exitCode = await handleCursorAfterEdit(hookInput);
|
|
171
|
+
} else {
|
|
172
|
+
exitCode = await handleClaudeCodeAfterEdit(hookInput);
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
default:
|
|
176
|
+
log('INFO', `Unhandled event: ${eventName}`);
|
|
177
|
+
exitCode = 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
process.exit(exitCode);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
main().catch(error => {
|
|
184
|
+
log('FATAL', error.message);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
});
|