pumuki-ast-hooks 5.3.27 → 5.3.29

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki-ast-hooks",
3
- "version": "5.3.27",
3
+ "version": "5.3.29",
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": {
@@ -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
- # Do NOT block on summary lines like: "CRITICAL=0 HIGH=0".
121
- if echo "$OUTPUT" | grep -qE "CRITICAL=[1-9]|HIGH=[1-9]|\[CRITICAL\]|\[HIGH\]"; then
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
- // Directory creation might fail if race condition, can be ignored as next write will retry or fail
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', reject);
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
- // Ignore if it already exists or permissions issues during init
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 mcp-ai-evidence-watcher processes
20
- PIDS=$(pgrep -f "mcp-ai-evidence-watcher" || true)
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
- return {
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
 
@@ -368,18 +368,36 @@ function generateOutput(findings, context, project, root) {
368
368
 
369
369
  // Top violations
370
370
  const grouped = {};
371
+ const levelRank = { LOW: 1, MEDIUM: 2, HIGH: 3, CRITICAL: 4 };
372
+ const emojiForLevel = (level) => (level === 'CRITICAL' || level === 'HIGH' ? '🔴' : '🔵');
373
+
371
374
  findings.forEach(f => {
372
- grouped[f.ruleId] = (grouped[f.ruleId] || 0) + 1;
375
+ if (!f || !f.ruleId) return;
376
+ const level = mapToLevel(f.severity);
377
+ if (!grouped[f.ruleId]) {
378
+ grouped[f.ruleId] = { count: 0, worstLevel: level };
379
+ }
380
+ grouped[f.ruleId].count += 1;
381
+ if ((levelRank[level] || 0) > (levelRank[grouped[f.ruleId].worstLevel] || 0)) {
382
+ grouped[f.ruleId].worstLevel = level;
383
+ }
384
+ });
385
+
386
+ const entries = Object.entries(grouped)
387
+ .map(([ruleId, data]) => ({ ruleId, count: data.count, worstLevel: data.worstLevel }))
388
+ .sort((a, b) => b.count - a.count);
389
+
390
+ const blockers = entries.filter(e => e.worstLevel === 'CRITICAL' || e.worstLevel === 'HIGH');
391
+ const nonBlockers = entries.filter(e => e.worstLevel !== 'CRITICAL' && e.worstLevel !== 'HIGH');
392
+
393
+ blockers.forEach(({ ruleId, count, worstLevel }) => {
394
+ console.error(`${emojiForLevel(worstLevel)} ${ruleId} - ${count} violations`);
373
395
  });
374
396
 
375
- Object.entries(grouped)
376
- .sort((a, b) => b[1] - a[1])
377
- .slice(0, 20)
378
- .forEach(([ruleId, count]) => {
379
- const severity = ruleId.includes("types.any") || ruleId.includes("security.") || ruleId.includes("architecture.") ? "error" :
380
- ruleId.includes("performance.") || ruleId.includes("debug.") ? "warning" : "info";
381
- const emoji = severity === "error" ? "🔴" : severity === "warning" ? "🟡" : "🔵";
382
- console.error(`${emoji} ${ruleId} - ${count} violations`);
397
+ nonBlockers
398
+ .slice(0, Math.max(0, 20 - blockers.length))
399
+ .forEach(({ ruleId, count, worstLevel }) => {
400
+ console.error(`${emojiForLevel(worstLevel)} ${ruleId} - ${count} violations`);
383
401
  });
384
402
 
385
403
  // Summary
@@ -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",
@@ -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
- process.stderr.write(`[MCP] Another instance is already running (pid ${existingPid}). Exiting.\n`);
111
- process.exit(0);
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
- setInterval(async () => {
1058
- try {
1059
- const now = Date.now();
1060
- const gitFlowService = getCompositionRoot().getGitFlowService();
1061
- const gitQuery = getCompositionRoot().getGitQueryAdapter();
1062
- const evidenceMonitor = getCompositionRoot().getEvidenceMonitor();
1063
- const orchestrator = getCompositionRoot().getOrchestrator();
1064
-
1065
- const currentBranch = gitFlowService.getCurrentBranch();
1066
- const baseBranch = process.env.AST_BASE_BRANCH || 'develop';
1067
- const isProtectedBranch = ['main', 'master', baseBranch].includes(currentBranch);
1068
-
1069
- const uncommittedChanges = gitQuery.getUncommittedChanges();
1070
- const hasUncommittedChanges = uncommittedChanges && uncommittedChanges.length > 0;
1071
-
1072
- // 1. Protected Branch Guard
1073
- if (isProtectedBranch && hasUncommittedChanges) {
1074
- if (now - lastGitFlowNotification > NOTIFICATION_COOLDOWN) {
1075
- const state = gitQuery.getBranchState(currentBranch);
1076
- sendNotification(
1077
- '⚠️ Git Flow Violation',
1078
- `branch=${currentBranch} changes detected on protected branch. Create a feature branch.`,
1079
- 'Basso'
1080
- );
1081
- lastGitFlowNotification = now;
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
- // 3. Autonomous Orchestration
1097
- if (orchestrator.shouldReanalyze()) {
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(' AI Start Executed', `Platforms: ${decision.platforms.map(p => p.platform.toUpperCase()).join(', ')}`, 'Glass');
1103
- } catch (e) {
1104
- sendNotification(' AI Start Error', `Failed to execute: ${e.message}`, 'Basso');
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
- } catch (error) {
1110
- if (process.env.DEBUG) console.error('[MCP] Polling loop error:', error);
1111
- }
1112
- }, 30000);
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
- // AUTO-COMMIT: Only for project code changes (no node_modules, no library)
1115
- setInterval(async () => {
1116
- if (!AUTO_COMMIT_ENABLED) {
1117
- return;
1118
- }
1128
+ } catch (error) {
1129
+ if (process.env.DEBUG) console.error('[MCP] Polling loop error:', error);
1130
+ }
1131
+ }, 30000);
1132
+ }
1119
1133
 
1120
- const now = Date.now();
1121
- if (now - lastAutoCommitTime < AUTO_COMMIT_INTERVAL) return;
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
- try {
1124
- const gitFlowService = getCompositionRoot().getGitFlowService();
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
- const currentBranch = gitFlowService.getCurrentBranch();
1129
- const isFeatureBranch = currentBranch.match(/^(feature|fix|hotfix)\//);
1144
+ try {
1145
+ const gitFlowService = getCompositionRoot().getGitFlowService();
1146
+ const gitQuery = getCompositionRoot().getGitQueryAdapter();
1147
+ const gitCommand = getCompositionRoot().getGitCommandAdapter();
1130
1148
 
1131
- if (!isFeatureBranch) {
1132
- return;
1133
- }
1149
+ const currentBranch = gitFlowService.getCurrentBranch();
1150
+ const isFeatureBranch = currentBranch.match(/^(feature|fix|hotfix)\//);
1134
1151
 
1135
- if (gitFlowService.isClean()) {
1136
- return;
1137
- }
1152
+ if (!isFeatureBranch) {
1153
+ return;
1154
+ }
1138
1155
 
1139
- // Get uncommitted changes
1140
- const uncommittedChanges = gitQuery.getUncommittedChanges();
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
- // Exclude library itself
1159
- if (libraryPath && file.startsWith(libraryPath + '/')) {
1160
- return false;
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
- // Code/Doc files only
1164
- const codeExtensions = ['.ts', '.tsx', '.js', '.jsx', '.swift', '.kt', '.py', '.java', '.go', '.rs', '.md', '.json', '.yaml', '.yml'];
1165
- return codeExtensions.some(ext => file.endsWith(ext));
1166
- });
1193
+ // Stage files
1194
+ filesToCommit.forEach(file => {
1195
+ gitCommand.add(file);
1196
+ });
1167
1197
 
1168
- if (filesToCommit.length === 0) {
1169
- return;
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
- // Stage files
1173
- filesToCommit.forEach(file => {
1174
- gitCommand.add(file);
1175
- });
1202
+ // Commit
1203
+ gitCommand.commit(commitMessage);
1176
1204
 
1177
- const branchType = currentBranch.split('/')[0];
1178
- const branchName = currentBranch.split('/').slice(1).join('/');
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
- // Commit
1182
- gitCommand.commit(commitMessage);
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
- sendNotification('✅ Auto-Commit', `${filesToCommit.length} files in ${currentBranch}`, 'Purr');
1185
- lastAutoCommitTime = now;
1214
+ if (AUTO_PR_ENABLED) {
1215
+ const baseBranch = process.env.AST_BASE_BRANCH || 'develop';
1216
+ const branchState = gitQuery.getBranchState(currentBranch);
1186
1217
 
1187
- if (AUTO_PUSH_ENABLED) {
1188
- if (gitFlowService.isGitHubAvailable()) {
1189
- try {
1190
- gitCommand.push('origin', currentBranch);
1191
- sendNotification('✅ Auto-Push', `Pushed to origin/${currentBranch}`, 'Glass');
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
- } catch (e) {
1206
- if (!e.message.includes('No remote')) {
1207
- sendNotification('⚠️ Auto-Push Failed', 'Push manual required', 'Basso');
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
- } catch (error) {
1214
- if (process.env.DEBUG) console.error('[MCP] Auto-commit error:', error);
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
  }