pumuki-ast-hooks 5.3.26 → 5.3.28
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 +2 -2
- package/scripts/hooks-system/application/services/installation/GitEnvironmentService.js +10 -18
- package/scripts/hooks-system/application/services/logging/UnifiedLogger.js +1 -1
- package/scripts/hooks-system/application/services/monitoring/EvidenceMonitor.js +102 -1
- package/scripts/hooks-system/application/services/notification/NotificationCenterService.js +1 -1
- package/scripts/hooks-system/bin/kill-mcp-zombies.sh +6 -2
- package/scripts/hooks-system/domain/exceptions/index.js +6 -3
- package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +2 -2
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseAnalyzer.js +3 -0
- package/scripts/hooks-system/infrastructure/ast/ios/ast-ios.js +2 -2
- package/scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js +154 -132
- package/scripts/hooks-system/infrastructure/repositories/datasources/CursorApiDataSource.js +45 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki-ast-hooks",
|
|
3
|
-
"version": "5.3.
|
|
3
|
+
"version": "5.3.28",
|
|
4
4
|
"description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -123,4 +123,4 @@
|
|
|
123
123
|
"./skills": "./skills/skill-rules.json",
|
|
124
124
|
"./hooks": "./hooks/index.js"
|
|
125
125
|
}
|
|
126
|
-
}
|
|
126
|
+
}
|
|
@@ -117,27 +117,19 @@ if [ -f "node_modules/.bin/ast-hooks" ]; then
|
|
|
117
117
|
exit $EXIT_CODE
|
|
118
118
|
fi
|
|
119
119
|
# Block commits only when there are actual CRITICAL/HIGH violations.
|
|
120
|
-
#
|
|
121
|
-
|
|
120
|
+
# Source of truth is the AST SUMMARY LEVELS line.
|
|
121
|
+
SUMMARY_LINE=$(echo "$OUTPUT" | grep -E "^AST SUMMARY LEVELS:" | tail -1)
|
|
122
|
+
CRITICAL_COUNT=$(echo "$SUMMARY_LINE" | grep -oE "CRITICAL=[0-9]+" | head -1 | cut -d= -f2)
|
|
123
|
+
HIGH_COUNT=$(echo "$SUMMARY_LINE" | grep -oE "HIGH=[0-9]+" | head -1 | cut -d= -f2)
|
|
124
|
+
CRITICAL_COUNT=$(printf '%s' "$CRITICAL_COUNT" | tr -cd '0-9')
|
|
125
|
+
HIGH_COUNT=$(printf '%s' "$HIGH_COUNT" | tr -cd '0-9')
|
|
126
|
+
[ -z "$CRITICAL_COUNT" ] && CRITICAL_COUNT=0
|
|
127
|
+
[ -z "$HIGH_COUNT" ] && HIGH_COUNT=0
|
|
128
|
+
|
|
129
|
+
if [ "$CRITICAL_COUNT" -gt 0 ] || [ "$HIGH_COUNT" -gt 0 ]; then
|
|
122
130
|
echo ""
|
|
123
131
|
echo "❌ Commit blocked: Critical or High violations detected in staged files"
|
|
124
132
|
|
|
125
|
-
# Extract counts (best-effort) for notification.
|
|
126
|
-
# Prefer explicit totals (CRITICAL=, HIGH=) and fallback to tag counts.
|
|
127
|
-
CRITICAL_COUNT=$(echo "$OUTPUT" | grep -oE "CRITICAL=[0-9]+" | head -1 | cut -d= -f2)
|
|
128
|
-
HIGH_COUNT=$(echo "$OUTPUT" | grep -oE "HIGH=[0-9]+" | head -1 | cut -d= -f2)
|
|
129
|
-
|
|
130
|
-
if [ -z "$CRITICAL_COUNT" ]; then
|
|
131
|
-
CRITICAL_COUNT=$(echo "$OUTPUT" | grep -oE "\[CRITICAL\]" | wc -l | tr -d ' ')
|
|
132
|
-
fi
|
|
133
|
-
if [ -z "$HIGH_COUNT" ]; then
|
|
134
|
-
HIGH_COUNT=$(echo "$OUTPUT" | grep -oE "\[HIGH\]" | wc -l | tr -d ' ')
|
|
135
|
-
fi
|
|
136
|
-
|
|
137
|
-
CRITICAL_COUNT=$(printf '%s' "$CRITICAL_COUNT" | tr -cd '0-9')
|
|
138
|
-
HIGH_COUNT=$(printf '%s' "$HIGH_COUNT" | tr -cd '0-9')
|
|
139
|
-
[ -z "$CRITICAL_COUNT" ] && CRITICAL_COUNT=0
|
|
140
|
-
[ -z "$HIGH_COUNT" ] && HIGH_COUNT=0
|
|
141
133
|
TOTAL_VIOLATIONS=$((CRITICAL_COUNT + HIGH_COUNT))
|
|
142
134
|
[ "$TOTAL_VIOLATIONS" -le 0 ] && TOTAL_VIOLATIONS=1
|
|
143
135
|
|
|
@@ -38,7 +38,7 @@ class UnifiedLogger {
|
|
|
38
38
|
try {
|
|
39
39
|
fs.mkdirSync(dir, { recursive: true });
|
|
40
40
|
} catch (error) {
|
|
41
|
-
|
|
41
|
+
console.warn(`[UnifiedLogger] Failed to create directory ${dir}:`, error.message);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
}
|
|
@@ -14,6 +14,80 @@ class EvidenceMonitor {
|
|
|
14
14
|
this.evidencePath = path.join(repoRoot, '.AI_EVIDENCE.json');
|
|
15
15
|
this.tempDir = path.join(repoRoot, '.audit_tmp');
|
|
16
16
|
this.updateScript = this.resolveUpdateEvidenceScript();
|
|
17
|
+
this.refreshInFlight = false;
|
|
18
|
+
this.refreshTimeoutMs = options.refreshTimeoutMs || 120000;
|
|
19
|
+
this.refreshLockFile = path.join(this.tempDir, 'evidence-refresh.lock');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
isPidRunning(pid) {
|
|
23
|
+
if (!pid || !Number.isFinite(pid) || pid <= 0) return false;
|
|
24
|
+
try {
|
|
25
|
+
process.kill(pid, 0);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
acquireRefreshLock() {
|
|
33
|
+
try {
|
|
34
|
+
fs.mkdirSync(this.tempDir, { recursive: true });
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.warn('[EvidenceMonitor] Failed to ensure temp dir:', error.message);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const fd = fs.openSync(this.refreshLockFile, 'wx');
|
|
41
|
+
const payload = JSON.stringify({ pid: process.pid, timestamp: new Date().toISOString() });
|
|
42
|
+
fs.writeFileSync(fd, payload, { encoding: 'utf8' });
|
|
43
|
+
fs.closeSync(fd);
|
|
44
|
+
return { acquired: true };
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error && error.code !== 'EEXIST') {
|
|
47
|
+
return { acquired: false, reason: 'error', error };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const raw = String(fs.readFileSync(this.refreshLockFile, 'utf8') || '').trim();
|
|
52
|
+
const data = raw ? JSON.parse(raw) : null;
|
|
53
|
+
const lockPid = data && Number(data.pid);
|
|
54
|
+
if (lockPid && this.isPidRunning(lockPid)) {
|
|
55
|
+
return { acquired: false, reason: 'locked', pid: lockPid };
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.warn('[EvidenceMonitor] Failed to read refresh lock file:', error.message);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
fs.unlinkSync(this.refreshLockFile);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.warn('[EvidenceMonitor] Failed to remove stale refresh lock:', error.message);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const fd = fs.openSync(this.refreshLockFile, 'wx');
|
|
69
|
+
const payload = JSON.stringify({ pid: process.pid, timestamp: new Date().toISOString() });
|
|
70
|
+
fs.writeFileSync(fd, payload, { encoding: 'utf8' });
|
|
71
|
+
fs.closeSync(fd);
|
|
72
|
+
return { acquired: true };
|
|
73
|
+
} catch (retryError) {
|
|
74
|
+
return { acquired: false, reason: 'locked', error: retryError };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
releaseRefreshLock() {
|
|
80
|
+
try {
|
|
81
|
+
if (!fs.existsSync(this.refreshLockFile)) return;
|
|
82
|
+
const raw = String(fs.readFileSync(this.refreshLockFile, 'utf8') || '').trim();
|
|
83
|
+
const data = raw ? JSON.parse(raw) : null;
|
|
84
|
+
const lockPid = data && Number(data.pid);
|
|
85
|
+
if (lockPid === process.pid) {
|
|
86
|
+
fs.unlinkSync(this.refreshLockFile);
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.warn('[EvidenceMonitor] Failed to release refresh lock:', error.message);
|
|
90
|
+
}
|
|
17
91
|
}
|
|
18
92
|
|
|
19
93
|
resolveUpdateEvidenceScript() {
|
|
@@ -51,6 +125,17 @@ class EvidenceMonitor {
|
|
|
51
125
|
throw new ConfigurationError('Update evidence script not found', 'updateScript');
|
|
52
126
|
}
|
|
53
127
|
|
|
128
|
+
if (this.refreshInFlight) {
|
|
129
|
+
return '';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const lock = this.acquireRefreshLock();
|
|
133
|
+
if (!lock.acquired) {
|
|
134
|
+
return '';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.refreshInFlight = true;
|
|
138
|
+
|
|
54
139
|
return new Promise((resolve, reject) => {
|
|
55
140
|
const child = require('child_process').spawn('bash', [this.updateScript, '--auto', '--refresh-only'], {
|
|
56
141
|
cwd: this.repoRoot,
|
|
@@ -62,7 +147,18 @@ class EvidenceMonitor {
|
|
|
62
147
|
output += data.toString();
|
|
63
148
|
});
|
|
64
149
|
|
|
150
|
+
const timeoutId = setTimeout(() => {
|
|
151
|
+
try {
|
|
152
|
+
child.kill('SIGKILL');
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.warn('[EvidenceMonitor] Failed to kill timed-out refresh process:', error.message);
|
|
155
|
+
}
|
|
156
|
+
}, this.refreshTimeoutMs);
|
|
157
|
+
|
|
65
158
|
child.on('close', (code) => {
|
|
159
|
+
clearTimeout(timeoutId);
|
|
160
|
+
this.refreshInFlight = false;
|
|
161
|
+
this.releaseRefreshLock();
|
|
66
162
|
if (code === 0) {
|
|
67
163
|
resolve(output);
|
|
68
164
|
} else {
|
|
@@ -70,7 +166,12 @@ class EvidenceMonitor {
|
|
|
70
166
|
}
|
|
71
167
|
});
|
|
72
168
|
|
|
73
|
-
child.on('error',
|
|
169
|
+
child.on('error', (err) => {
|
|
170
|
+
clearTimeout(timeoutId);
|
|
171
|
+
this.refreshInFlight = false;
|
|
172
|
+
this.releaseRefreshLock();
|
|
173
|
+
reject(err);
|
|
174
|
+
});
|
|
74
175
|
});
|
|
75
176
|
}
|
|
76
177
|
|
|
@@ -31,7 +31,7 @@ class NotificationCenterService {
|
|
|
31
31
|
try {
|
|
32
32
|
fs.mkdirSync(path.dirname(defaultLogPath), { recursive: true });
|
|
33
33
|
} catch (e) {
|
|
34
|
-
|
|
34
|
+
console.warn(`[NotificationCenter] Failed to create log directory:`, e.message);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
this.logger = config.logger || new UnifiedLogger({
|
|
@@ -16,8 +16,12 @@ NC='\033[0m'
|
|
|
16
16
|
echo -e "${YELLOW}🧟 Searching for zombie MCP processes...${NC}"
|
|
17
17
|
echo ""
|
|
18
18
|
|
|
19
|
-
# Find all
|
|
20
|
-
PIDS=$(
|
|
19
|
+
# Find all MCP-related processes
|
|
20
|
+
PIDS=$( (
|
|
21
|
+
pgrep -f "mcp-ai-evidence-watcher" || true
|
|
22
|
+
pgrep -f "scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation\.js" || true
|
|
23
|
+
pgrep -f "update-evidence\.sh.*--auto.*--refresh-only" || true
|
|
24
|
+
) | sort -u || true )
|
|
21
25
|
|
|
22
26
|
if [[ -z "$PIDS" ]]; then
|
|
23
27
|
echo -e "${GREEN}✅ No zombie processes found!${NC}"
|
|
@@ -20,15 +20,18 @@ class BaseException extends Error {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
toJSON() {
|
|
23
|
-
|
|
23
|
+
const result = {
|
|
24
24
|
name: this.name,
|
|
25
25
|
message: this.message,
|
|
26
26
|
code: this.code,
|
|
27
27
|
statusCode: this.statusCode,
|
|
28
28
|
timestamp: this.timestamp,
|
|
29
|
-
context: this.context
|
|
30
|
-
stack: this.stack
|
|
29
|
+
context: this.context
|
|
31
30
|
};
|
|
31
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
32
|
+
result.stack = this.stack;
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
|
|
@@ -504,7 +504,7 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
504
504
|
|
|
505
505
|
sf.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
|
|
506
506
|
const name = cls.getName();
|
|
507
|
-
if (name && /Dto|DTO|Request|Response/.test(name)) {
|
|
507
|
+
if (name && /Dto|DTO|Request|Response/.test(name) && !/Exception|Error/.test(name)) {
|
|
508
508
|
const hasValidation = sf.getFullText().includes("@IsString") ||
|
|
509
509
|
sf.getFullText().includes("@IsEmail") ||
|
|
510
510
|
sf.getFullText().includes("@IsNotEmpty") ||
|
|
@@ -1604,7 +1604,7 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
1604
1604
|
if (fullText.includes('extends HttpException') || fullText.includes('extends Error')) {
|
|
1605
1605
|
const hasFilter = fullText.includes('@Catch(');
|
|
1606
1606
|
|
|
1607
|
-
if (!hasFilter && filePath.includes('/exceptions/')) {
|
|
1607
|
+
if (!hasFilter && filePath.includes('/exceptions/') && !fullText.includes('class ') && !fullText.includes('extends BaseException')) {
|
|
1608
1608
|
pushFinding(
|
|
1609
1609
|
"backend.error_handling.missing_exception_filter",
|
|
1610
1610
|
"high",
|
|
@@ -269,6 +269,9 @@ class iOSEnterpriseAnalyzer {
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
async analyzeNetworking(content, filePath) {
|
|
272
|
+
if (String(filePath || '').endsWith('/Package.swift') || String(filePath || '').endsWith('Package.swift')) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
272
275
|
if (!content.includes('URLSession') && !content.includes('Alamofire')) {
|
|
273
276
|
if (content.includes('http://') || content.includes('https://')) {
|
|
274
277
|
this.addFinding('ios.networking.missing_urlsession', 'high', filePath, 1,
|
|
@@ -1250,7 +1250,7 @@ async function runIOSIntelligence(project, findings, platform) {
|
|
|
1250
1250
|
);
|
|
1251
1251
|
}
|
|
1252
1252
|
|
|
1253
|
-
if (filePath.endsWith('.swift') && !filePath.includes('/Domain/') && !filePath.includes('/Application/') &&
|
|
1253
|
+
if (filePath.endsWith('.swift') && !filePath.endsWith('/Package.swift') && !filePath.endsWith('Package.swift') && !filePath.includes('/Domain/') && !filePath.includes('/Application/') &&
|
|
1254
1254
|
!filePath.includes('/Infrastructure/') && !filePath.includes('/Presentation/') &&
|
|
1255
1255
|
!filePath.includes('/Tests/') && !filePath.includes('AppDelegate')) {
|
|
1256
1256
|
pushFinding(
|
|
@@ -1803,7 +1803,7 @@ async function runIOSIntelligence(project, findings, platform) {
|
|
|
1803
1803
|
);
|
|
1804
1804
|
}
|
|
1805
1805
|
|
|
1806
|
-
if (content.includes('Keychain') && !content.includes('SecItemAdd') && !content.includes('KeychainSwift')) {
|
|
1806
|
+
if (!filePath.includes('Package.swift') && content.includes('Keychain') && !content.includes('SecItemAdd') && !content.includes('KeychainSwift')) {
|
|
1807
1807
|
pushFinding(
|
|
1808
1808
|
"ios.security.keychain_usage",
|
|
1809
1809
|
"low",
|
|
@@ -56,6 +56,9 @@ const REPO_ROOT = resolveRepoRoot();
|
|
|
56
56
|
const MCP_LOCK_DIR = path.join(REPO_ROOT, '.audit_tmp', 'mcp-singleton.lock');
|
|
57
57
|
const MCP_LOCK_PID = path.join(MCP_LOCK_DIR, 'pid');
|
|
58
58
|
|
|
59
|
+
let MCP_IS_PRIMARY = true;
|
|
60
|
+
let MCP_PRIMARY_PID = null;
|
|
61
|
+
|
|
59
62
|
function isPidRunning(pid) {
|
|
60
63
|
if (!pid || !Number.isFinite(pid) || pid <= 0) return false;
|
|
61
64
|
try {
|
|
@@ -107,8 +110,10 @@ function acquireSingletonLock() {
|
|
|
107
110
|
} catch (error) {
|
|
108
111
|
const existingPid = safeReadPid(MCP_LOCK_PID);
|
|
109
112
|
if (existingPid && isPidRunning(existingPid)) {
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
MCP_IS_PRIMARY = false;
|
|
114
|
+
MCP_PRIMARY_PID = existingPid;
|
|
115
|
+
process.stderr.write(`[MCP] Another instance is already running (pid ${existingPid}). Secondary mode enabled.\n`);
|
|
116
|
+
return { acquired: false, pid: existingPid };
|
|
112
117
|
}
|
|
113
118
|
|
|
114
119
|
removeLockDir();
|
|
@@ -137,6 +142,8 @@ function acquireSingletonLock() {
|
|
|
137
142
|
cleanup();
|
|
138
143
|
process.exit(0);
|
|
139
144
|
});
|
|
145
|
+
|
|
146
|
+
return { acquired: true, pid: process.pid };
|
|
140
147
|
}
|
|
141
148
|
|
|
142
149
|
acquireSingletonLock();
|
|
@@ -760,6 +767,17 @@ async function handleMcpMessage(message) {
|
|
|
760
767
|
try {
|
|
761
768
|
const request = JSON.parse(message);
|
|
762
769
|
|
|
770
|
+
if (!MCP_IS_PRIMARY && request.method !== 'initialize' && request.method !== 'resources/list' && request.method !== 'resources/read' && request.method !== 'tools/list') {
|
|
771
|
+
return {
|
|
772
|
+
jsonrpc: '2.0',
|
|
773
|
+
id: request.id,
|
|
774
|
+
error: {
|
|
775
|
+
code: -32603,
|
|
776
|
+
message: `MCP instance already running (pid ${MCP_PRIMARY_PID || 'unknown'}). Please restart the IDE or kill the running instance.`
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
763
781
|
if ((typeof request.id === 'undefined' || request.id === null) && request.method?.startsWith('notifications/')) {
|
|
764
782
|
return null;
|
|
765
783
|
}
|
|
@@ -1054,163 +1072,167 @@ protocolHandler.start(handleMcpMessage);
|
|
|
1054
1072
|
/**
|
|
1055
1073
|
* Polling loop for background notifications and automations
|
|
1056
1074
|
*/
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
if (
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
// 2. Evidence Freshness Guard
|
|
1086
|
-
if (evidenceMonitor.isStale() && (now - lastEvidenceNotification > NOTIFICATION_COOLDOWN)) {
|
|
1087
|
-
try {
|
|
1088
|
-
await evidenceMonitor.refresh();
|
|
1089
|
-
sendNotification('🔄 Evidence Auto-Updated', 'AI Evidence has been refreshed automatically', 'Purr');
|
|
1090
|
-
} catch (err) {
|
|
1091
|
-
sendNotification('⚠️ Evidence Stale', `Failed to auto-refresh evidence: ${err.message}`, 'Basso');
|
|
1075
|
+
if (MCP_IS_PRIMARY) {
|
|
1076
|
+
setInterval(async () => {
|
|
1077
|
+
try {
|
|
1078
|
+
const now = Date.now();
|
|
1079
|
+
const gitFlowService = getCompositionRoot().getGitFlowService();
|
|
1080
|
+
const gitQuery = getCompositionRoot().getGitQueryAdapter();
|
|
1081
|
+
const evidenceMonitor = getCompositionRoot().getEvidenceMonitor();
|
|
1082
|
+
const orchestrator = getCompositionRoot().getOrchestrator();
|
|
1083
|
+
|
|
1084
|
+
const currentBranch = gitFlowService.getCurrentBranch();
|
|
1085
|
+
const baseBranch = process.env.AST_BASE_BRANCH || 'develop';
|
|
1086
|
+
const isProtectedBranch = ['main', 'master', baseBranch].includes(currentBranch);
|
|
1087
|
+
|
|
1088
|
+
const uncommittedChanges = gitQuery.getUncommittedChanges();
|
|
1089
|
+
const hasUncommittedChanges = uncommittedChanges && uncommittedChanges.length > 0;
|
|
1090
|
+
|
|
1091
|
+
// 1. Protected Branch Guard
|
|
1092
|
+
if (isProtectedBranch && hasUncommittedChanges) {
|
|
1093
|
+
if (now - lastGitFlowNotification > NOTIFICATION_COOLDOWN) {
|
|
1094
|
+
const state = gitQuery.getBranchState(currentBranch);
|
|
1095
|
+
sendNotification(
|
|
1096
|
+
'⚠️ Git Flow Violation',
|
|
1097
|
+
`branch=${currentBranch} changes detected on protected branch. Create a feature branch.`,
|
|
1098
|
+
'Basso'
|
|
1099
|
+
);
|
|
1100
|
+
lastGitFlowNotification = now;
|
|
1101
|
+
}
|
|
1092
1102
|
}
|
|
1093
|
-
lastEvidenceNotification = now;
|
|
1094
|
-
}
|
|
1095
1103
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
const decision = await orchestrator.analyzeContext();
|
|
1099
|
-
if (decision.action === 'auto-execute' && decision.platforms.length > 0) {
|
|
1104
|
+
// 2. Evidence Freshness Guard
|
|
1105
|
+
if (evidenceMonitor.isStale() && (now - lastEvidenceNotification > NOTIFICATION_COOLDOWN)) {
|
|
1100
1106
|
try {
|
|
1101
1107
|
await evidenceMonitor.refresh();
|
|
1102
|
-
sendNotification('
|
|
1103
|
-
} catch (
|
|
1104
|
-
sendNotification('
|
|
1108
|
+
sendNotification('🔄 Evidence Auto-Updated', 'AI Evidence has been refreshed automatically', 'Purr');
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
sendNotification('⚠️ Evidence Stale', `Failed to auto-refresh evidence: ${err.message}`, 'Basso');
|
|
1105
1111
|
}
|
|
1112
|
+
lastEvidenceNotification = now;
|
|
1106
1113
|
}
|
|
1107
|
-
}
|
|
1108
1114
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1115
|
+
// 3. Autonomous Orchestration
|
|
1116
|
+
if (orchestrator.shouldReanalyze()) {
|
|
1117
|
+
const decision = await orchestrator.analyzeContext();
|
|
1118
|
+
if (decision.action === 'auto-execute' && decision.platforms.length > 0) {
|
|
1119
|
+
try {
|
|
1120
|
+
await evidenceMonitor.refresh();
|
|
1121
|
+
sendNotification('✅ AI Start Executed', `Platforms: ${decision.platforms.map(p => p.platform.toUpperCase()).join(', ')}`, 'Glass');
|
|
1122
|
+
} catch (e) {
|
|
1123
|
+
sendNotification('❌ AI Start Error', `Failed to execute: ${e.message}`, 'Basso');
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1113
1127
|
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
if (process.env.DEBUG) console.error('[MCP] Polling loop error:', error);
|
|
1130
|
+
}
|
|
1131
|
+
}, 30000);
|
|
1132
|
+
}
|
|
1119
1133
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1134
|
+
// AUTO-COMMIT: Only for project code changes (no node_modules, no library)
|
|
1135
|
+
if (MCP_IS_PRIMARY) {
|
|
1136
|
+
setInterval(async () => {
|
|
1137
|
+
if (!AUTO_COMMIT_ENABLED) {
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1122
1140
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
const gitQuery = getCompositionRoot().getGitQueryAdapter();
|
|
1126
|
-
const gitCommand = getCompositionRoot().getGitCommandAdapter();
|
|
1141
|
+
const now = Date.now();
|
|
1142
|
+
if (now - lastAutoCommitTime < AUTO_COMMIT_INTERVAL) return;
|
|
1127
1143
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1144
|
+
try {
|
|
1145
|
+
const gitFlowService = getCompositionRoot().getGitFlowService();
|
|
1146
|
+
const gitQuery = getCompositionRoot().getGitQueryAdapter();
|
|
1147
|
+
const gitCommand = getCompositionRoot().getGitCommandAdapter();
|
|
1130
1148
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
}
|
|
1149
|
+
const currentBranch = gitFlowService.getCurrentBranch();
|
|
1150
|
+
const isFeatureBranch = currentBranch.match(/^(feature|fix|hotfix)\//);
|
|
1134
1151
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1152
|
+
if (!isFeatureBranch) {
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1138
1155
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
// Detect library installation path
|
|
1143
|
-
const libraryPath = getLibraryInstallPath();
|
|
1144
|
-
|
|
1145
|
-
// Filter changes: project code only
|
|
1146
|
-
const filesToCommit = uncommittedChanges.filter(file => {
|
|
1147
|
-
// Exclude noise
|
|
1148
|
-
if (file.startsWith('node_modules/') ||
|
|
1149
|
-
file.includes('package-lock.json') ||
|
|
1150
|
-
file.startsWith('.git/') ||
|
|
1151
|
-
file.startsWith('.cursor/') ||
|
|
1152
|
-
file.startsWith('.ast-intelligence/') ||
|
|
1153
|
-
file.startsWith('.vscode/') ||
|
|
1154
|
-
file.startsWith('.idea/')) {
|
|
1155
|
-
return false;
|
|
1156
|
+
if (gitFlowService.isClean()) {
|
|
1157
|
+
return;
|
|
1156
1158
|
}
|
|
1157
1159
|
|
|
1158
|
-
//
|
|
1159
|
-
|
|
1160
|
-
|
|
1160
|
+
// Get uncommitted changes
|
|
1161
|
+
const uncommittedChanges = gitQuery.getUncommittedChanges();
|
|
1162
|
+
|
|
1163
|
+
// Detect library installation path
|
|
1164
|
+
const libraryPath = getLibraryInstallPath();
|
|
1165
|
+
|
|
1166
|
+
// Filter changes: project code only
|
|
1167
|
+
const filesToCommit = uncommittedChanges.filter(file => {
|
|
1168
|
+
// Exclude noise
|
|
1169
|
+
if (file.startsWith('node_modules/') ||
|
|
1170
|
+
file.includes('package-lock.json') ||
|
|
1171
|
+
file.startsWith('.git/') ||
|
|
1172
|
+
file.startsWith('.cursor/') ||
|
|
1173
|
+
file.startsWith('.ast-intelligence/') ||
|
|
1174
|
+
file.startsWith('.vscode/') ||
|
|
1175
|
+
file.startsWith('.idea/')) {
|
|
1176
|
+
return false;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// Exclude library itself
|
|
1180
|
+
if (libraryPath && file.startsWith(libraryPath + '/')) {
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Code/Doc files only
|
|
1185
|
+
const codeExtensions = ['.ts', '.tsx', '.js', '.jsx', '.swift', '.kt', '.py', '.java', '.go', '.rs', '.md', '.json', '.yaml', '.yml'];
|
|
1186
|
+
return codeExtensions.some(ext => file.endsWith(ext));
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
if (filesToCommit.length === 0) {
|
|
1190
|
+
return;
|
|
1161
1191
|
}
|
|
1162
1192
|
|
|
1163
|
-
//
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1193
|
+
// Stage files
|
|
1194
|
+
filesToCommit.forEach(file => {
|
|
1195
|
+
gitCommand.add(file);
|
|
1196
|
+
});
|
|
1167
1197
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1198
|
+
const branchType = currentBranch.split('/')[0];
|
|
1199
|
+
const branchName = currentBranch.split('/').slice(1).join('/');
|
|
1200
|
+
const commitMessage = `${branchType}(auto): ${branchName} - ${filesToCommit.length} files`;
|
|
1171
1201
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
gitCommand.add(file);
|
|
1175
|
-
});
|
|
1202
|
+
// Commit
|
|
1203
|
+
gitCommand.commit(commitMessage);
|
|
1176
1204
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
const commitMessage = `${branchType}(auto): ${branchName} - ${filesToCommit.length} files`;
|
|
1205
|
+
sendNotification('✅ Auto-Commit', `${filesToCommit.length} files in ${currentBranch}`, 'Purr');
|
|
1206
|
+
lastAutoCommitTime = now;
|
|
1180
1207
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1208
|
+
if (AUTO_PUSH_ENABLED) {
|
|
1209
|
+
if (gitFlowService.isGitHubAvailable()) {
|
|
1210
|
+
try {
|
|
1211
|
+
gitCommand.push('origin', currentBranch);
|
|
1212
|
+
sendNotification('✅ Auto-Push', `Pushed to origin/${currentBranch}`, 'Glass');
|
|
1183
1213
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1214
|
+
if (AUTO_PR_ENABLED) {
|
|
1215
|
+
const baseBranch = process.env.AST_BASE_BRANCH || 'develop';
|
|
1216
|
+
const branchState = gitQuery.getBranchState(currentBranch);
|
|
1186
1217
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
if (AUTO_PR_ENABLED) {
|
|
1194
|
-
const baseBranch = process.env.AST_BASE_BRANCH || 'develop';
|
|
1195
|
-
const branchState = gitQuery.getBranchState(currentBranch);
|
|
1196
|
-
|
|
1197
|
-
if (branchState.ahead >= 3) {
|
|
1198
|
-
const prTitle = `Auto-PR: ${branchName}`;
|
|
1199
|
-
const prUrl = gitFlowService.createPullRequest(currentBranch, baseBranch, prTitle, 'Automated PR by Pumuki Git Flow');
|
|
1200
|
-
if (prUrl) {
|
|
1201
|
-
sendNotification('✅ Auto-PR Created', prTitle, 'Hero');
|
|
1218
|
+
if (branchState.ahead >= 3) {
|
|
1219
|
+
const prTitle = `Auto-PR: ${branchName}`;
|
|
1220
|
+
const prUrl = gitFlowService.createPullRequest(currentBranch, baseBranch, prTitle, 'Automated PR by Pumuki Git Flow');
|
|
1221
|
+
if (prUrl) {
|
|
1222
|
+
sendNotification('✅ Auto-PR Created', prTitle, 'Hero');
|
|
1223
|
+
}
|
|
1202
1224
|
}
|
|
1203
1225
|
}
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1226
|
+
} catch (e) {
|
|
1227
|
+
if (!e.message.includes('No remote')) {
|
|
1228
|
+
sendNotification('⚠️ Auto-Push Failed', 'Push manual required', 'Basso');
|
|
1229
|
+
}
|
|
1208
1230
|
}
|
|
1209
1231
|
}
|
|
1210
1232
|
}
|
|
1211
|
-
}
|
|
1212
1233
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
}, AUTO_COMMIT_INTERVAL);
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
if (process.env.DEBUG) console.error('[MCP] Auto-commit error:', error);
|
|
1236
|
+
}
|
|
1237
|
+
}, AUTO_COMMIT_INTERVAL);
|
|
1238
|
+
}
|
|
@@ -11,6 +11,42 @@ class CursorApiDataSource {
|
|
|
11
11
|
this.apiToken = apiToken;
|
|
12
12
|
this.fetch = fetchImpl;
|
|
13
13
|
this.logger = logger;
|
|
14
|
+
this.failureCount = 0;
|
|
15
|
+
this.failureThreshold = 5;
|
|
16
|
+
this.circuitOpenUntil = null;
|
|
17
|
+
this.circuitResetTimeoutMs = 60000;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
isCircuitOpen() {
|
|
21
|
+
if (!this.circuitOpenUntil) return false;
|
|
22
|
+
if (Date.now() >= this.circuitOpenUntil) {
|
|
23
|
+
this.circuitOpenUntil = null;
|
|
24
|
+
this.failureCount = 0;
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
recordFailure() {
|
|
31
|
+
this.failureCount++;
|
|
32
|
+
if (this.failureCount >= this.failureThreshold) {
|
|
33
|
+
this.circuitOpenUntil = Date.now() + this.circuitResetTimeoutMs;
|
|
34
|
+
if (this.logger && this.logger.warn) {
|
|
35
|
+
this.logger.warn('CURSOR_API_CIRCUIT_BREAKER_OPEN', {
|
|
36
|
+
failureCount: this.failureCount,
|
|
37
|
+
resetTimeoutMs: this.circuitResetTimeoutMs
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
recordSuccess() {
|
|
44
|
+
this.failureCount = 0;
|
|
45
|
+
this.circuitOpenUntil = null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
circuitBreaker() {
|
|
49
|
+
return this.isCircuitOpen();
|
|
14
50
|
}
|
|
15
51
|
|
|
16
52
|
async fetchUsage(maxRetries = 3, initialDelayMs = 1000) {
|
|
@@ -18,6 +54,13 @@ class CursorApiDataSource {
|
|
|
18
54
|
return null;
|
|
19
55
|
}
|
|
20
56
|
|
|
57
|
+
if (this.isCircuitOpen()) {
|
|
58
|
+
if (this.logger && this.logger.warn) {
|
|
59
|
+
this.logger.warn('CURSOR_API_CIRCUIT_BREAKER_OPEN', { message: 'Circuit is open, skipping request' });
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
21
64
|
const retryPolicy = { maxAttempts: maxRetries, backoff: 'exponential' };
|
|
22
65
|
const requestTimeoutMs = 30000;
|
|
23
66
|
|
|
@@ -43,6 +86,7 @@ class CursorApiDataSource {
|
|
|
43
86
|
return null;
|
|
44
87
|
}
|
|
45
88
|
|
|
89
|
+
this.recordSuccess();
|
|
46
90
|
return payload;
|
|
47
91
|
} catch (fetchError) {
|
|
48
92
|
if (timeoutId) clearTimeout(timeoutId);
|
|
@@ -54,6 +98,7 @@ class CursorApiDataSource {
|
|
|
54
98
|
} catch (error) {
|
|
55
99
|
const isLastAttempt = attempt === retryPolicy.maxAttempts;
|
|
56
100
|
if (isLastAttempt) {
|
|
101
|
+
this.recordFailure();
|
|
57
102
|
if (this.logger && this.logger.warn) {
|
|
58
103
|
this.logger.warn('CURSOR_API_DATASOURCE_FAILED', { error: error.message, attempts: attempt + 1 });
|
|
59
104
|
}
|