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.
- package/docs/RELEASE_NOTES.md +35 -0
- package/docs/VIOLATIONS_RESOLUTION_PLAN.md +64 -60
- package/package.json +9 -3
- package/scripts/hooks-system/.AI_TOKEN_STATUS.txt +1 -1
- package/scripts/hooks-system/.audit-reports/notifications.log +935 -0
- package/scripts/hooks-system/.audit-reports/token-monitor.log +2809 -0
- package/scripts/hooks-system/application/CompositionRoot.js +38 -22
- package/scripts/hooks-system/application/services/DynamicRulesLoader.js +2 -1
- package/scripts/hooks-system/application/services/GitTreeState.js +2 -1
- package/scripts/hooks-system/application/services/PlaybookRunner.js +1 -1
- package/scripts/hooks-system/application/services/RealtimeGuardService.js +71 -14
- package/scripts/hooks-system/application/services/guard/GuardAutoManagerService.js +31 -2
- package/scripts/hooks-system/application/services/guard/GuardConfig.js +17 -9
- package/scripts/hooks-system/application/services/guard/GuardHeartbeatMonitor.js +6 -9
- package/scripts/hooks-system/application/services/guard/GuardProcessManager.js +23 -0
- package/scripts/hooks-system/application/services/installation/GitEnvironmentService.js +1 -1
- package/scripts/hooks-system/application/services/installation/HookInstaller.js +62 -5
- package/scripts/hooks-system/application/services/installation/McpConfigurator.js +2 -1
- package/scripts/hooks-system/application/services/logging/AuditLogger.js +0 -4
- package/scripts/hooks-system/application/services/logging/UnifiedLogger.js +13 -4
- package/scripts/hooks-system/application/services/monitoring/EvidenceMonitorService.js +4 -3
- package/scripts/hooks-system/application/services/token/TokenMetricsService.js +2 -1
- package/scripts/hooks-system/bin/cli.js +15 -1
- package/scripts/hooks-system/bin/guard-env.sh +18 -38
- package/scripts/hooks-system/bin/guard-supervisor.js +5 -515
- package/scripts/hooks-system/bin/session-loader.sh +3 -262
- package/scripts/hooks-system/bin/start-guards.sh +21 -184
- package/scripts/hooks-system/bin/update-evidence.sh +10 -1161
- package/scripts/hooks-system/config/project.config.json +1 -1
- package/scripts/hooks-system/domain/events/index.js +32 -6
- package/scripts/hooks-system/domain/exceptions/index.js +87 -0
- package/scripts/hooks-system/infrastructure/ast/android/analyzers/AndroidAnalysisOrchestrator.js +3 -2
- package/scripts/hooks-system/infrastructure/ast/ast-core.js +12 -20
- package/scripts/hooks-system/infrastructure/ast/ast-intelligence.js +8 -18
- package/scripts/hooks-system/infrastructure/ast/backend/analyzers/BackendPatternDetector.js +2 -1
- package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +10 -8
- package/scripts/hooks-system/infrastructure/ast/frontend/ast-frontend.js +196 -196
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js +3 -2
- package/scripts/hooks-system/infrastructure/config/config.js +5 -0
- package/scripts/hooks-system/infrastructure/hooks/skill-activation-prompt.js +3 -2
- package/scripts/hooks-system/infrastructure/logging/UnifiedLoggerFactory.js +5 -4
- package/scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js +88 -0
- package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +17 -16
- package/scripts/hooks-system/infrastructure/telemetry/metric-scope.js +98 -0
- package/scripts/hooks-system/infrastructure/telemetry/metrics-server.js +3 -2
- 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:
|
|
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:
|
|
187
|
-
pollIntervalMs:
|
|
188
|
-
reminderIntervalMs:
|
|
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:
|
|
198
|
-
unstagedThreshold:
|
|
199
|
-
totalThreshold:
|
|
200
|
-
checkIntervalMs:
|
|
201
|
-
reminderMs:
|
|
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:
|
|
223
|
-
mainBranch:
|
|
224
|
-
autoSyncEnabled:
|
|
225
|
-
autoCleanEnabled:
|
|
226
|
-
requireClean:
|
|
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:
|
|
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:
|
|
251
|
-
staleThresholdMs:
|
|
252
|
-
autoRefreshEnabled:
|
|
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:
|
|
267
|
-
cooldownMs:
|
|
268
|
-
enabled:
|
|
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 =
|
|
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('
|
|
5
|
+
const { recordMetric } = require(path.join(__dirname, '..', '..', 'infrastructure', 'telemetry', 'metrics-logger'));
|
|
5
6
|
|
|
6
7
|
const extractFilePath = line => {
|
|
7
8
|
recordMetric({
|
|
@@ -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 =
|
|
40
|
-
this.reminderIntervalMs =
|
|
41
|
-
this.inactivityGraceMs =
|
|
42
|
-
this.pollIntervalMs =
|
|
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 =
|
|
48
|
-
this.gitTreeUnstagedThreshold =
|
|
49
|
-
this.gitTreeTotalThreshold =
|
|
50
|
-
this.gitTreeCheckIntervalMs =
|
|
51
|
-
this.gitTreeReminderMs =
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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
|
-
},
|
|
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 (
|
|
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 =
|
|
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 =
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
env.
|
|
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 =
|
|
9
|
-
this.monitorIntervalMs =
|
|
10
|
-
this.restartCooldownMs =
|
|
11
|
-
this.stopSupervisorOnExit =
|
|
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
|
-
|
|
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 =
|
|
22
|
-
env.
|
|
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
|
|
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
|