pumuki-ast-hooks 5.3.19 → 5.3.21

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 (46) hide show
  1. package/docs/RELEASE_NOTES.md +35 -0
  2. package/docs/VIOLATIONS_RESOLUTION_PLAN.md +64 -60
  3. package/package.json +9 -3
  4. package/scripts/hooks-system/.AI_TOKEN_STATUS.txt +1 -1
  5. package/scripts/hooks-system/.audit-reports/notifications.log +935 -0
  6. package/scripts/hooks-system/.audit-reports/token-monitor.log +2809 -0
  7. package/scripts/hooks-system/application/CompositionRoot.js +38 -22
  8. package/scripts/hooks-system/application/services/DynamicRulesLoader.js +2 -1
  9. package/scripts/hooks-system/application/services/GitTreeState.js +2 -1
  10. package/scripts/hooks-system/application/services/PlaybookRunner.js +1 -1
  11. package/scripts/hooks-system/application/services/RealtimeGuardService.js +71 -14
  12. package/scripts/hooks-system/application/services/guard/GuardAutoManagerService.js +31 -2
  13. package/scripts/hooks-system/application/services/guard/GuardConfig.js +17 -9
  14. package/scripts/hooks-system/application/services/guard/GuardHeartbeatMonitor.js +6 -9
  15. package/scripts/hooks-system/application/services/guard/GuardProcessManager.js +23 -0
  16. package/scripts/hooks-system/application/services/installation/GitEnvironmentService.js +1 -1
  17. package/scripts/hooks-system/application/services/installation/HookInstaller.js +62 -5
  18. package/scripts/hooks-system/application/services/installation/McpConfigurator.js +2 -1
  19. package/scripts/hooks-system/application/services/logging/AuditLogger.js +0 -4
  20. package/scripts/hooks-system/application/services/logging/UnifiedLogger.js +13 -4
  21. package/scripts/hooks-system/application/services/monitoring/EvidenceMonitorService.js +4 -3
  22. package/scripts/hooks-system/application/services/token/TokenMetricsService.js +2 -1
  23. package/scripts/hooks-system/bin/cli.js +15 -1
  24. package/scripts/hooks-system/bin/guard-env.sh +18 -38
  25. package/scripts/hooks-system/bin/guard-supervisor.js +5 -515
  26. package/scripts/hooks-system/bin/session-loader.sh +3 -262
  27. package/scripts/hooks-system/bin/start-guards.sh +21 -184
  28. package/scripts/hooks-system/bin/update-evidence.sh +10 -1161
  29. package/scripts/hooks-system/config/project.config.json +1 -1
  30. package/scripts/hooks-system/domain/events/index.js +32 -6
  31. package/scripts/hooks-system/domain/exceptions/index.js +87 -0
  32. package/scripts/hooks-system/infrastructure/ast/android/analyzers/AndroidAnalysisOrchestrator.js +3 -2
  33. package/scripts/hooks-system/infrastructure/ast/ast-core.js +12 -20
  34. package/scripts/hooks-system/infrastructure/ast/ast-intelligence.js +8 -18
  35. package/scripts/hooks-system/infrastructure/ast/backend/analyzers/BackendPatternDetector.js +2 -1
  36. package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +10 -8
  37. package/scripts/hooks-system/infrastructure/ast/frontend/ast-frontend.js +196 -196
  38. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js +3 -2
  39. package/scripts/hooks-system/infrastructure/config/config.js +5 -0
  40. package/scripts/hooks-system/infrastructure/hooks/skill-activation-prompt.js +3 -2
  41. package/scripts/hooks-system/infrastructure/logging/UnifiedLoggerFactory.js +5 -4
  42. package/scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js +88 -0
  43. package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +17 -16
  44. package/scripts/hooks-system/infrastructure/telemetry/metric-scope.js +98 -0
  45. package/scripts/hooks-system/infrastructure/telemetry/metrics-server.js +3 -2
  46. package/scripts/hooks-system/infrastructure/validators/enforce-english-literals.js +6 -8
@@ -21,9 +21,11 @@ const TokenMonitor = require('./services/monitoring/TokenMonitor');
21
21
  const ActivityMonitor = require('./services/monitoring/ActivityMonitor');
22
22
  const DevDocsMonitor = require('./services/monitoring/DevDocsMonitor');
23
23
  const AstMonitor = require('./services/monitoring/AstMonitor');
24
+ const AuditLogger = require('./services/logging/AuditLogger');
24
25
 
25
26
  const path = require('path');
26
27
  const fs = require('fs');
28
+ const env = require('../config/env');
27
29
 
28
30
  class CompositionRoot {
29
31
  constructor(repoRoot) {
@@ -51,7 +53,7 @@ class CompositionRoot {
51
53
  file: {
52
54
  enabled: true,
53
55
  path: path.join(this.auditDir, 'guard-audit.jsonl'),
54
- level: process.env.HOOK_LOG_LEVEL || 'info'
56
+ level: env.get('HOOK_LOG_LEVEL', env.isProd ? 'warn' : 'info')
55
57
  },
56
58
  console: {
57
59
  enabled: false,
@@ -73,6 +75,18 @@ class CompositionRoot {
73
75
  return this.instances.get('notificationService');
74
76
  }
75
77
 
78
+ getAuditLogger() {
79
+ if (!this.instances.has('auditLogger')) {
80
+ const logger = this.getLogger();
81
+ this.instances.set('auditLogger', new AuditLogger({
82
+ repoRoot: this.repoRoot,
83
+ filename: path.join('.audit_tmp', 'audit.log'),
84
+ logger
85
+ }));
86
+ }
87
+ return this.instances.get('auditLogger');
88
+ }
89
+
76
90
  // --- Infrastructure Adapters ---
77
91
 
78
92
  getNotificationAdapter() {
@@ -183,9 +197,9 @@ class CompositionRoot {
183
197
  getEvidenceMonitor() {
184
198
  if (!this.instances.has('evidenceMonitor')) {
185
199
  this.instances.set('evidenceMonitor', new EvidenceMonitor(this.repoRoot, {
186
- staleThresholdMs: Number(process.env.HOOK_GUARD_EVIDENCE_STALE_THRESHOLD || 180000),
187
- pollIntervalMs: Number(process.env.HOOK_GUARD_EVIDENCE_POLL_INTERVAL || 30000),
188
- reminderIntervalMs: Number(process.env.HOOK_GUARD_EVIDENCE_REMINDER_INTERVAL || 60000)
200
+ staleThresholdMs: env.getNumber('HOOK_GUARD_EVIDENCE_STALE_THRESHOLD', 180000),
201
+ pollIntervalMs: env.getNumber('HOOK_GUARD_EVIDENCE_POLL_INTERVAL', 30000),
202
+ reminderIntervalMs: env.getNumber('HOOK_GUARD_EVIDENCE_REMINDER_INTERVAL', 60000)
189
203
  }));
190
204
  }
191
205
  return this.instances.get('evidenceMonitor');
@@ -194,11 +208,11 @@ class CompositionRoot {
194
208
  getGitTreeMonitor() {
195
209
  if (!this.instances.has('gitTreeMonitor')) {
196
210
  this.instances.set('gitTreeMonitor', new GitTreeMonitor(this.repoRoot, {
197
- stagedThreshold: Number(process.env.HOOK_GUARD_DIRTY_TREE_STAGED_LIMIT || 10),
198
- unstagedThreshold: Number(process.env.HOOK_GUARD_DIRTY_TREE_UNSTAGED_LIMIT || 15),
199
- totalThreshold: Number(process.env.HOOK_GUARD_DIRTY_TREE_TOTAL_LIMIT || 20),
200
- checkIntervalMs: Number(process.env.HOOK_GUARD_DIRTY_TREE_INTERVAL || 60000),
201
- reminderMs: Number(process.env.HOOK_GUARD_DIRTY_TREE_REMINDER || 300000)
211
+ stagedThreshold: env.getNumber('HOOK_GUARD_DIRTY_TREE_STAGED_LIMIT', 10),
212
+ unstagedThreshold: env.getNumber('HOOK_GUARD_DIRTY_TREE_UNSTAGED_LIMIT', 15),
213
+ totalThreshold: env.getNumber('HOOK_GUARD_DIRTY_TREE_TOTAL_LIMIT', 20),
214
+ checkIntervalMs: env.getNumber('HOOK_GUARD_DIRTY_TREE_INTERVAL', 60000),
215
+ reminderMs: env.getNumber('HOOK_GUARD_DIRTY_TREE_REMINDER', 300000)
202
216
  }));
203
217
  }
204
218
  return this.instances.get('gitTreeMonitor');
@@ -219,11 +233,11 @@ class CompositionRoot {
219
233
  const github = this.getGitHubAdapter();
220
234
 
221
235
  this.instances.set('gitFlowService', new GitFlowService(this.repoRoot, {
222
- developBranch: process.env.HOOK_GUARD_GITFLOW_DEVELOP_BRANCH || 'develop',
223
- mainBranch: process.env.HOOK_GUARD_GITFLOW_MAIN_BRANCH || 'main',
224
- autoSyncEnabled: process.env.HOOK_GUARD_GITFLOW_AUTOSYNC !== 'false',
225
- autoCleanEnabled: process.env.HOOK_GUARD_GITFLOW_AUTOCLEAN !== 'false',
226
- requireClean: process.env.HOOK_GUARD_GITFLOW_REQUIRE_CLEAN !== 'false'
236
+ developBranch: env.get('HOOK_GUARD_GITFLOW_DEVELOP_BRANCH', 'develop'),
237
+ mainBranch: env.get('HOOK_GUARD_GITFLOW_MAIN_BRANCH', 'main'),
238
+ autoSyncEnabled: env.getBool('HOOK_GUARD_GITFLOW_AUTOSYNC', true),
239
+ autoCleanEnabled: env.getBool('HOOK_GUARD_GITFLOW_AUTOCLEAN', true),
240
+ requireClean: env.getBool('HOOK_GUARD_GITFLOW_REQUIRE_CLEAN', true)
227
241
  }, logger, gitQuery, gitCommand, github));
228
242
  }
229
243
  return this.instances.get('gitFlowService');
@@ -234,7 +248,7 @@ class CompositionRoot {
234
248
  const logger = this.getLogger();
235
249
  this.instances.set('activityMonitor', new ActivityMonitor({
236
250
  repoRoot: this.repoRoot,
237
- inactivityGraceMs: Number(process.env.HOOK_GUARD_INACTIVITY_GRACE_MS || 420000),
251
+ inactivityGraceMs: env.getNumber('HOOK_GUARD_INACTIVITY_GRACE_MS', 420000),
238
252
  logger
239
253
  }));
240
254
  }
@@ -247,9 +261,9 @@ class CompositionRoot {
247
261
  const notificationService = this.getNotificationService();
248
262
  this.instances.set('devDocsMonitor', new DevDocsMonitor({
249
263
  repoRoot: this.repoRoot,
250
- checkIntervalMs: Number(process.env.HOOK_GUARD_DEV_DOCS_CHECK_INTERVAL || 300000),
251
- staleThresholdMs: Number(process.env.HOOK_GUARD_DEV_DOCS_STALE_THRESHOLD || 86400000),
252
- autoRefreshEnabled: process.env.HOOK_GUARD_DEV_DOCS_AUTO_REFRESH !== 'false',
264
+ checkIntervalMs: env.getNumber('HOOK_GUARD_DEV_DOCS_CHECK_INTERVAL', 300000),
265
+ staleThresholdMs: env.getNumber('HOOK_GUARD_DEV_DOCS_STALE_THRESHOLD', 86400000),
266
+ autoRefreshEnabled: env.getBool('HOOK_GUARD_DEV_DOCS_AUTO_REFRESH', true),
253
267
  logger,
254
268
  notificationService
255
269
  }));
@@ -263,9 +277,9 @@ class CompositionRoot {
263
277
  const notificationService = this.getNotificationService();
264
278
  this.instances.set('astMonitor', new AstMonitor({
265
279
  repoRoot: this.repoRoot,
266
- debounceMs: Number(process.env.HOOK_AST_WATCH_DEBOUNCE || 8000),
267
- cooldownMs: Number(process.env.HOOK_AST_WATCH_COOLDOWN || 30000),
268
- enabled: process.env.HOOK_AST_WATCH !== 'false',
280
+ debounceMs: env.getNumber('HOOK_AST_WATCH_DEBOUNCE', 8000),
281
+ cooldownMs: env.getNumber('HOOK_AST_WATCH_COOLDOWN', 30000),
282
+ enabled: env.getBool('HOOK_AST_WATCH', true),
269
283
  logger,
270
284
  notificationService
271
285
  }));
@@ -291,6 +305,7 @@ class CompositionRoot {
291
305
  const notificationService = this.getNotificationService();
292
306
  const monitors = this.getMonitors();
293
307
  const orchestrator = this.getOrchestrator();
308
+ const auditLogger = this.getAuditLogger();
294
309
  const config = {
295
310
  debugLogPath: path.join(this.auditDir, 'guard-debug.log'),
296
311
  repoRoot: this.repoRoot
@@ -301,7 +316,8 @@ class CompositionRoot {
301
316
  notificationService,
302
317
  monitors,
303
318
  orchestration: orchestrator,
304
- config
319
+ config,
320
+ auditLogger
305
321
  }));
306
322
  }
307
323
  return this.instances.get('guardService');
@@ -1,5 +1,6 @@
1
1
  const fs = require('fs').promises;
2
2
  const path = require('path');
3
+ const env = require('../config/env');
3
4
 
4
5
  class DynamicRulesLoader {
5
6
  constructor(rulesDirectory, logger = console) {
@@ -27,7 +28,7 @@ class DynamicRulesLoader {
27
28
  return [this.rulesDirectory];
28
29
  }
29
30
 
30
- const envRaw = process.env.AST_RULES_DIRECTORIES;
31
+ const envRaw = env.get('AST_RULES_DIRECTORIES');
31
32
  if (envRaw && typeof envRaw === 'string' && envRaw.trim().length > 0) {
32
33
  return envRaw
33
34
  .split(',')
@@ -1,7 +1,8 @@
1
1
  const { execSync } = require('child_process');
2
+ const path = require('path');
2
3
 
3
4
  // Import recordMetric for prometheus metrics
4
- const { recordMetric } = require('../../../infrastructure/telemetry/metrics-logger');
5
+ const { recordMetric } = require(path.join(__dirname, '..', '..', 'infrastructure', 'telemetry', 'metrics-logger'));
5
6
 
6
7
  const extractFilePath = line => {
7
8
  recordMetric({
@@ -18,7 +18,7 @@ class PlaybookRunner {
18
18
  run(id) {
19
19
  const playbook = this.playbooks[id];
20
20
  if (!playbook) {
21
- throw new Error(`Playbook '${id}' not found`);
21
+ throw new NotFoundError(`Playbook '${id}'`);
22
22
  }
23
23
 
24
24
  for (const step of playbook.steps) {
@@ -1,6 +1,9 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { getGitTreeState, isTreeBeyondLimit } = require('./GitTreeState');
4
+ const AuditLogger = require('./logging/AuditLogger');
5
+ const { recordMetric } = require('../../infrastructure/telemetry/metrics-logger');
6
+ const env = require('../../config/env');
4
7
 
5
8
  class RealtimeGuardService {
6
9
  /**
@@ -30,39 +33,42 @@ class RealtimeGuardService {
30
33
  this.monitors = monitors || {};
31
34
  this.orchestration = orchestration;
32
35
  this.config = config || {};
36
+ this.auditLogger = dependencies.auditLogger || new AuditLogger({ repoRoot: process.cwd(), logger: this.logger });
33
37
 
34
38
  if (!this.config.debugLogPath) {
35
39
  this.config.debugLogPath = path.join(process.cwd(), '.audit-reports', 'guard-debug.log');
36
40
  }
37
41
 
38
42
  this.evidencePath = this.config.evidencePath || path.join(process.cwd(), '.AI_EVIDENCE.json');
39
- this.staleThresholdMs = Number(process.env.HOOK_GUARD_EVIDENCE_STALE_THRESHOLD || 60000);
40
- this.reminderIntervalMs = Number(process.env.HOOK_GUARD_EVIDENCE_REMINDER_INTERVAL || 60000);
41
- this.inactivityGraceMs = Number(process.env.HOOK_GUARD_INACTIVITY_GRACE_MS || 120000);
42
- this.pollIntervalMs = Number(process.env.HOOK_GUARD_EVIDENCE_POLL_INTERVAL || 30000);
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);
43
47
  this.pollTimer = null;
44
48
  this.lastStaleNotification = 0;
45
49
  this.lastUserActivityAt = 0;
46
50
 
47
- this.gitTreeStagedThreshold = Number(process.env.HOOK_GUARD_DIRTY_TREE_STAGED_LIMIT || 10);
48
- this.gitTreeUnstagedThreshold = Number(process.env.HOOK_GUARD_DIRTY_TREE_UNSTAGED_LIMIT || 15);
49
- this.gitTreeTotalThreshold = Number(process.env.HOOK_GUARD_DIRTY_TREE_TOTAL_LIMIT || 20);
50
- this.gitTreeCheckIntervalMs = Number(process.env.HOOK_GUARD_DIRTY_TREE_INTERVAL || 60000);
51
- this.gitTreeReminderMs = Number(process.env.HOOK_GUARD_DIRTY_TREE_REMINDER || 300000);
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);
52
56
  this.gitTreeTimer = null;
53
57
  this.lastDirtyTreeNotification = 0;
54
58
  this.dirtyTreeActive = false;
55
59
 
56
- this.autoRefreshCooldownMs = Number(process.env.HOOK_GUARD_EVIDENCE_AUTO_REFRESH_COOLDOWN || 180000);
60
+ this.autoRefreshCooldownMs = env.getNumber('HOOK_GUARD_EVIDENCE_AUTO_REFRESH_COOLDOWN', 180000);
57
61
  this.lastAutoRefresh = 0;
58
62
  this.autoRefreshInFlight = false;
59
63
 
60
64
  this.watchers = [];
61
- this.embedTokenMonitor = process.env.HOOK_GUARD_EMBEDDED_TOKEN_MONITOR === 'true';
65
+ this.embedTokenMonitor = env.getBool('HOOK_GUARD_EMBEDDED_TOKEN_MONITOR', false);
62
66
  }
63
67
 
64
68
  start() {
65
69
  this.logger.info('Starting RealtimeGuardService...');
70
+ this.auditLogger.record({ action: 'guard.realtime.start', resource: 'realtime_guard', status: 'success' });
71
+ recordMetric({ hook: 'realtime_guard', status: 'start' });
66
72
 
67
73
  // Start all monitors
68
74
  this._startEvidenceMonitoring();
@@ -82,6 +88,8 @@ class RealtimeGuardService {
82
88
 
83
89
  stop() {
84
90
  this.logger.info('Stopping RealtimeGuardService...');
91
+ this.auditLogger.record({ action: 'guard.realtime.stop', resource: 'realtime_guard', status: 'success' });
92
+ recordMetric({ hook: 'realtime_guard', status: 'stop' });
85
93
 
86
94
  this.watchers.forEach(w => w.close());
87
95
  this.watchers = [];
@@ -103,14 +111,27 @@ class RealtimeGuardService {
103
111
  }
104
112
 
105
113
  _startGitTreeMonitoring() {
106
- if (process.env.HOOK_GUARD_DIRTY_TREE_DISABLED === 'true') return;
114
+ if (env.getBool('HOOK_GUARD_DIRTY_TREE_DISABLED', false)) return;
107
115
 
108
116
  this.monitors.gitTree.startMonitoring((state) => {
109
117
  if (state.isBeyondLimit) {
110
118
  const message = `Git tree has too many files: ${state.total} total (${state.staged} staged, ${state.unstaged} unstaged)`;
111
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 });
112
127
  } else {
113
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' });
114
135
  }
115
136
  });
116
137
  }
@@ -124,22 +145,44 @@ class RealtimeGuardService {
124
145
  try {
125
146
  this.monitors.token.start();
126
147
  this.notify('🔋 Token monitor started', 'info');
148
+ this.auditLogger.record({
149
+ action: 'guard.token_monitor.start',
150
+ resource: 'token_monitor',
151
+ status: 'success'
152
+ });
153
+ recordMetric({ hook: 'token_monitor', status: 'start' });
127
154
  } catch (error) {
128
155
  this.notify(`Failed to start token monitor: ${error.message}`, 'error');
156
+ this.auditLogger.record({
157
+ action: 'guard.token_monitor.start',
158
+ resource: 'token_monitor',
159
+ status: 'fail',
160
+ meta: { message: error.message }
161
+ });
162
+ recordMetric({ hook: 'token_monitor', status: 'fail' });
129
163
  }
130
164
  }
131
165
 
132
166
  _startGitFlowSync() {
133
167
  if (!this.monitors.gitFlow.autoSyncEnabled) return;
134
168
 
169
+ this.auditLogger.record({
170
+ action: 'guard.gitflow.autosync.enabled',
171
+ resource: 'gitflow',
172
+ status: 'success',
173
+ meta: { intervalMs: env.getNumber('HOOK_GUARD_GITFLOW_AUTOSYNC_INTERVAL', 300000) }
174
+ });
175
+ recordMetric({ hook: 'gitflow_autosync', status: 'enabled' });
176
+
135
177
  const syncInterval = setInterval(() => {
136
178
  if (this.monitors.gitFlow.isClean()) {
137
179
  const result = this.monitors.gitFlow.syncBranches();
138
180
  if (result.success) {
139
181
  this.notify('🔄 Branches synchronized', 'info');
182
+ recordMetric({ hook: 'gitflow_autosync', status: 'sync_success' });
140
183
  }
141
184
  }
142
- }, Number(process.env.HOOK_GUARD_GITFLOW_AUTOSYNC_INTERVAL || 300000));
185
+ }, env.getNumber('HOOK_GUARD_GITFLOW_AUTOSYNC_INTERVAL', 300000));
143
186
 
144
187
  syncInterval.unref();
145
188
  }
@@ -247,11 +290,18 @@ class RealtimeGuardService {
247
290
  this.lastStaleNotification = now;
248
291
  const ageSec = Math.floor(ageMs / 1000);
249
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 });
250
300
  void this.attemptAutoRefresh('stale');
251
301
  }
252
302
 
253
303
  async attemptAutoRefresh(reason = 'manual') {
254
- if (process.env.HOOK_GUARD_AUTO_REFRESH !== 'true') {
304
+ if (!env.getBool('HOOK_GUARD_AUTO_REFRESH', false)) {
255
305
  return;
256
306
  }
257
307
 
@@ -284,6 +334,13 @@ class RealtimeGuardService {
284
334
  try {
285
335
  await this.runDirectEvidenceRefresh(reason);
286
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 });
287
344
  } finally {
288
345
  this.autoRefreshInFlight = false;
289
346
  }
@@ -10,6 +10,9 @@ const GuardConfig = require('./GuardConfig');
10
10
  const GuardNotificationHandler = require('./GuardNotificationHandler');
11
11
  const GuardMonitorLoop = require('./GuardMonitorLoop');
12
12
  const GuardHealthReminder = require('./GuardHealthReminder');
13
+ const AuditLogger = require('../logging/AuditLogger');
14
+ const envHelper = require('../../../config/env');
15
+ const { recordMetric } = require('../../../infrastructure/telemetry/metrics-logger');
13
16
 
14
17
  class GuardAutoManagerService {
15
18
  constructor({
@@ -19,9 +22,10 @@ class GuardAutoManagerService {
19
22
  fsModule = fs,
20
23
  childProcess = { spawnSync },
21
24
  timers = { setInterval, clearInterval },
22
- env = process.env,
25
+ env = envHelper,
23
26
  processRef = process,
24
- heartbeatMonitor = null
27
+ heartbeatMonitor = null,
28
+ auditLogger = null
25
29
  } = {}) {
26
30
  this.process = processRef;
27
31
 
@@ -30,6 +34,7 @@ class GuardAutoManagerService {
30
34
  this.eventLogger = new GuardEventLogger({ repoRoot, logger, fsModule });
31
35
  this.lockManager = new GuardLockManager({ repoRoot, logger, fsModule });
32
36
  this.processManager = new GuardProcessManager({ repoRoot, logger, fsModule, childProcess });
37
+ this.auditLogger = auditLogger || new AuditLogger({ repoRoot, logger });
33
38
 
34
39
  // Monitors & Handlers
35
40
  this.heartbeatMonitor = heartbeatMonitor || new GuardHeartbeatMonitor({
@@ -59,10 +64,14 @@ class GuardAutoManagerService {
59
64
  start() {
60
65
  if (!this.lockManager.acquireLock()) {
61
66
  this.eventLogger.log('Another guard auto manager instance detected. Exiting.');
67
+ this.auditLogger.record({ action: 'guard.lock.acquire', resource: 'guard_auto_manager', status: 'fail', meta: { reason: 'lock_exists' } });
68
+ recordMetric({ hook: 'guard_auto_manager', status: 'lock_fail' });
62
69
  return false;
63
70
  }
64
71
  this.lockManager.writePidFile();
65
72
  this.eventLogger.log('Guard auto manager started');
73
+ this.auditLogger.record({ action: 'guard.manager.start', resource: 'guard_auto_manager', status: 'success' });
74
+ recordMetric({ hook: 'guard_auto_manager', status: 'start' });
66
75
 
67
76
  this.ensureSupervisor('initial-start');
68
77
  this._startReminder();
@@ -94,6 +103,12 @@ class GuardAutoManagerService {
94
103
  handleMissingSupervisor() {
95
104
  this.lastHeartbeatState = { healthy: false, reason: 'missing-supervisor' };
96
105
  this.eventLogger.recordEvent('Guard supervisor no se encuentra en ejecución; reinicio automático.');
106
+ this.auditLogger.record({
107
+ action: 'guard.supervisor.missing',
108
+ resource: 'guard_supervisor',
109
+ status: 'fail',
110
+ meta: { reason: 'missing-supervisor' }
111
+ });
97
112
  this.ensureSupervisor('missing-supervisor');
98
113
  }
99
114
 
@@ -106,9 +121,21 @@ class GuardAutoManagerService {
106
121
  this.eventLogger.log(`Heartbeat degraded (${heartbeat.reason}); attempting supervisor ensure.`);
107
122
  this.lastHeartbeatRestart = now;
108
123
  this.ensureSupervisor(`heartbeat-${heartbeat.reason}`);
124
+ this.auditLogger.record({
125
+ action: 'guard.supervisor.ensure',
126
+ resource: 'guard_supervisor',
127
+ status: 'success',
128
+ meta: { reason: heartbeat.reason }
129
+ });
109
130
  } else {
110
131
  this.eventLogger.log(`Heartbeat degraded (${heartbeat.reason}); restart suppressed (cooldown).`);
111
132
  this.eventLogger.recordEvent(`Heartbeat degradado (${heartbeat.reason}); reinicio omitido por cooldown.`);
133
+ this.auditLogger.record({
134
+ action: 'guard.supervisor.ensure',
135
+ resource: 'guard_supervisor',
136
+ status: 'fail',
137
+ meta: { reason: heartbeat.reason, suppressed: true }
138
+ });
112
139
  }
113
140
  } else {
114
141
  this.eventLogger.recordEvent(`Heartbeat en estado ${heartbeat.reason}; reinicio no requerido.`);
@@ -148,6 +175,8 @@ class GuardAutoManagerService {
148
175
  this.lockManager.removePidFile();
149
176
  this.lockManager.releaseLock();
150
177
  this.healthReminder.stop();
178
+ this.auditLogger.record({ action: 'guard.manager.stop', resource: 'guard_auto_manager', status: 'success' });
179
+ recordMetric({ hook: 'guard_auto_manager', status: 'stop' });
151
180
  }
152
181
 
153
182
  ensureSupervisor(reason) {
@@ -1,14 +1,22 @@
1
+ const envHelper = require('../../../config/env');
2
+
1
3
  class GuardConfig {
2
- constructor(env = process.env) {
3
- this.healthyReminderIntervalMs = Number(env.GUARD_AUTOSTART_HEALTHY_INTERVAL || 0);
4
- this.heartbeatNotifyCooldownMs = Number(env.GUARD_AUTOSTART_NOTIFY_COOLDOWN || 60000);
5
- this.healthyReminderCooldownMs = Number(
6
- env.GUARD_AUTOSTART_HEALTHY_COOLDOWN || (this.healthyReminderIntervalMs > 0 ? this.healthyReminderIntervalMs : 0)
4
+ constructor(env = envHelper) {
5
+ const getNumber = (name, def) =>
6
+ typeof env.getNumber === 'function' ? env.getNumber(name, def) : Number(env[name] || def);
7
+ const getBool = (name, def) =>
8
+ typeof env.getBool === 'function' ? env.getBool(name, def) : (env[name] !== 'false');
9
+
10
+ this.healthyReminderIntervalMs = getNumber('GUARD_AUTOSTART_HEALTHY_INTERVAL', 0);
11
+ this.heartbeatNotifyCooldownMs = getNumber('GUARD_AUTOSTART_NOTIFY_COOLDOWN', 60000);
12
+ this.healthyReminderCooldownMs = getNumber(
13
+ 'GUARD_AUTOSTART_HEALTHY_COOLDOWN',
14
+ this.healthyReminderIntervalMs > 0 ? this.healthyReminderIntervalMs : 0
7
15
  );
8
- this.heartbeatRestartCooldownMs = Number(env.GUARD_AUTOSTART_HEARTBEAT_COOLDOWN || 60000);
9
- this.monitorIntervalMs = Number(env.GUARD_AUTOSTART_MONITOR_INTERVAL || 5000);
10
- this.restartCooldownMs = Number(env.GUARD_AUTOSTART_RESTART_COOLDOWN || 2000);
11
- this.stopSupervisorOnExit = env.GUARD_AUTOSTART_STOP_SUPERVISOR_ON_EXIT !== 'false';
16
+ this.heartbeatRestartCooldownMs = getNumber('GUARD_AUTOSTART_HEARTBEAT_COOLDOWN', 60000);
17
+ this.monitorIntervalMs = getNumber('GUARD_AUTOSTART_MONITOR_INTERVAL', 5000);
18
+ this.restartCooldownMs = getNumber('GUARD_AUTOSTART_RESTART_COOLDOWN', 2000);
19
+ this.stopSupervisorOnExit = getBool('GUARD_AUTOSTART_STOP_SUPERVISOR_ON_EXIT', true);
12
20
  }
13
21
  }
14
22
 
@@ -1,29 +1,26 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const env = require('../../../config/env');
3
4
 
4
5
  class GuardHeartbeatMonitor {
5
6
  constructor({
6
7
  repoRoot = process.cwd(),
7
8
  logger = console,
8
- fsModule = fs,
9
- env = process.env
9
+ fsModule = fs
10
10
  } = {}) {
11
11
  this.repoRoot = repoRoot;
12
12
  this.logger = logger;
13
13
  this.fs = fsModule;
14
- this.env = env;
15
-
16
- const heartbeatRelative = env.HOOK_GUARD_HEARTBEAT_PATH || path.join('.audit_tmp', 'guard-heartbeat.json');
14
+ const heartbeatRelative = env.get('HOOK_GUARD_HEARTBEAT_PATH', path.join('.audit_tmp', 'guard-heartbeat.json'));
17
15
  this.heartbeatPath = path.isAbsolute(heartbeatRelative)
18
16
  ? heartbeatRelative
19
17
  : path.join(this.repoRoot, heartbeatRelative);
20
18
 
21
- this.heartbeatMaxAgeMs = Number(
22
- env.GUARD_AUTOSTART_HEARTBEAT_MAX_AGE || env.HOOK_GUARD_HEARTBEAT_MAX_AGE || 60000
23
- );
19
+ this.heartbeatMaxAgeMs = env.getNumber('GUARD_AUTOSTART_HEARTBEAT_MAX_AGE',
20
+ env.getNumber('HOOK_GUARD_HEARTBEAT_MAX_AGE', 60000));
24
21
 
25
22
  this.heartbeatRestartReasons = new Set(
26
- (env.GUARD_AUTOSTART_HEARTBEAT_RESTART || 'missing,stale,invalid,degraded')
23
+ (env.get('GUARD_AUTOSTART_HEARTBEAT_RESTART', 'missing,stale,invalid,degraded'))
27
24
  .split(',')
28
25
  .map(entry => entry.trim().toLowerCase())
29
26
  .filter(Boolean)
@@ -15,6 +15,7 @@ class GuardProcessManager {
15
15
 
16
16
  this.supervisorPidFile = path.join(this.repoRoot, '.guard-supervisor.pid');
17
17
  this.startScript = path.join(this.repoRoot, 'bin', 'start-guards.sh');
18
+ this.busy = false;
18
19
  }
19
20
 
20
21
  isSupervisorRunning() {
@@ -58,6 +59,15 @@ class GuardProcessManager {
58
59
  }
59
60
 
60
61
  startSupervisor() {
62
+ if (this.busy) {
63
+ return {
64
+ success: false,
65
+ error: new Error('bulkhead_busy'),
66
+ stdout: '',
67
+ stderr: 'bulkhead_busy'
68
+ };
69
+ }
70
+ this.busy = true;
61
71
  try {
62
72
  const result = this.childProcess.spawnSync(this.startScript, ['start'], {
63
73
  cwd: this.repoRoot,
@@ -80,10 +90,21 @@ class GuardProcessManager {
80
90
  stdout: '',
81
91
  stderr: error.message
82
92
  };
93
+ } finally {
94
+ this.busy = false;
83
95
  }
84
96
  }
85
97
 
86
98
  stopSupervisor() {
99
+ if (this.busy) {
100
+ return {
101
+ success: false,
102
+ error: new Error('bulkhead_busy'),
103
+ stdout: '',
104
+ stderr: 'bulkhead_busy'
105
+ };
106
+ }
107
+ this.busy = true;
87
108
  try {
88
109
  const result = this.childProcess.spawnSync(this.startScript, ['stop'], {
89
110
  cwd: this.repoRoot,
@@ -106,6 +127,8 @@ class GuardProcessManager {
106
127
  stdout: '',
107
128
  stderr: error.message
108
129
  };
130
+ } finally {
131
+ this.busy = false;
109
132
  }
110
133
  }
111
134
  }
@@ -110,7 +110,7 @@ fi
110
110
 
111
111
  # Try node_modules/.bin first (works with npm install)
112
112
  if [ -f "node_modules/.bin/ast-hooks" ]; then
113
- OUTPUT=$(node_modules/.bin/ast-hooks ast 2>&1)
113
+ OUTPUT=$(node_modules/.bin/ast-hooks ast --staged 2>&1)
114
114
  EXIT_CODE=$?
115
115
  echo "$OUTPUT"
116
116
  if [ $EXIT_CODE -ne 0 ]; then