pumuki-ast-hooks 5.3.30 → 5.4.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.
@@ -0,0 +1,153 @@
1
+ const ContextDetectionEngine = require('../services/ContextDetectionEngine');
2
+ const PlatformDetectionService = require('../services/PlatformDetectionService');
3
+ const AutonomousOrchestrator = require('../services/AutonomousOrchestrator');
4
+ const AutoExecuteAIStartUseCase = require('../use-cases/AutoExecuteAIStartUseCase');
5
+ const UnifiedLogger = require('../services/logging/UnifiedLogger');
6
+ const NotificationCenterService = require('../services/notification/NotificationCenterService');
7
+ const GitFlowService = require('../services/GitFlowService');
8
+ const AuditLogger = require('../services/logging/AuditLogger');
9
+ const RealtimeGuardService = require('../services/RealtimeGuardService');
10
+ const path = require('path');
11
+ const env = require('../../config/env');
12
+
13
+ class ServiceFactory {
14
+ constructor(repoRoot, instances, adapterFactory) {
15
+ this.repoRoot = repoRoot;
16
+ this.instances = instances;
17
+ this.adapterFactory = adapterFactory;
18
+ this.auditDir = path.join(repoRoot, '.audit-reports');
19
+ }
20
+
21
+ getLogger() {
22
+ if (!this.instances.has('logger')) {
23
+ this.instances.set('logger', new UnifiedLogger({
24
+ component: 'HookSystem',
25
+ file: {
26
+ enabled: true,
27
+ path: path.join(this.auditDir, 'guard-audit.jsonl'),
28
+ level: env.get('HOOK_LOG_LEVEL', env.isProd ? 'warn' : 'info')
29
+ },
30
+ console: {
31
+ enabled: false,
32
+ level: 'info'
33
+ }
34
+ }));
35
+ }
36
+ return this.instances.get('logger');
37
+ }
38
+
39
+ getNotificationService() {
40
+ if (!this.instances.has('notificationService')) {
41
+ const logger = this.getLogger();
42
+ this.instances.set('notificationService', new NotificationCenterService({
43
+ repoRoot: this.repoRoot,
44
+ logger
45
+ }));
46
+ }
47
+ return this.instances.get('notificationService');
48
+ }
49
+
50
+ getAuditLogger() {
51
+ if (!this.instances.has('auditLogger')) {
52
+ const logger = this.getLogger();
53
+ this.instances.set('auditLogger', new AuditLogger({
54
+ repoRoot: this.repoRoot,
55
+ filename: path.join('.audit_tmp', 'audit.log'),
56
+ logger
57
+ }));
58
+ }
59
+ return this.instances.get('auditLogger');
60
+ }
61
+
62
+ getContextDetectionEngine() {
63
+ if (!this.instances.has('contextEngine')) {
64
+ const gitQuery = this.adapterFactory.getGitQueryAdapter();
65
+ const logger = this.getLogger();
66
+ this.instances.set('contextEngine', new ContextDetectionEngine(gitQuery, logger));
67
+ }
68
+ return this.instances.get('contextEngine');
69
+ }
70
+
71
+ getPlatformDetectionService() {
72
+ if (!this.instances.has('platformDetector')) {
73
+ this.instances.set('platformDetector', new PlatformDetectionService());
74
+ }
75
+ return this.instances.get('platformDetector');
76
+ }
77
+
78
+ getOrchestrator() {
79
+ if (!this.instances.has('orchestrator')) {
80
+ const contextEngine = this.getContextDetectionEngine();
81
+ const platformDetector = this.getPlatformDetectionService();
82
+ const logger = this.getLogger();
83
+
84
+ this.instances.set('orchestrator', new AutonomousOrchestrator(
85
+ contextEngine,
86
+ platformDetector,
87
+ null,
88
+ logger
89
+ ));
90
+ }
91
+ return this.instances.get('orchestrator');
92
+ }
93
+
94
+ getAutoExecuteAIStartUseCase() {
95
+ if (!this.instances.has('autoExecuteAIStart')) {
96
+ const orchestrator = this.getOrchestrator();
97
+ const logger = this.getLogger();
98
+ this.instances.set('autoExecuteAIStart', new AutoExecuteAIStartUseCase(orchestrator, this.repoRoot, logger));
99
+ }
100
+ return this.instances.get('autoExecuteAIStart');
101
+ }
102
+
103
+ getBlockCommitUseCase() {
104
+ if (!this.instances.has('blockCommit')) {
105
+ const BlockCommitUseCase = require('../use-cases/BlockCommitUseCase');
106
+ this.instances.set('blockCommit', new BlockCommitUseCase());
107
+ }
108
+ return this.instances.get('blockCommit');
109
+ }
110
+
111
+ getGitFlowService() {
112
+ if (!this.instances.has('gitFlowService')) {
113
+ const logger = this.getLogger();
114
+ const gitQuery = this.adapterFactory.getGitQueryAdapter();
115
+ const gitCommand = this.adapterFactory.getGitCommandAdapter();
116
+ const github = this.adapterFactory.getGitHubAdapter();
117
+
118
+ this.instances.set('gitFlowService', new GitFlowService(this.repoRoot, {
119
+ developBranch: env.get('HOOK_GUARD_GITFLOW_DEVELOP_BRANCH', 'develop'),
120
+ mainBranch: env.get('HOOK_GUARD_GITFLOW_MAIN_BRANCH', 'main'),
121
+ autoSyncEnabled: env.getBool('HOOK_GUARD_GITFLOW_AUTOSYNC', true),
122
+ autoCleanEnabled: env.getBool('HOOK_GUARD_GITFLOW_AUTOCLEAN', true),
123
+ requireClean: env.getBool('HOOK_GUARD_GITFLOW_REQUIRE_CLEAN', true)
124
+ }, logger, gitQuery, gitCommand, github));
125
+ }
126
+ return this.instances.get('gitFlowService');
127
+ }
128
+
129
+ getRealtimeGuardService(monitors) {
130
+ if (!this.instances.has('guardService')) {
131
+ const logger = this.getLogger();
132
+ const notificationService = this.getNotificationService();
133
+ const orchestrator = this.getOrchestrator();
134
+ const auditLogger = this.getAuditLogger();
135
+ const config = {
136
+ debugLogPath: path.join(this.auditDir, 'guard-debug.log'),
137
+ repoRoot: this.repoRoot
138
+ };
139
+
140
+ this.instances.set('guardService', new RealtimeGuardService({
141
+ logger,
142
+ notificationService,
143
+ monitors,
144
+ orchestration: orchestrator,
145
+ config,
146
+ auditLogger
147
+ }));
148
+ }
149
+ return this.instances.get('guardService');
150
+ }
151
+ }
152
+
153
+ module.exports = ServiceFactory;
@@ -1,9 +1,11 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { getGitTreeState, isTreeBeyondLimit } = require('./GitTreeState');
4
3
  const AuditLogger = require('./logging/AuditLogger');
5
4
  const { recordMetric } = require('../../infrastructure/telemetry/metrics-logger');
6
5
  const env = require('../../config/env.js');
6
+ const GuardNotifier = require('./guard/GuardNotifier');
7
+ const EvidenceManager = require('./guard/EvidenceManager');
8
+ const GitTreeManager = require('./guard/GitTreeManager');
7
9
 
8
10
  class RealtimeGuardService {
9
11
  /**
@@ -27,9 +29,6 @@ class RealtimeGuardService {
27
29
  } = dependencies;
28
30
 
29
31
  this.logger = logger || console;
30
- this.notificationService = notificationService;
31
- this.notifier = typeof notifier === 'function' ? notifier : null;
32
- this.notificationsEnabled = notifications !== false;
33
32
  this.monitors = monitors || {};
34
33
  this.orchestration = orchestration;
35
34
  this.config = config || {};
@@ -40,29 +39,21 @@ class RealtimeGuardService {
40
39
  }
41
40
 
42
41
  this.evidencePath = this.config.evidencePath || path.join(process.cwd(), '.AI_EVIDENCE.json');
43
- this.staleThresholdMs = env.getNumber('HOOK_GUARD_EVIDENCE_STALE_THRESHOLD', 60000);
44
- this.reminderIntervalMs = env.getNumber('HOOK_GUARD_EVIDENCE_REMINDER_INTERVAL', 60000);
45
- this.inactivityGraceMs = env.getNumber('HOOK_GUARD_INACTIVITY_GRACE_MS', 120000);
46
- this.pollIntervalMs = env.getNumber('HOOK_GUARD_EVIDENCE_POLL_INTERVAL', 30000);
47
- this.pollTimer = null;
48
- this.lastStaleNotification = 0;
49
- this.lastUserActivityAt = 0;
50
-
51
- this.gitTreeStagedThreshold = env.getNumber('HOOK_GUARD_DIRTY_TREE_STAGED_LIMIT', 10);
52
- this.gitTreeUnstagedThreshold = env.getNumber('HOOK_GUARD_DIRTY_TREE_UNSTAGED_LIMIT', 15);
53
- this.gitTreeTotalThreshold = env.getNumber('HOOK_GUARD_DIRTY_TREE_TOTAL_LIMIT', 20);
54
- this.gitTreeCheckIntervalMs = env.getNumber('HOOK_GUARD_DIRTY_TREE_INTERVAL', 60000);
55
- this.gitTreeReminderMs = env.getNumber('HOOK_GUARD_DIRTY_TREE_REMINDER', 300000);
56
- this.gitTreeTimer = null;
57
- this.lastDirtyTreeNotification = 0;
58
- this.dirtyTreeActive = false;
59
-
60
- this.autoRefreshCooldownMs = env.getNumber('HOOK_GUARD_EVIDENCE_AUTO_REFRESH_COOLDOWN', 180000);
61
- this.lastAutoRefresh = 0;
62
- this.autoRefreshInFlight = false;
42
+ this.embedTokenMonitor = env.getBool('HOOK_GUARD_EMBEDDED_TOKEN_MONITOR', false);
43
+
44
+ // Initialize specialized components
45
+ this.notifier = new GuardNotifier(
46
+ this.logger,
47
+ notificationService,
48
+ typeof notifier === 'function' ? notifier : null,
49
+ notifications !== false
50
+ );
51
+ this.notifier.setDebugLogPath(this.config.debugLogPath);
52
+
53
+ this.evidenceManager = new EvidenceManager(this.evidencePath, this.notifier, this.auditLogger);
54
+ this.gitTreeManager = new GitTreeManager(this.notifier, this.auditLogger);
63
55
 
64
56
  this.watchers = [];
65
- this.embedTokenMonitor = env.getBool('HOOK_GUARD_EMBEDDED_TOKEN_MONITOR', false);
66
57
  }
67
58
 
68
59
  start() {
@@ -70,7 +61,7 @@ class RealtimeGuardService {
70
61
  this.auditLogger.record({ action: 'guard.realtime.start', resource: 'realtime_guard', status: 'success' });
71
62
  recordMetric({ hook: 'realtime_guard', status: 'start' });
72
63
 
73
- // Start all monitors
64
+ // Start all monitors via specialized managers
74
65
  this._startEvidenceMonitoring();
75
66
  this._startGitTreeMonitoring();
76
67
  this._startActivityMonitoring();
@@ -100,40 +91,22 @@ class RealtimeGuardService {
100
91
  }
101
92
  });
102
93
 
94
+ // Stop specialized managers
95
+ this.evidenceManager.stopPolling();
96
+ this.gitTreeManager.stopMonitoring();
97
+
103
98
  this.logger.info('[RealtimeGuardService] All services stopped');
104
99
  }
105
100
 
106
101
  _startEvidenceMonitoring() {
107
- this.monitors.evidence.startPolling(
108
- () => this.notify('🔄 Evidence is stale - Auto-refreshing...', 'warning'),
109
- () => this.notify('Evidence refreshed successfully', 'success')
102
+ this.evidenceManager.startPolling(
103
+ () => this.notifier.notify('Evidence is stale - Auto-refreshing...', 'warning'),
104
+ () => this.notifier.notify('Evidence refreshed successfully', 'success')
110
105
  );
111
106
  }
112
107
 
113
108
  _startGitTreeMonitoring() {
114
- if (env.getBool('HOOK_GUARD_DIRTY_TREE_DISABLED', false)) return;
115
-
116
- this.monitors.gitTree.startMonitoring((state) => {
117
- if (state.isBeyondLimit) {
118
- const message = `Git tree has too many files: ${state.total} total (${state.staged} staged, ${state.unstaged} unstaged)`;
119
- this.notify(message, 'error', { forceDialog: true });
120
- this.auditLogger.record({
121
- action: 'guard.git_tree.dirty',
122
- resource: 'git_tree',
123
- status: 'warning',
124
- meta: { total: state.total, staged: state.staged, unstaged: state.unstaged }
125
- });
126
- recordMetric({ hook: 'git_tree', status: 'dirty', total: state.total, staged: state.staged, unstaged: state.unstaged });
127
- } else {
128
- this.notify('✅ Git tree is clean', 'success');
129
- this.auditLogger.record({
130
- action: 'guard.git_tree.clean',
131
- resource: 'git_tree',
132
- status: 'success'
133
- });
134
- recordMetric({ hook: 'git_tree', status: 'clean' });
135
- }
136
- });
109
+ this.gitTreeManager.startMonitoring();
137
110
  }
138
111
 
139
112
  _startTokenMonitoring() {
@@ -144,7 +117,7 @@ class RealtimeGuardService {
144
117
 
145
118
  try {
146
119
  this.monitors.token.start();
147
- this.notify('🔋 Token monitor started', 'info');
120
+ this.notifier.notify('🔋 Token monitor started', 'info');
148
121
  this.auditLogger.record({
149
122
  action: 'guard.token_monitor.start',
150
123
  resource: 'token_monitor',
@@ -152,7 +125,7 @@ class RealtimeGuardService {
152
125
  });
153
126
  recordMetric({ hook: 'token_monitor', status: 'start' });
154
127
  } catch (error) {
155
- this.notify(`Failed to start token monitor: ${error.message}`, 'error');
128
+ this.notifier.notify(`Failed to start token monitor: ${error.message}`, 'error');
156
129
  this.auditLogger.record({
157
130
  action: 'guard.token_monitor.start',
158
131
  resource: 'token_monitor',
@@ -178,7 +151,7 @@ class RealtimeGuardService {
178
151
  if (this.monitors.gitFlow.isClean()) {
179
152
  const result = this.monitors.gitFlow.syncBranches();
180
153
  if (result.success) {
181
- this.notify('🔄 Branches synchronized', 'info');
154
+ this.notifier.notify('🔄 Branches synchronized', 'info');
182
155
  recordMetric({ hook: 'gitflow_autosync', status: 'sync_success' });
183
156
  }
184
157
  }
@@ -199,232 +172,62 @@ class RealtimeGuardService {
199
172
  if (this.monitors.ast) this.monitors.ast.start();
200
173
  }
201
174
 
202
- notify(message, level = 'info', options = {}) {
203
- const { forceDialog = false, ...metadata } = options;
204
- this._appendDebugLog(`NOTIFY|${level}|${forceDialog ? 'force-dialog|' : ''}${message}`);
205
-
206
- if (this.notifier && this.notificationsEnabled) {
207
- try {
208
- this.notifier(message, level, { ...metadata, forceDialog });
209
- } catch (error) {
210
- const msg = error && error.message ? error.message : String(error);
211
- this._appendDebugLog(`NOTIFIER_ERROR|${msg}`);
212
- this.logger?.debug?.('REALTIME_GUARD_NOTIFIER_ERROR', { error: msg });
213
- }
214
- }
215
-
216
- if (this.notificationService) {
217
- this.notificationService.enqueue({
218
- message,
219
- level,
220
- metadata: { ...metadata, forceDialog }
221
- });
222
- }
223
- }
175
+ // --- Delegation to specialized components ---
224
176
 
225
- _appendDebugLog(entry) {
226
- try {
227
- const timestamp = new Date().toISOString();
228
- fs.appendFileSync(this.config.debugLogPath, `[${timestamp}] ${entry}\n`);
229
- } catch (error) {
230
- console.error('[RealtimeGuardService] Failed to write debug log:', error.message);
231
- }
177
+ notify(message, level = 'info', options = {}) {
178
+ return this.notifier.notify(message, level, options);
232
179
  }
233
180
 
234
181
  appendDebugLog(entry) {
235
- this._appendDebugLog(entry);
182
+ return this.notifier.appendDebugLog(entry);
236
183
  }
237
184
 
238
185
  readEvidenceTimestamp() {
239
- try {
240
- if (!fs.existsSync(this.evidencePath)) {
241
- return null;
242
- }
243
- const raw = fs.readFileSync(this.evidencePath, 'utf8');
244
- const json = JSON.parse(raw);
245
- const ts = json?.timestamp;
246
- if (!ts) {
247
- return null;
248
- }
249
- const ms = new Date(ts).getTime();
250
- if (Number.isNaN(ms)) {
251
- return null;
252
- }
253
- return ms;
254
- } catch (error) {
255
- const msg = error && error.message ? error.message : String(error);
256
- this._appendDebugLog(`EVIDENCE_TIMESTAMP_ERROR|${msg}`);
257
- this.logger?.debug?.('REALTIME_GUARD_EVIDENCE_TIMESTAMP_ERROR', { error: msg });
258
- return null;
259
- }
186
+ return this.evidenceManager.readEvidenceTimestamp();
260
187
  }
261
188
 
262
189
  evaluateEvidenceAge(source = 'manual', notifyFresh = false) {
263
- const now = Date.now();
264
- const timestamp = this.readEvidenceTimestamp();
265
- if (!timestamp) {
266
- return;
267
- }
268
-
269
- const ageMs = now - timestamp;
270
- const isStale = ageMs > this.staleThresholdMs;
271
- const isRecentlyActive = this.lastUserActivityAt && (now - this.lastUserActivityAt) < this.inactivityGraceMs;
272
-
273
- if (isStale && !isRecentlyActive) {
274
- this.triggerStaleAlert(source, ageMs);
275
- return;
276
- }
277
-
278
- if (notifyFresh && this.lastStaleNotification > 0 && !isStale) {
279
- this.notify('Evidence updated; back within SLA.', 'info');
280
- this.lastStaleNotification = 0;
281
- }
190
+ return this.evidenceManager.evaluateEvidenceAge(source, notifyFresh);
282
191
  }
283
192
 
284
193
  triggerStaleAlert(source, ageMs) {
285
- const now = Date.now();
286
- if (this.lastStaleNotification && (now - this.lastStaleNotification) < this.reminderIntervalMs) {
287
- return;
288
- }
289
-
290
- this.lastStaleNotification = now;
291
- const ageSec = Math.floor(ageMs / 1000);
292
- this.notify(`Evidence has been stale for ${ageSec}s (source: ${source}).`, 'warn', { forceDialog: true });
293
- this.auditLogger.record({
294
- action: 'guard.evidence.stale',
295
- resource: 'evidence',
296
- status: 'warning',
297
- meta: { ageSec, source }
298
- });
299
- recordMetric({ hook: 'evidence', status: 'stale', ageSec, source });
300
- void this.attemptAutoRefresh('stale');
194
+ return this.evidenceManager.triggerStaleAlert(source, ageMs);
301
195
  }
302
196
 
303
197
  async attemptAutoRefresh(reason = 'manual') {
304
- if (!env.getBool('HOOK_GUARD_AUTO_REFRESH', false)) {
305
- return;
306
- }
307
-
308
- const updateScriptCandidates = [
309
- path.join(process.cwd(), 'scripts/hooks-system/bin/update-evidence.sh'),
310
- path.join(process.cwd(), 'node_modules/@pumuki/ast-intelligence-hooks/bin/update-evidence.sh'),
311
- path.join(process.cwd(), 'bin/update-evidence.sh')
312
- ];
313
-
314
- const updateScript = updateScriptCandidates.find(p => fs.existsSync(p));
315
- if (!updateScript) {
316
- return;
317
- }
318
-
319
- const now = Date.now();
320
- const ts = this.readEvidenceTimestamp();
321
- if (ts && (now - ts) <= this.staleThresholdMs) {
322
- return;
323
- }
324
-
325
- if (this.lastAutoRefresh && (now - this.lastAutoRefresh) < this.autoRefreshCooldownMs) {
326
- return;
327
- }
328
-
329
- if (this.autoRefreshInFlight) {
330
- return;
331
- }
332
-
333
- this.autoRefreshInFlight = true;
334
- try {
335
- await this.runDirectEvidenceRefresh(reason);
336
- this.lastAutoRefresh = now;
337
- this.auditLogger.record({
338
- action: 'guard.evidence.auto_refresh',
339
- resource: 'evidence',
340
- status: 'success',
341
- meta: { reason }
342
- });
343
- recordMetric({ hook: 'evidence', status: 'auto_refresh_success', reason });
344
- } finally {
345
- this.autoRefreshInFlight = false;
346
- }
198
+ return this.evidenceManager.attemptAutoRefresh(reason);
347
199
  }
348
200
 
349
- async runDirectEvidenceRefresh(_reason) {
350
- return;
201
+ async runDirectEvidenceRefresh(reason) {
202
+ return this.evidenceManager.runDirectEvidenceRefresh(reason);
351
203
  }
352
204
 
353
205
  async evaluateGitTree() {
354
- try {
355
- const state = getGitTreeState();
356
- const limits = {
357
- stagedLimit: this.gitTreeStagedThreshold,
358
- unstagedLimit: this.gitTreeUnstagedThreshold,
359
- totalLimit: this.gitTreeTotalThreshold
360
- };
361
-
362
- if (isTreeBeyondLimit(state, limits)) {
363
- this.handleDirtyTree(state, limits);
364
- return;
365
- }
366
- this.resolveDirtyTree(state, limits);
367
- } catch (error) {
368
- this.appendDebugLog(`DIRTY_TREE_ERROR|${error.message}`);
369
- }
206
+ return this.gitTreeManager.evaluateGitTree();
370
207
  }
371
208
 
372
- resolveDirtyTree(_state, _limits) {
373
- this.dirtyTreeActive = false;
209
+ resolveDirtyTree(state, limits) {
210
+ return this.gitTreeManager.resolveDirtyTree(state, limits);
374
211
  }
375
212
 
376
- handleDirtyTree(_state, limitOrLimits) {
377
- const now = Date.now();
378
- const limits = typeof limitOrLimits === 'number'
379
- ? { totalLimit: limitOrLimits }
380
- : (limitOrLimits || {});
381
-
382
- if (this.lastDirtyTreeNotification && (now - this.lastDirtyTreeNotification) < this.gitTreeReminderMs) {
383
- return;
384
- }
385
-
386
- this.lastDirtyTreeNotification = now;
387
- this.dirtyTreeActive = true;
388
- this.notify('Git tree is dirty; please stage/unstage to reduce file count.', 'warn', { forceDialog: true, ...limits });
389
- this.persistDirtyTreeState();
213
+ handleDirtyTree(state, limits) {
214
+ return this.gitTreeManager.handleDirtyTree(state, limits);
390
215
  }
391
216
 
392
217
  persistDirtyTreeState() {
393
- return;
218
+ return this.gitTreeManager.persistDirtyTreeState();
394
219
  }
395
220
 
396
221
  startGitTreeMonitoring() {
397
- if (this.gitTreeTimer) {
398
- clearInterval(this.gitTreeTimer);
399
- this.gitTreeTimer = null;
400
- }
401
-
402
- const thresholdsValid = this.gitTreeStagedThreshold > 0 || this.gitTreeUnstagedThreshold > 0 || this.gitTreeTotalThreshold > 0;
403
- if (!thresholdsValid || this.gitTreeCheckIntervalMs <= 0) {
404
- this.gitTreeTimer = null;
405
- return;
406
- }
407
-
408
- void this.evaluateGitTree();
409
- this.gitTreeTimer = setInterval(() => {
410
- void this.evaluateGitTree();
411
- }, this.gitTreeCheckIntervalMs);
222
+ return this.gitTreeManager.startMonitoring();
412
223
  }
413
224
 
414
225
  startEvidencePolling() {
415
- if (this.pollTimer) {
416
- clearInterval(this.pollTimer);
417
- this.pollTimer = null;
418
- }
419
-
420
- if (this.pollIntervalMs <= 0) {
421
- this.pollTimer = null;
422
- return;
423
- }
226
+ return this.evidenceManager.startPolling();
227
+ }
424
228
 
425
- this.pollTimer = setInterval(() => {
426
- this.evaluateEvidenceAge('polling');
427
- }, this.pollIntervalMs);
229
+ updateUserActivity() {
230
+ return this.evidenceManager.updateUserActivity();
428
231
  }
429
232
  }
430
233