pumuki-ast-hooks 5.5.60 → 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 +361 -1101
- package/bin/__tests__/check-version.spec.js +32 -57
- package/docs/ARCHITECTURE.md +66 -1
- package/docs/TODO.md +41 -0
- package/docs/images/ast_intelligence_01.svg +40 -0
- package/docs/images/ast_intelligence_02.svg +39 -0
- package/docs/images/ast_intelligence_03.svg +55 -0
- package/docs/images/ast_intelligence_04.svg +39 -0
- package/docs/images/ast_intelligence_05.svg +45 -0
- package/docs/images/logo.png +0 -0
- package/package.json +1 -1
- package/scripts/hooks-system/.audit_tmp/hook-metrics.jsonl +20 -0
- package/scripts/hooks-system/application/DIValidationService.js +43 -0
- package/scripts/hooks-system/application/__tests__/DIValidationService.spec.js +81 -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/config/di-rules.json +42 -0
- package/scripts/hooks-system/domain/ports/FileSystemPort.js +19 -0
- package/scripts/hooks-system/domain/strategies/ConcreteDependencyStrategy.js +78 -0
- package/scripts/hooks-system/domain/strategies/DIStrategy.js +31 -0
- package/scripts/hooks-system/infrastructure/adapters/NodeFileSystemAdapter.js +28 -0
- package/scripts/hooks-system/infrastructure/ast/ast-core.js +124 -0
- package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +19 -1
- package/scripts/hooks-system/infrastructure/ast/backend/detectors/god-class-detector.js +28 -8
- package/scripts/hooks-system/infrastructure/ast/common/ast-common.js +133 -0
- 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/analyzers/iOSASTIntelligentAnalyzer.js +3 -2
- package/scripts/hooks-system/infrastructure/ast/ios/ast-ios.js +1 -1
- package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-ast-intelligent-strategies.js +40 -46
- 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/orchestration/intelligent-audit.js +87 -1
- package/scripts/hooks-system/infrastructure/registry/StrategyRegistry.js +63 -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,127 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# 🚀 Claude Code Hook - PreToolUse (Write/Edit)
|
|
4
|
+
# =============================================================================
|
|
5
|
+
#
|
|
6
|
+
# Este hook se ejecuta ANTES de que Claude Code escriba o edite archivos.
|
|
7
|
+
# Usa AST Intelligence para validar el código propuesto y BLOQUEAR
|
|
8
|
+
# si hay violaciones críticas.
|
|
9
|
+
#
|
|
10
|
+
# Configuración en ~/.config/claude-code/settings.json:
|
|
11
|
+
# {
|
|
12
|
+
# "hooks": {
|
|
13
|
+
# "PreToolUse": [
|
|
14
|
+
# {
|
|
15
|
+
# "matcher": "Write",
|
|
16
|
+
# "hooks": [{ "type": "command", "command": "/path/to/claude-code-hook.sh" }]
|
|
17
|
+
# },
|
|
18
|
+
# {
|
|
19
|
+
# "matcher": "Edit",
|
|
20
|
+
# "hooks": [{ "type": "command", "command": "/path/to/claude-code-hook.sh" }]
|
|
21
|
+
# }
|
|
22
|
+
# ]
|
|
23
|
+
# }
|
|
24
|
+
# }
|
|
25
|
+
#
|
|
26
|
+
# Exit codes:
|
|
27
|
+
# - 0: Permitir escritura
|
|
28
|
+
# - 2: BLOQUEAR escritura (violaciones críticas)
|
|
29
|
+
#
|
|
30
|
+
# Author: Pumuki Team®
|
|
31
|
+
# =============================================================================
|
|
32
|
+
|
|
33
|
+
set -e
|
|
34
|
+
|
|
35
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
36
|
+
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")"
|
|
37
|
+
LOG_FILE="$REPO_ROOT/.audit_tmp/claude-code-hook.log"
|
|
38
|
+
|
|
39
|
+
log() {
|
|
40
|
+
local level="$1"
|
|
41
|
+
local message="$2"
|
|
42
|
+
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
43
|
+
mkdir -p "$(dirname "$LOG_FILE")"
|
|
44
|
+
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
|
|
45
|
+
|
|
46
|
+
if [ -n "$DEBUG" ]; then
|
|
47
|
+
echo "[$timestamp] [$level] $message" >&2
|
|
48
|
+
fi
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Leer JSON de stdin
|
|
52
|
+
input=$(cat)
|
|
53
|
+
|
|
54
|
+
log "INFO" "Claude Code hook triggered"
|
|
55
|
+
|
|
56
|
+
# Extraer tool_name del input
|
|
57
|
+
tool_name=$(echo "$input" | jq -r '.tool_name // .tool // "unknown"' 2>/dev/null || echo "unknown")
|
|
58
|
+
log "INFO" "Tool: $tool_name"
|
|
59
|
+
|
|
60
|
+
# Solo procesar Write y Edit
|
|
61
|
+
if [[ "$tool_name" != "Write" && "$tool_name" != "Edit" ]]; then
|
|
62
|
+
log "INFO" "Skipping non-write tool: $tool_name"
|
|
63
|
+
exit 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Extraer file_path y content
|
|
67
|
+
file_path=$(echo "$input" | jq -r '.tool_input.file_path // .file_path // ""' 2>/dev/null || echo "")
|
|
68
|
+
content=$(echo "$input" | jq -r '.tool_input.content // .content // ""' 2>/dev/null || echo "")
|
|
69
|
+
|
|
70
|
+
if [ -z "$file_path" ]; then
|
|
71
|
+
log "WARN" "No file_path in input"
|
|
72
|
+
exit 0
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
log "INFO" "Analyzing: $file_path"
|
|
76
|
+
|
|
77
|
+
# Skip test files (TDD allowed)
|
|
78
|
+
if [[ "$file_path" =~ \.(spec|test)\.(js|ts|swift|kt)$ ]]; then
|
|
79
|
+
log "INFO" "TDD: Test file allowed: $file_path"
|
|
80
|
+
exit 0
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# Ejecutar análisis AST si hay contenido
|
|
84
|
+
if [ -n "$content" ]; then
|
|
85
|
+
# Llamar al analizador Node.js
|
|
86
|
+
analysis_result=$(echo "$content" | node -e "
|
|
87
|
+
const path = require('path');
|
|
88
|
+
const repoRoot = '$REPO_ROOT';
|
|
89
|
+
const filePath = '$file_path';
|
|
90
|
+
|
|
91
|
+
let input = '';
|
|
92
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
93
|
+
process.stdin.on('end', () => {
|
|
94
|
+
try {
|
|
95
|
+
const { analyzeCodeInMemory } = require(path.join(repoRoot, 'scripts/hooks-system/infrastructure/ast/ast-core'));
|
|
96
|
+
const result = analyzeCodeInMemory(input, filePath);
|
|
97
|
+
console.log(JSON.stringify(result));
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.log(JSON.stringify({ success: false, error: e.message, hasCritical: false }));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
" 2>/dev/null || echo '{"success":false,"hasCritical":false}')
|
|
103
|
+
|
|
104
|
+
has_critical=$(echo "$analysis_result" | jq -r '.hasCritical // false' 2>/dev/null || echo "false")
|
|
105
|
+
|
|
106
|
+
if [ "$has_critical" = "true" ]; then
|
|
107
|
+
violations=$(echo "$analysis_result" | jq -r '.violations[] | select(.severity == "CRITICAL") | " ❌ [\(.ruleId)] \(.message)"' 2>/dev/null || echo "Unknown violations")
|
|
108
|
+
|
|
109
|
+
log "BLOCKED" "Critical violations in $file_path"
|
|
110
|
+
|
|
111
|
+
# Salida a stderr para que Claude Code la procese
|
|
112
|
+
echo "" >&2
|
|
113
|
+
echo "🚫 AST INTELLIGENCE BLOCKED THIS WRITE" >&2
|
|
114
|
+
echo "═══════════════════════════════════════" >&2
|
|
115
|
+
echo "File: $file_path" >&2
|
|
116
|
+
echo "" >&2
|
|
117
|
+
echo "$violations" >&2
|
|
118
|
+
echo "" >&2
|
|
119
|
+
echo "Fix these violations before writing." >&2
|
|
120
|
+
echo "═══════════════════════════════════════" >&2
|
|
121
|
+
|
|
122
|
+
exit 2 # BLOQUEAR
|
|
123
|
+
fi
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
log "ALLOWED" "No critical violations in $file_path"
|
|
127
|
+
exit 0
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* =============================================================================
|
|
4
|
+
* Cascade Hook - post_write_code (Logging & Analytics)
|
|
5
|
+
* =============================================================================
|
|
6
|
+
*
|
|
7
|
+
* This hook is executed by Windsurf AFTER code is written.
|
|
8
|
+
* It logs the write operation for analytics and audit trail.
|
|
9
|
+
*
|
|
10
|
+
* Author: Pumuki Team®
|
|
11
|
+
* =============================================================================
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
|
|
17
|
+
const REPO_ROOT = (() => {
|
|
18
|
+
try {
|
|
19
|
+
const { execSync } = require('child_process');
|
|
20
|
+
return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return process.cwd();
|
|
23
|
+
}
|
|
24
|
+
})();
|
|
25
|
+
|
|
26
|
+
const LOG_FILE = path.join(REPO_ROOT, '.audit_tmp', 'cascade-writes.log');
|
|
27
|
+
|
|
28
|
+
async function main() {
|
|
29
|
+
let inputData = '';
|
|
30
|
+
|
|
31
|
+
for await (const chunk of process.stdin) {
|
|
32
|
+
inputData += chunk;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let hookInput;
|
|
36
|
+
try {
|
|
37
|
+
hookInput = JSON.parse(inputData);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { agent_action_name, tool_info, timestamp, trajectory_id } = hookInput;
|
|
43
|
+
|
|
44
|
+
if (agent_action_name !== 'post_write_code') {
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const filePath = tool_info?.file_path || 'unknown';
|
|
49
|
+
const edits = tool_info?.edits || [];
|
|
50
|
+
|
|
51
|
+
const logEntry = {
|
|
52
|
+
timestamp: timestamp || new Date().toISOString(),
|
|
53
|
+
trajectory_id,
|
|
54
|
+
file: filePath,
|
|
55
|
+
edits_count: edits.length,
|
|
56
|
+
total_chars_added: edits.reduce((sum, e) => sum + (e.new_string?.length || 0), 0),
|
|
57
|
+
total_chars_removed: edits.reduce((sum, e) => sum + (e.old_string?.length || 0), 0)
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
|
|
62
|
+
fs.appendFileSync(LOG_FILE, JSON.stringify(logEntry) + '\n');
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (process.env.DEBUG) {
|
|
65
|
+
process.stderr.write(`[post-write-hook] Log write failed: ${error.message}\n`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
main().catch(() => process.exit(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
|
+
});
|