pumuki-ast-hooks 5.5.49 → 5.5.51

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.
Files changed (26) hide show
  1. package/hooks/git-status-monitor.ts +5 -0
  2. package/hooks/notify-macos.ts +1 -0
  3. package/hooks/pre-tool-use-evidence-validator.ts +1 -0
  4. package/package.json +1 -1
  5. package/scripts/hooks-system/.audit_tmp/hook-metrics.jsonl +56 -0
  6. package/scripts/hooks-system/application/services/guard/GuardConfig.js +4 -2
  7. package/scripts/hooks-system/application/services/installation/GitEnvironmentService.js +20 -2
  8. package/scripts/hooks-system/application/services/installation/HookAssetsInstaller.js +0 -0
  9. package/scripts/hooks-system/application/services/installation/McpConfigurator.js +84 -18
  10. package/scripts/hooks-system/application/services/monitoring/EvidenceMonitor.js +5 -146
  11. package/scripts/hooks-system/application/services/monitoring/EvidenceRefreshRunner.js +161 -0
  12. package/scripts/hooks-system/infrastructure/ast/ast-core.js +13 -1
  13. package/scripts/hooks-system/infrastructure/ast/ast-intelligence.js +2 -3
  14. package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +1 -4
  15. package/scripts/hooks-system/infrastructure/ast/backend/detectors/god-class-detector.js +1 -2
  16. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/StagedSwiftFilePreparer.js +59 -0
  17. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/SwiftAstRunner.js +51 -0
  18. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/SwiftToolchainResolver.js +57 -0
  19. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js +27 -137
  20. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSAstAnalysisOrchestrator.js +32 -0
  21. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseAnalyzer.js +34 -397
  22. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseChecks.js +350 -0
  23. package/scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js +407 -5
  24. package/scripts/hooks-system/infrastructure/orchestration/__tests__/intelligent-audit.spec.js +16 -0
  25. package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +1 -1
  26. package/scripts/hooks-system/infrastructure/shell/gitflow/gitflow-enforcer.sh +33 -11
@@ -42,6 +42,7 @@ function getGitStatus(projectDir: string): GitStatus | null {
42
42
  hasUncommittedChanges: lines.length > 0
43
43
  };
44
44
  } catch (err) {
45
+ console.error(`[git-status-monitor] Failed to read git status: ${(err as Error).message}`);
45
46
  return null;
46
47
  }
47
48
  }
@@ -79,6 +80,7 @@ function detectPlatformFromFiles(projectDir: string): string[] {
79
80
  }
80
81
  }
81
82
  } catch (err) {
83
+ console.error(`[git-status-monitor] Failed to detect platforms from files: ${(err as Error).message}`);
82
84
  }
83
85
 
84
86
  return platforms.length > 0 ? platforms : ['frontend', 'backend', 'ios', 'android'];
@@ -134,6 +136,7 @@ async function main() {
134
136
  sound: 'Ping'
135
137
  });
136
138
  } catch (err) {
139
+ console.error(`[git-status-monitor] Notification failed (staged): ${(err as Error).message}`);
137
140
  }
138
141
  } else if (totalChanges > 10) {
139
142
  try {
@@ -144,11 +147,13 @@ async function main() {
144
147
  sound: 'Glass'
145
148
  });
146
149
  } catch (err) {
150
+ console.error(`[git-status-monitor] Notification failed (unstaged): ${(err as Error).message}`);
147
151
  }
148
152
  }
149
153
 
150
154
  process.exit(0);
151
155
  } catch (err) {
156
+ console.error(`[git-status-monitor] Unexpected error: ${(err as Error).message}`);
152
157
  process.exit(0);
153
158
  }
154
159
  }
@@ -24,6 +24,7 @@ export function sendMacOSNotification(options: NotificationOptions): void {
24
24
  try {
25
25
  execSync(`osascript -e '${script}'`, { stdio: 'ignore' });
26
26
  } catch (err) {
27
+ console.error(`[notify-macos] Failed to send notification: ${(err as Error).message}`);
27
28
  }
28
29
  }
29
30
 
@@ -235,6 +235,7 @@ async function main() {
235
235
  sound: 'Basso'
236
236
  });
237
237
  } catch (err) {
238
+ process.stderr.write(`Notification failed: ${err instanceof Error ? err.message : String(err)}\n`);
238
239
  }
239
240
  process.stderr.write(`${validation.error || ''}\n`);
240
241
  process.exit(2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki-ast-hooks",
3
- "version": "5.5.49",
3
+ "version": "5.5.51",
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": {
@@ -102,3 +102,59 @@
102
102
  {"timestamp":1767739777882,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
103
103
  {"timestamp":1767739777882,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
104
104
  {"timestamp":1767739777882,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
105
+ {"timestamp":1767772854432,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
106
+ {"timestamp":1767772854432,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
107
+ {"timestamp":1767772854432,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
108
+ {"timestamp":1767772854432,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
109
+ {"timestamp":1767772971939,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
110
+ {"timestamp":1767772971939,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
111
+ {"timestamp":1767772971939,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
112
+ {"timestamp":1767772971939,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
113
+ {"timestamp":1767775802167,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
114
+ {"timestamp":1767775802167,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
115
+ {"timestamp":1767775802167,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
116
+ {"timestamp":1767775802167,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
117
+ {"timestamp":1767775833943,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
118
+ {"timestamp":1767775833943,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
119
+ {"timestamp":1767775833943,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
120
+ {"timestamp":1767775833943,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
121
+ {"timestamp":1767776135844,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
122
+ {"timestamp":1767776135844,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
123
+ {"timestamp":1767776135844,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
124
+ {"timestamp":1767776135844,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
125
+ {"timestamp":1767776384649,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
126
+ {"timestamp":1767776384649,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
127
+ {"timestamp":1767776384649,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
128
+ {"timestamp":1767776384649,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
129
+ {"timestamp":1767777653343,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
130
+ {"timestamp":1767777653343,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
131
+ {"timestamp":1767777653343,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
132
+ {"timestamp":1767777653344,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
133
+ {"timestamp":1767778142566,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
134
+ {"timestamp":1767778142567,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
135
+ {"timestamp":1767778142567,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
136
+ {"timestamp":1767778142567,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
137
+ {"timestamp":1767778800954,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
138
+ {"timestamp":1767778800955,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
139
+ {"timestamp":1767778800955,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
140
+ {"timestamp":1767778800955,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
141
+ {"timestamp":1767779528552,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
142
+ {"timestamp":1767779528552,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
143
+ {"timestamp":1767779528552,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
144
+ {"timestamp":1767779528552,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
145
+ {"timestamp":1767779903813,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
146
+ {"timestamp":1767779903813,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
147
+ {"timestamp":1767779903813,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
148
+ {"timestamp":1767779903813,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
149
+ {"timestamp":1767780497468,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
150
+ {"timestamp":1767780497468,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
151
+ {"timestamp":1767780497468,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
152
+ {"timestamp":1767780497468,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
153
+ {"timestamp":1767780655724,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
154
+ {"timestamp":1767780655725,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
155
+ {"timestamp":1767780655725,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
156
+ {"timestamp":1767780655725,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
157
+ {"timestamp":1767782291627,"hook":"audit_logger","operation":"constructor","status":"started","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
158
+ {"timestamp":1767782291627,"hook":"audit_logger","operation":"ensure_dir","status":"started"}
159
+ {"timestamp":1767782291627,"hook":"audit_logger","operation":"ensure_dir","status":"success"}
160
+ {"timestamp":1767782291627,"hook":"audit_logger","operation":"constructor","status":"success","repoRoot":"/Users/juancarlosmerlosalbarracin/Developer/Projects/ast-intelligence-hooks/scripts/hooks-system"}
@@ -3,9 +3,11 @@ const AuditLogger = require('../logging/AuditLogger');
3
3
 
4
4
  class GuardConfig {
5
5
  constructor(env = envHelper) {
6
-
7
- this.auditLogger = new AuditLogger({ repoRoot: process.cwd() });const getNumber = (name, def) =>
6
+ this.auditLogger = new AuditLogger({ repoRoot: process.cwd() });
7
+
8
+ const getNumber = (name, def) =>
8
9
  typeof env.getNumber === 'function' ? env.getNumber(name, def) : Number(env[name] || def);
10
+
9
11
  const getBool = (name, def) =>
10
12
  typeof env.getBool === 'function' ? env.getBool(name, def) : (env[name] !== 'false');
11
13
 
@@ -128,6 +128,20 @@ if [[ "$CURRENT_BRANCH" == "main" ]] || [[ "$CURRENT_BRANCH" == "master" ]] || [
128
128
  exit 1
129
129
  fi
130
130
 
131
+ # Enforce Git Flow checks (strict) before allowing commit
132
+ ENFORCER_SCRIPT="scripts/hooks-system/infrastructure/shell/gitflow/gitflow-enforcer.sh"
133
+ if [[ -f "$ENFORCER_SCRIPT" ]]; then
134
+ echo ""
135
+ echo "🔍 Running Git Flow checks (strict)..."
136
+ echo ""
137
+ if ! GITFLOW_STRICT_CHECK=true bash "$ENFORCER_SCRIPT" check; then
138
+ echo ""
139
+ echo "🚨 COMMIT BLOCKED: Git Flow checks failed"
140
+ echo ""
141
+ exit 1
142
+ fi
143
+ fi
144
+
131
145
  # Check if there are staged files
132
146
  STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\\.(ts|tsx|js|jsx|swift|kt)$' || true)
133
147
  if [ -z "$STAGED_FILES" ]; then
@@ -263,10 +277,14 @@ fi
263
277
  # Run gitflow-enforcer if available (optional validation)
264
278
  ENFORCER_SCRIPT="scripts/hooks-system/infrastructure/shell/gitflow/gitflow-enforcer.sh"
265
279
  if [[ -f "$ENFORCER_SCRIPT" ]]; then
266
- if ! bash "$ENFORCER_SCRIPT" check 2>/dev/null; then
280
+ echo ""
281
+ echo "🔍 Running Git Flow checks (strict)..."
282
+ echo ""
283
+ if ! GITFLOW_STRICT_CHECK=true bash "$ENFORCER_SCRIPT" check; then
267
284
  echo ""
268
- echo "⚠️ Git Flow check completed with warnings (non-blocking)"
285
+ echo "🚨 PUSH BLOCKED: Git Flow checks failed"
269
286
  echo ""
287
+ exit 1
270
288
  fi
271
289
  fi
272
290
 
@@ -130,33 +130,99 @@ class McpConfigurator {
130
130
  }
131
131
  };
132
132
 
133
- const globalConfigPath = this.getGlobalWindsurfConfigPath();
134
- const globalConfigDir = path.dirname(globalConfigPath);
135
- if (!fs.existsSync(globalConfigDir)) {
136
- fs.mkdirSync(globalConfigDir, { recursive: true });
137
- }
133
+ this.configureProjectScoped(mcpConfig, serverId);
134
+ this.cleanupGlobalConfig(serverId);
135
+ }
136
+
137
+ configureProjectScoped(mcpConfig, serverId) {
138
+ const windsurfProjectDir = path.join(this.targetRoot, '.windsurf');
139
+ const windsurfProjectPath = path.join(windsurfProjectDir, 'mcp.json');
138
140
 
139
141
  try {
140
- if (!fs.existsSync(globalConfigPath)) {
141
- fs.writeFileSync(globalConfigPath, JSON.stringify(mcpConfig, null, 2));
142
- this.logSuccess(`Configured global Windsurf MCP at ${globalConfigPath}`);
143
- if (this.logger) this.logger.info('MCP_GLOBAL_CONFIGURED', { path: globalConfigPath });
144
- } else {
145
- const existing = JSON.parse(fs.readFileSync(globalConfigPath, 'utf8'));
142
+ if (!fs.existsSync(windsurfProjectDir)) {
143
+ fs.mkdirSync(windsurfProjectDir, { recursive: true });
144
+ }
145
+
146
+ let finalConfig = mcpConfig;
147
+ if (fs.existsSync(windsurfProjectPath)) {
148
+ const existing = JSON.parse(fs.readFileSync(windsurfProjectPath, 'utf8'));
146
149
  if (!existing.mcpServers) existing.mcpServers = {};
150
+ Object.keys(existing.mcpServers).forEach(id => {
151
+ if (id.startsWith('ast-intelligence-automation-') && id !== serverId) {
152
+ delete existing.mcpServers[id];
153
+ }
154
+ });
155
+ existing.mcpServers[serverId] = mcpConfig.mcpServers[serverId];
156
+ finalConfig = existing;
157
+ }
158
+
159
+ fs.writeFileSync(windsurfProjectPath, JSON.stringify(finalConfig, null, 2));
160
+ this.logSuccess(`Configured project-scoped Windsurf MCP at ${windsurfProjectPath}`);
161
+ if (this.logger) this.logger.info('MCP_PROJECT_CONFIGURED', { path: windsurfProjectPath, serverId });
162
+ } catch (error) {
163
+ this.logWarning(`Failed to configure project-scoped MCP: ${error.message}`);
164
+ if (this.logger) this.logger.warn('MCP_PROJECT_CONFIGURE_FAILED', { error: error.message });
165
+ }
147
166
 
148
- // Prevent duplicate MCP servers for the same repoRoot by disabling legacy entries.
149
- this.disableDuplicateServersForRepo(existing, serverId);
167
+ const cursorProjectDir = path.join(this.targetRoot, '.cursor');
168
+ const cursorProjectPath = path.join(cursorProjectDir, 'mcp.json');
150
169
 
170
+ try {
171
+ if (!fs.existsSync(cursorProjectDir)) {
172
+ fs.mkdirSync(cursorProjectDir, { recursive: true });
173
+ }
174
+
175
+ let finalConfig = mcpConfig;
176
+ if (fs.existsSync(cursorProjectPath)) {
177
+ const existing = JSON.parse(fs.readFileSync(cursorProjectPath, 'utf8'));
178
+ if (!existing.mcpServers) existing.mcpServers = {};
179
+ Object.keys(existing.mcpServers).forEach(id => {
180
+ if (id.startsWith('ast-intelligence-automation-') && id !== serverId) {
181
+ delete existing.mcpServers[id];
182
+ }
183
+ });
151
184
  existing.mcpServers[serverId] = mcpConfig.mcpServers[serverId];
185
+ finalConfig = existing;
186
+ }
187
+
188
+ fs.writeFileSync(cursorProjectPath, JSON.stringify(finalConfig, null, 2));
189
+ this.logSuccess(`Configured project-scoped Cursor MCP at ${cursorProjectPath}`);
190
+ if (this.logger) this.logger.info('MCP_CURSOR_CONFIGURED', { path: cursorProjectPath, serverId });
191
+ } catch (error) {
192
+ this.logWarning(`Failed to configure Cursor MCP: ${error.message}`);
193
+ if (this.logger) this.logger.warn('MCP_CURSOR_CONFIGURE_FAILED', { error: error.message });
194
+ }
195
+ }
196
+
197
+ cleanupGlobalConfig(currentServerId) {
198
+ const globalConfigPath = this.getGlobalWindsurfConfigPath();
152
199
 
200
+ try {
201
+ if (!fs.existsSync(globalConfigPath)) return;
202
+
203
+ const existing = JSON.parse(fs.readFileSync(globalConfigPath, 'utf8'));
204
+ if (!existing.mcpServers) return;
205
+
206
+ let modified = false;
207
+ Object.keys(existing.mcpServers).forEach(id => {
208
+ const server = existing.mcpServers[id];
209
+ if (!server || !server.env) return;
210
+
211
+ if (server.env.REPO_ROOT === this.targetRoot) {
212
+ delete existing.mcpServers[id];
213
+ modified = true;
214
+ this.logInfo(`Removed ${id} from global config (now project-scoped)`);
215
+ }
216
+ });
217
+
218
+ if (modified) {
153
219
  fs.writeFileSync(globalConfigPath, JSON.stringify(existing, null, 2));
154
- this.logSuccess(`Updated global Windsurf MCP at ${globalConfigPath}`);
155
- if (this.logger) this.logger.info('MCP_GLOBAL_UPDATED', { path: globalConfigPath });
220
+ if (this.logger) this.logger.info('MCP_GLOBAL_CLEANUP', { removedForRepo: this.targetRoot });
221
+ }
222
+ } catch (error) {
223
+ if (process.env.DEBUG) {
224
+ process.stderr.write(`[MCP] cleanupGlobalConfig failed: ${error.message}\n`);
156
225
  }
157
- } catch (mergeError) {
158
- this.logWarning(`${globalConfigPath} exists but couldn't be merged, skipping`);
159
- if (this.logger) this.logger.warn('MCP_GLOBAL_MERGE_FAILED', { error: mergeError.message });
160
226
  }
161
227
  }
162
228
 
@@ -1,7 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { execSync } = require('child_process');
4
- const { ConfigurationError, DomainError } = require('../../../domain/errors');
3
+ const { EvidenceRefreshRunner } = require('./EvidenceRefreshRunner');
5
4
  const AuditLogger = require('../logging/AuditLogger');
6
5
 
7
6
  class EvidenceMonitor {
@@ -14,98 +13,9 @@ class EvidenceMonitor {
14
13
  this.lastStaleNotification = 0;
15
14
  this.pollTimer = null;
16
15
  this.evidencePath = path.join(repoRoot, '.AI_EVIDENCE.json');
17
- this.tempDir = path.join(repoRoot, '.audit_tmp');
18
- this.updateScript = this.resolveUpdateEvidenceScript();
19
- this.refreshInFlight = false;
20
- this.refreshTimeoutMs = options.refreshTimeoutMs || 120000;
21
- this.refreshLockFile = path.join(this.tempDir, 'evidence-refresh.lock');
22
- }
23
-
24
- isPidRunning(pid) {
25
- if (!pid || !Number.isFinite(pid) || pid <= 0) return false;
26
- try {
27
- process.kill(pid, 0);
28
- return true;
29
- } catch {
30
- return false;
31
- }
32
- }
33
-
34
- acquireRefreshLock() {
35
- try {
36
- fs.mkdirSync(this.tempDir, { recursive: true });
37
- } catch (error) {
38
- console.warn('[EvidenceMonitor] Failed to ensure temp dir:', error.message);
39
- }
40
-
41
- try {
42
- const fd = fs.openSync(this.refreshLockFile, 'wx');
43
- const payload = JSON.stringify({ pid: process.pid, timestamp: new Date().toISOString() });
44
- fs.writeFileSync(fd, payload, { encoding: 'utf8' });
45
- fs.closeSync(fd);
46
- return { acquired: true };
47
- } catch (error) {
48
- if (error && error.code !== 'EEXIST') {
49
- return { acquired: false, reason: 'error', error };
50
- }
51
-
52
- try {
53
- const raw = String(fs.readFileSync(this.refreshLockFile, 'utf8') || '').trim();
54
- const data = raw ? JSON.parse(raw) : null;
55
- const lockPid = data && Number(data.pid);
56
- if (lockPid && this.isPidRunning(lockPid)) {
57
- return { acquired: false, reason: 'locked', pid: lockPid };
58
- }
59
- } catch (error) {
60
- console.warn('[EvidenceMonitor] Failed to read refresh lock file:', error.message);
61
- }
62
-
63
- try {
64
- fs.unlinkSync(this.refreshLockFile);
65
- } catch (error) {
66
- console.warn('[EvidenceMonitor] Failed to remove stale refresh lock:', error.message);
67
- }
68
-
69
- try {
70
- const fd = fs.openSync(this.refreshLockFile, 'wx');
71
- const payload = JSON.stringify({ pid: process.pid, timestamp: new Date().toISOString() });
72
- fs.writeFileSync(fd, payload, { encoding: 'utf8' });
73
- fs.closeSync(fd);
74
- return { acquired: true };
75
- } catch (retryError) {
76
- return { acquired: false, reason: 'locked', error: retryError };
77
- }
78
- }
79
- }
80
-
81
- releaseRefreshLock() {
82
- try {
83
- if (!fs.existsSync(this.refreshLockFile)) return;
84
- const raw = String(fs.readFileSync(this.refreshLockFile, 'utf8') || '').trim();
85
- const data = raw ? JSON.parse(raw) : null;
86
- const lockPid = data && Number(data.pid);
87
- if (lockPid === process.pid) {
88
- fs.unlinkSync(this.refreshLockFile);
89
- }
90
- } catch (error) {
91
- console.warn('[EvidenceMonitor] Failed to release refresh lock:', error.message);
92
- }
93
- }
94
-
95
- resolveUpdateEvidenceScript() {
96
- const candidates = [
97
- path.join(this.repoRoot, 'node_modules/@pumuki/ast-intelligence-hooks/bin/update-evidence.sh'),
98
- path.join(this.repoRoot, 'scripts/hooks-system/bin/update-evidence.sh'),
99
- path.join(this.repoRoot, 'bin/update-evidence.sh')
100
- ];
101
-
102
- for (const candidate of candidates) {
103
- if (fs.existsSync(candidate)) {
104
- return candidate;
105
- }
106
- }
107
-
108
- return null;
16
+ this.refreshRunner = new EvidenceRefreshRunner(repoRoot, {
17
+ refreshTimeoutMs: options.refreshTimeoutMs
18
+ });
109
19
  }
110
20
 
111
21
  isStale() {
@@ -123,58 +33,7 @@ class EvidenceMonitor {
123
33
  }
124
34
 
125
35
  async refresh() {
126
- if (!this.updateScript) {
127
- throw new ConfigurationError('Update evidence script not found', 'updateScript');
128
- }
129
-
130
- if (this.refreshInFlight) {
131
- return '';
132
- }
133
-
134
- const lock = this.acquireRefreshLock();
135
- if (!lock.acquired) {
136
- return '';
137
- }
138
-
139
- this.refreshInFlight = true;
140
-
141
- return new Promise((resolve, reject) => {
142
- const child = require('child_process').spawn('bash', [this.updateScript, '--auto', '--refresh-only'], {
143
- cwd: this.repoRoot,
144
- stdio: ['pipe', 'pipe', 'pipe']
145
- });
146
-
147
- let output = '';
148
- child.stdout.on('data', (data) => {
149
- output += data.toString();
150
- });
151
-
152
- const timeoutId = setTimeout(() => {
153
- try {
154
- child.kill('SIGKILL');
155
- } catch (error) {
156
- console.warn('[EvidenceMonitor] Failed to kill timed-out refresh process:', error.message);
157
- }
158
- }, this.refreshTimeoutMs);
159
-
160
- child.on('close', (code) => {
161
- clearTimeout(timeoutId);
162
- this.refreshInFlight = false;
163
- this.releaseRefreshLock();
164
- if (code === 0) {
165
- resolve(output);
166
- } else {
167
- reject(new DomainError(`Evidence refresh failed with code ${code}`, 'EVIDENCE_REFRESH_FAILED'));
168
- }
169
- });
170
-
171
- child.on('error', (err) => {
172
- clearTimeout(timeoutId);
173
- this.refreshInFlight = false;
174
- this.releaseRefreshLock();
175
- reject(err);
176
- });
177
- });
36
+ return this.refreshRunner.refresh();
178
37
  }
179
38
 
180
39
  startPolling(onStale, onRefreshed) {
@@ -0,0 +1,161 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawn } = require('child_process');
4
+ const { ConfigurationError, DomainError } = require('../../../domain/errors');
5
+
6
+ class EvidenceRefreshRunner {
7
+ constructor(repoRoot, options = {}) {
8
+ this.repoRoot = repoRoot;
9
+ this.tempDir = path.join(repoRoot, '.audit_tmp');
10
+ this.refreshLockFile = path.join(this.tempDir, 'evidence-refresh.lock');
11
+ this.refreshTimeoutMs = options.refreshTimeoutMs || 120000;
12
+ this.refreshInFlight = false;
13
+ this.updateScript = this.resolveUpdateEvidenceScript();
14
+ }
15
+
16
+ resolveUpdateEvidenceScript() {
17
+ const candidates = [
18
+ path.join(this.repoRoot, 'node_modules/@pumuki/ast-intelligence-hooks/bin/update-evidence.sh'),
19
+ path.join(this.repoRoot, 'scripts/hooks-system/bin/update-evidence.sh'),
20
+ path.join(this.repoRoot, 'bin/update-evidence.sh')
21
+ ];
22
+
23
+ for (const candidate of candidates) {
24
+ if (fs.existsSync(candidate)) {
25
+ return candidate;
26
+ }
27
+ }
28
+
29
+ return null;
30
+ }
31
+
32
+ isPidRunning(pid) {
33
+ if (!pid || !Number.isFinite(pid) || pid <= 0) return false;
34
+ try {
35
+ process.kill(pid, 0);
36
+ return true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ acquireRefreshLock() {
43
+ try {
44
+ fs.mkdirSync(this.tempDir, { recursive: true });
45
+ } catch (error) {
46
+ console.warn('[EvidenceRefreshRunner] Failed to ensure temp dir:', error.message);
47
+ }
48
+
49
+ try {
50
+ const fd = fs.openSync(this.refreshLockFile, 'wx');
51
+ const payload = JSON.stringify({ pid: process.pid, timestamp: new Date().toISOString() });
52
+ fs.writeFileSync(fd, payload, { encoding: 'utf8' });
53
+ fs.closeSync(fd);
54
+ return { acquired: true };
55
+ } catch (error) {
56
+ if (error && error.code !== 'EEXIST') {
57
+ return { acquired: false, reason: 'error', error };
58
+ }
59
+
60
+ try {
61
+ const raw = String(fs.readFileSync(this.refreshLockFile, 'utf8') || '').trim();
62
+ const data = raw ? JSON.parse(raw) : null;
63
+ const lockPid = data && Number(data.pid);
64
+ if (lockPid && this.isPidRunning(lockPid)) {
65
+ return { acquired: false, reason: 'locked', pid: lockPid };
66
+ }
67
+ } catch (readError) {
68
+ console.warn('[EvidenceRefreshRunner] Failed to read refresh lock file:', readError.message);
69
+ }
70
+
71
+ try {
72
+ fs.unlinkSync(this.refreshLockFile);
73
+ } catch (removeError) {
74
+ console.warn('[EvidenceRefreshRunner] Failed to remove stale refresh lock:', removeError.message);
75
+ }
76
+
77
+ try {
78
+ const fd = fs.openSync(this.refreshLockFile, 'wx');
79
+ const payload = JSON.stringify({ pid: process.pid, timestamp: new Date().toISOString() });
80
+ fs.writeFileSync(fd, payload, { encoding: 'utf8' });
81
+ fs.closeSync(fd);
82
+ return { acquired: true };
83
+ } catch (retryError) {
84
+ return { acquired: false, reason: 'locked', error: retryError };
85
+ }
86
+ }
87
+ }
88
+
89
+ releaseRefreshLock() {
90
+ try {
91
+ if (!fs.existsSync(this.refreshLockFile)) return;
92
+ const raw = String(fs.readFileSync(this.refreshLockFile, 'utf8') || '').trim();
93
+ const data = raw ? JSON.parse(raw) : null;
94
+ const lockPid = data && Number(data.pid);
95
+ if (lockPid === process.pid) {
96
+ fs.unlinkSync(this.refreshLockFile);
97
+ }
98
+ } catch (error) {
99
+ console.warn('[EvidenceRefreshRunner] Failed to release refresh lock:', error.message);
100
+ }
101
+ }
102
+
103
+ async refresh() {
104
+ if (!this.updateScript) {
105
+ throw new ConfigurationError('Update evidence script not found', 'updateScript');
106
+ }
107
+
108
+ if (this.refreshInFlight) {
109
+ return '';
110
+ }
111
+
112
+ const lock = this.acquireRefreshLock();
113
+ if (!lock.acquired) {
114
+ return '';
115
+ }
116
+
117
+ this.refreshInFlight = true;
118
+
119
+ return new Promise((resolve, reject) => {
120
+ const child = spawn('bash', [this.updateScript, '--auto', '--refresh-only'], {
121
+ cwd: this.repoRoot,
122
+ stdio: ['pipe', 'pipe', 'pipe']
123
+ });
124
+
125
+ let output = '';
126
+ child.stdout.on('data', (data) => {
127
+ output += data.toString();
128
+ });
129
+
130
+ const timeoutId = setTimeout(() => {
131
+ try {
132
+ child.kill('SIGKILL');
133
+ } catch (error) {
134
+ console.warn('[EvidenceRefreshRunner] Failed to kill timed-out refresh process:', error.message);
135
+ }
136
+ }, this.refreshTimeoutMs);
137
+
138
+ const finalize = () => {
139
+ clearTimeout(timeoutId);
140
+ this.refreshInFlight = false;
141
+ this.releaseRefreshLock();
142
+ };
143
+
144
+ child.on('close', (code) => {
145
+ finalize();
146
+ if (code === 0) {
147
+ resolve(output);
148
+ } else {
149
+ reject(new DomainError(`Evidence refresh failed with code ${code}`, 'EVIDENCE_REFRESH_FAILED'));
150
+ }
151
+ });
152
+
153
+ child.on('error', (err) => {
154
+ finalize();
155
+ reject(err);
156
+ });
157
+ });
158
+ }
159
+ }
160
+
161
+ module.exports = { EvidenceRefreshRunner };