@esotech/contextuate 2.0.0 → 2.1.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.
Files changed (105) hide show
  1. package/README.md +169 -1
  2. package/dist/commands/claude.d.ts +21 -0
  3. package/dist/commands/claude.js +213 -0
  4. package/dist/commands/context.d.ts +1 -0
  5. package/dist/commands/create.d.ts +3 -0
  6. package/dist/commands/index.d.ts +4 -0
  7. package/dist/commands/init.d.ts +7 -0
  8. package/dist/commands/init.js +67 -6
  9. package/dist/commands/install.d.ts +28 -0
  10. package/dist/commands/install.js +116 -11
  11. package/dist/commands/monitor.d.ts +55 -0
  12. package/dist/commands/monitor.js +1007 -0
  13. package/dist/commands/remove.d.ts +3 -0
  14. package/dist/commands/run.d.ts +6 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +113 -1
  17. package/dist/monitor/daemon/circuit-breaker.d.ts +121 -0
  18. package/dist/monitor/daemon/circuit-breaker.js +552 -0
  19. package/dist/monitor/daemon/cli.d.ts +8 -0
  20. package/dist/monitor/daemon/cli.js +82 -0
  21. package/dist/monitor/daemon/index.d.ts +137 -0
  22. package/dist/monitor/daemon/index.js +695 -0
  23. package/dist/monitor/daemon/notifier.d.ts +25 -0
  24. package/dist/monitor/daemon/notifier.js +98 -0
  25. package/dist/monitor/daemon/processor.d.ts +89 -0
  26. package/dist/monitor/daemon/processor.js +455 -0
  27. package/dist/monitor/daemon/state.d.ts +80 -0
  28. package/dist/monitor/daemon/state.js +162 -0
  29. package/dist/monitor/daemon/watcher.d.ts +47 -0
  30. package/dist/monitor/daemon/watcher.js +171 -0
  31. package/dist/monitor/daemon/wrapper-manager.d.ts +106 -0
  32. package/dist/monitor/daemon/wrapper-manager.js +374 -0
  33. package/dist/monitor/hooks/emit-event.js +652 -0
  34. package/dist/monitor/persistence/file-store.d.ts +88 -0
  35. package/dist/monitor/persistence/file-store.js +335 -0
  36. package/dist/monitor/persistence/index.d.ts +7 -0
  37. package/dist/monitor/persistence/index.js +10 -0
  38. package/dist/monitor/server/adapters/redis.d.ts +38 -0
  39. package/dist/monitor/server/adapters/redis.js +213 -0
  40. package/dist/monitor/server/adapters/unix-socket.d.ts +33 -0
  41. package/dist/monitor/server/adapters/unix-socket.js +182 -0
  42. package/dist/monitor/server/broker.d.ts +135 -0
  43. package/dist/monitor/server/broker.js +475 -0
  44. package/dist/monitor/server/cli.d.ts +8 -0
  45. package/dist/monitor/server/cli.js +98 -0
  46. package/dist/monitor/server/fastify.d.ts +16 -0
  47. package/dist/monitor/server/fastify.js +184 -0
  48. package/dist/monitor/server/index.d.ts +36 -0
  49. package/dist/monitor/server/index.js +153 -0
  50. package/dist/monitor/server/websocket.d.ts +80 -0
  51. package/dist/monitor/server/websocket.js +453 -0
  52. package/dist/monitor/ui/assets/index-4IssW9On.js +59 -0
  53. package/dist/monitor/ui/assets/index-vo9hLe5R.css +32 -0
  54. package/dist/monitor/ui/favicon.png +0 -0
  55. package/dist/monitor/ui/index.html +14 -0
  56. package/dist/monitor/ui/logo.png +0 -0
  57. package/dist/monitor/ui/logo.svg +1 -0
  58. package/dist/runtime/driver.d.ts +16 -0
  59. package/dist/runtime/tools.d.ts +10 -0
  60. package/dist/templates/README.md +33 -7
  61. package/dist/templates/agents/aegis.md +4 -0
  62. package/dist/templates/agents/archon.md +13 -22
  63. package/dist/templates/agents/atlas.md +4 -0
  64. package/dist/templates/agents/canvas.md +4 -0
  65. package/dist/templates/agents/chronicle.md +4 -0
  66. package/dist/templates/agents/chronos.md +4 -0
  67. package/dist/templates/agents/cipher.md +4 -0
  68. package/dist/templates/agents/crucible.md +4 -0
  69. package/dist/templates/agents/echo.md +4 -0
  70. package/dist/templates/agents/forge.md +4 -0
  71. package/dist/templates/agents/ledger.md +4 -0
  72. package/dist/templates/agents/meridian.md +4 -0
  73. package/dist/templates/agents/nexus.md +4 -0
  74. package/dist/templates/agents/pythia.md +217 -0
  75. package/dist/templates/agents/scribe.md +4 -0
  76. package/dist/templates/agents/sentinel.md +4 -0
  77. package/dist/templates/agents/{oracle.md → thoth.md} +11 -7
  78. package/dist/templates/agents/unity.md +4 -0
  79. package/dist/templates/agents/vox.md +4 -0
  80. package/dist/templates/agents/weaver.md +4 -0
  81. package/dist/templates/framework-agents/documentation-expert.md +3 -3
  82. package/dist/templates/framework-agents/tools-expert.md +8 -8
  83. package/dist/templates/skills/consult.md +138 -0
  84. package/dist/templates/skills/orchestrate.md +173 -0
  85. package/dist/templates/skills/pythia.md +37 -0
  86. package/dist/templates/standards/agent-roles.md +68 -21
  87. package/dist/templates/standards/coding-standards.md +9 -26
  88. package/dist/templates/templates/context.md +17 -2
  89. package/dist/templates/templates/contextuate.md +21 -28
  90. package/dist/templates/templates/standards/go.md +167 -0
  91. package/dist/templates/templates/standards/java.md +167 -0
  92. package/dist/templates/templates/standards/javascript.md +292 -0
  93. package/dist/templates/templates/standards/php.md +181 -0
  94. package/dist/templates/templates/standards/python.md +175 -0
  95. package/dist/templates/tools/agent-creator.md +252 -0
  96. package/dist/templates/tools/agent-creator.tool.md +2 -2
  97. package/dist/templates/tools/quickref.md +216 -0
  98. package/dist/templates/tools/spawn.md +31 -0
  99. package/dist/templates/tools/standards-detector.md +301 -0
  100. package/dist/templates/version.json +1 -1
  101. package/dist/types/monitor.d.ts +660 -0
  102. package/dist/types/monitor.js +75 -0
  103. package/dist/utils/git.d.ts +9 -0
  104. package/dist/utils/tokens.d.ts +10 -0
  105. package/package.json +18 -5
@@ -0,0 +1,552 @@
1
+ "use strict";
2
+ /**
3
+ * Circuit Breaker
4
+ *
5
+ * Monitors Claude Code sessions for stagnation and takes corrective action.
6
+ * Inspired by Michael Nygard's "Release It!" circuit breaker pattern.
7
+ *
8
+ * States:
9
+ * - CLOSED: Normal operation, progress being made
10
+ * - HALF_OPEN: Warning state, monitoring for recovery
11
+ * - OPEN: Stagnation detected, intervention required
12
+ *
13
+ * Detection methods:
14
+ * - Time-based: No events for X seconds, no progress for Y seconds
15
+ * - Loop-based: N loops without file changes, M consecutive errors
16
+ *
17
+ * Actions:
18
+ * - Alert: Notify UI of state change
19
+ * - Inject prompt: Send "you're stuck" message to Claude
20
+ * - Kill: Terminate the session
21
+ * - Restart: Kill and spawn new session
22
+ */
23
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ var desc = Object.getOwnPropertyDescriptor(m, k);
26
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
27
+ desc = { enumerable: true, get: function() { return m[k]; } };
28
+ }
29
+ Object.defineProperty(o, k2, desc);
30
+ }) : (function(o, m, k, k2) {
31
+ if (k2 === undefined) k2 = k;
32
+ o[k2] = m[k];
33
+ }));
34
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
35
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
36
+ }) : function(o, v) {
37
+ o["default"] = v;
38
+ });
39
+ var __importStar = (this && this.__importStar) || (function () {
40
+ var ownKeys = function(o) {
41
+ ownKeys = Object.getOwnPropertyNames || function (o) {
42
+ var ar = [];
43
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
44
+ return ar;
45
+ };
46
+ return ownKeys(o);
47
+ };
48
+ return function (mod) {
49
+ if (mod && mod.__esModule) return mod;
50
+ var result = {};
51
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
52
+ __setModuleDefault(result, mod);
53
+ return result;
54
+ };
55
+ })();
56
+ Object.defineProperty(exports, "__esModule", { value: true });
57
+ exports.CircuitBreaker = void 0;
58
+ const cron = __importStar(require("node-cron"));
59
+ const monitor_js_1 = require("../../types/monitor.js");
60
+ /**
61
+ * Circuit Breaker class
62
+ *
63
+ * Monitors session health and takes action when stagnation is detected.
64
+ */
65
+ class CircuitBreaker {
66
+ constructor(config, wrapperManager, onAlert) {
67
+ this.sessionHealth = new Map();
68
+ this.cronJob = null;
69
+ this.pendingKills = new Map();
70
+ this.config = { ...monitor_js_1.DEFAULT_CIRCUIT_BREAKER_CONFIG, ...config };
71
+ this.wrapperManager = wrapperManager;
72
+ this.onAlert = onAlert;
73
+ }
74
+ /**
75
+ * Start the cron-based health monitoring
76
+ */
77
+ start() {
78
+ if (!this.config.enabled) {
79
+ console.log('[CircuitBreaker] Disabled by configuration');
80
+ return;
81
+ }
82
+ console.log(`[CircuitBreaker] Starting health checks: ${this.config.healthCheckInterval}`);
83
+ this.cronJob = cron.schedule(this.config.healthCheckInterval, () => {
84
+ this.runHealthChecks();
85
+ });
86
+ }
87
+ /**
88
+ * Stop health monitoring
89
+ */
90
+ stop() {
91
+ if (this.cronJob) {
92
+ this.cronJob.stop();
93
+ this.cronJob = null;
94
+ console.log('[CircuitBreaker] Stopped health checks');
95
+ }
96
+ // Clear any pending kill timers
97
+ for (const [sessionId, timeout] of this.pendingKills) {
98
+ clearTimeout(timeout);
99
+ }
100
+ this.pendingKills.clear();
101
+ }
102
+ /**
103
+ * Update configuration at runtime
104
+ */
105
+ updateConfig(config) {
106
+ const wasEnabled = this.config.enabled;
107
+ this.config = { ...this.config, ...config };
108
+ // Handle enable/disable changes
109
+ if (!wasEnabled && this.config.enabled) {
110
+ this.start();
111
+ }
112
+ else if (wasEnabled && !this.config.enabled) {
113
+ this.stop();
114
+ }
115
+ // If cron schedule changed, restart
116
+ if (this.cronJob && config.healthCheckInterval) {
117
+ this.stop();
118
+ this.start();
119
+ }
120
+ console.log('[CircuitBreaker] Configuration updated');
121
+ }
122
+ /**
123
+ * Get current configuration
124
+ */
125
+ getConfig() {
126
+ return { ...this.config };
127
+ }
128
+ /**
129
+ * Scheduled health check - runs on cron interval
130
+ */
131
+ runHealthChecks() {
132
+ const now = Date.now();
133
+ for (const [sessionId, health] of this.sessionHealth) {
134
+ // Skip sessions that are already OPEN and have no wrapper
135
+ if (health.state === 'OPEN' && !health.wrapperId)
136
+ continue;
137
+ // Skip sessions that haven't had any events yet
138
+ if (health.totalEvents === 0)
139
+ continue;
140
+ const timeSinceEvent = (now - health.lastEventTime) / 1000;
141
+ const timeSinceProgress = (now - health.lastProgressTime) / 1000;
142
+ const sessionDuration = (now - health.sessionStartTime) / 1000;
143
+ // Check 1: No events at all (process might be hung)
144
+ if (timeSinceEvent > this.config.noEventTimeout) {
145
+ this.transitionState(health, 'OPEN', 'no_events', {
146
+ message: `No events for ${Math.round(timeSinceEvent)}s`,
147
+ timeSinceEvent,
148
+ });
149
+ continue;
150
+ }
151
+ // Check 2: No progress (events happening but no file changes)
152
+ if (timeSinceProgress > this.config.noProgressTimeout) {
153
+ if (health.state === 'CLOSED') {
154
+ this.transitionState(health, 'HALF_OPEN', 'no_progress', {
155
+ message: `No file changes for ${Math.round(timeSinceProgress)}s`,
156
+ timeSinceProgress,
157
+ });
158
+ }
159
+ else if (health.state === 'HALF_OPEN' &&
160
+ timeSinceProgress > this.config.noProgressTimeout * 1.5) {
161
+ this.transitionState(health, 'OPEN', 'no_progress_extended', {
162
+ message: `Still no progress after warning`,
163
+ timeSinceProgress,
164
+ });
165
+ }
166
+ continue;
167
+ }
168
+ // Check 3: Session exceeded max duration
169
+ if (sessionDuration > this.config.maxSessionDuration) {
170
+ this.transitionState(health, 'OPEN', 'max_duration', {
171
+ message: `Session exceeded ${this.config.maxSessionDuration}s limit`,
172
+ sessionDuration,
173
+ });
174
+ continue;
175
+ }
176
+ // Check 4: Recovery detection - if we were warning but now have progress
177
+ if (health.state === 'HALF_OPEN' &&
178
+ timeSinceProgress < this.config.noProgressTimeout / 2) {
179
+ this.transitionState(health, 'CLOSED', 'recovered', {
180
+ message: 'Progress detected, circuit recovered',
181
+ });
182
+ }
183
+ }
184
+ }
185
+ /**
186
+ * Process an incoming event (called by EventProcessor)
187
+ */
188
+ processEvent(event, wrapperId) {
189
+ if (!this.config.enabled)
190
+ return;
191
+ const health = this.getOrCreateHealth(event.sessionId, wrapperId);
192
+ const now = Date.now();
193
+ // Update timestamps
194
+ health.lastEventTime = now;
195
+ health.totalEvents++;
196
+ // Track errors
197
+ if (event.eventType === 'tool_error') {
198
+ health.consecutiveErrors++;
199
+ health.totalErrors++;
200
+ health.lastError = event.data.error?.message || 'Unknown error';
201
+ // Immediate check for error threshold
202
+ if (health.consecutiveErrors >= this.config.sameErrorThreshold) {
203
+ this.transitionState(health, 'OPEN', 'error_threshold', {
204
+ message: `${health.consecutiveErrors} consecutive errors`,
205
+ lastError: health.lastError,
206
+ });
207
+ }
208
+ }
209
+ else {
210
+ // Reset consecutive errors on successful operation
211
+ health.consecutiveErrors = 0;
212
+ }
213
+ // Track progress (file modifications)
214
+ if (this.isProgressEvent(event)) {
215
+ health.lastProgressTime = now;
216
+ health.loopsSinceProgress = 0;
217
+ health.filesModified++;
218
+ // Cancel any pending kill
219
+ const pendingKill = this.pendingKills.get(event.sessionId);
220
+ if (pendingKill) {
221
+ clearTimeout(pendingKill);
222
+ this.pendingKills.delete(event.sessionId);
223
+ }
224
+ // Auto-recover from HALF_OPEN on progress
225
+ if (health.state === 'HALF_OPEN') {
226
+ this.transitionState(health, 'CLOSED', 'progress_detected', {
227
+ message: 'File modification detected',
228
+ });
229
+ }
230
+ }
231
+ // Track loop boundaries (Stop events indicate end of a Claude response)
232
+ if (event.hookType === 'Stop') {
233
+ health.loopsSinceProgress++;
234
+ // Loop-based threshold check
235
+ if (health.loopsSinceProgress >= this.config.noProgressLoops) {
236
+ const targetState = health.state === 'CLOSED' ? 'HALF_OPEN' : 'OPEN';
237
+ this.transitionState(health, targetState, 'loop_threshold', {
238
+ message: `${health.loopsSinceProgress} loops without progress`,
239
+ });
240
+ }
241
+ }
242
+ // Update recommendation based on current state
243
+ health.recommendation = this.getRecommendation(health);
244
+ }
245
+ /**
246
+ * Handle state transitions and take action
247
+ */
248
+ transitionState(health, newState, reason, context) {
249
+ const prevState = health.state;
250
+ if (prevState === newState)
251
+ return;
252
+ console.log(`[CircuitBreaker] ${health.sessionId}: ${prevState} → ${newState} (${reason})`);
253
+ health.state = newState;
254
+ health.recommendation = this.getRecommendation(health);
255
+ // Emit alert for UI
256
+ const alert = {
257
+ sessionId: health.sessionId,
258
+ wrapperId: health.wrapperId,
259
+ previousState: prevState,
260
+ newState,
261
+ reason,
262
+ message: context.message || reason,
263
+ timestamp: Date.now(),
264
+ context,
265
+ };
266
+ this.onAlert(alert);
267
+ // Take action on OPEN
268
+ if (newState === 'OPEN') {
269
+ this.takeAction(health, context);
270
+ }
271
+ }
272
+ /**
273
+ * Take intervention action
274
+ */
275
+ async takeAction(health, context) {
276
+ if (!health.wrapperId) {
277
+ console.log(`[CircuitBreaker] No wrapper for ${health.sessionId}, alert only`);
278
+ return;
279
+ }
280
+ const wrapper = this.wrapperManager.get(health.wrapperId);
281
+ if (!wrapper) {
282
+ console.log(`[CircuitBreaker] Wrapper ${health.wrapperId} not found, alert only`);
283
+ return;
284
+ }
285
+ // Build context-aware prompt
286
+ if (this.config.autoInjectPrompt) {
287
+ const prompt = this.buildInterventionPrompt(health, context);
288
+ console.log(`[CircuitBreaker] Injecting prompt to ${health.wrapperId}`);
289
+ this.wrapperManager.writeInput(health.wrapperId, prompt + '\n');
290
+ // Emit intervention alert
291
+ this.onAlert({
292
+ sessionId: health.sessionId,
293
+ wrapperId: health.wrapperId,
294
+ previousState: health.state,
295
+ newState: health.state,
296
+ reason: 'intervention_sent',
297
+ message: 'Intervention prompt injected',
298
+ timestamp: Date.now(),
299
+ context: { prompt },
300
+ });
301
+ // If autoKill is enabled, schedule a kill after grace period
302
+ if (this.config.autoKill) {
303
+ this.scheduleKill(health, wrapper);
304
+ }
305
+ return;
306
+ }
307
+ // Direct kill without prompt
308
+ if (this.config.autoKill) {
309
+ this.killAndMaybeRestart(health, wrapper);
310
+ }
311
+ }
312
+ /**
313
+ * Schedule a kill after the grace period
314
+ */
315
+ scheduleKill(health, wrapper) {
316
+ // Cancel any existing pending kill
317
+ const existing = this.pendingKills.get(health.sessionId);
318
+ if (existing) {
319
+ clearTimeout(existing);
320
+ }
321
+ console.log(`[CircuitBreaker] Scheduling kill for ${health.wrapperId} in ${this.config.gracePeriodMs}ms`);
322
+ const timeout = setTimeout(() => {
323
+ this.pendingKills.delete(health.sessionId);
324
+ // Check if still in OPEN state (might have recovered)
325
+ const currentHealth = this.sessionHealth.get(health.sessionId);
326
+ if (currentHealth?.state === 'OPEN') {
327
+ this.killAndMaybeRestart(currentHealth, wrapper);
328
+ }
329
+ }, this.config.gracePeriodMs);
330
+ this.pendingKills.set(health.sessionId, timeout);
331
+ }
332
+ /**
333
+ * Build a context-aware intervention prompt
334
+ */
335
+ buildInterventionPrompt(health, context) {
336
+ const base = this.config.stuckPrompt;
337
+ const reason = context.reason;
338
+ // Add context-specific guidance
339
+ if (reason === 'error_threshold') {
340
+ return `${base}\n\nYou've encountered the same error ${health.consecutiveErrors} times: "${health.lastError}"\nConsider: checking your assumptions, trying an alternative method, or asking for clarification.`;
341
+ }
342
+ if (reason === 'no_progress' || reason === 'no_progress_extended') {
343
+ const minutes = Math.round(context.timeSinceProgress / 60);
344
+ return `${base}\n\nNo files have been modified in ${minutes} minutes.\nIf you're researching, that's fine. If you're stuck, please describe the blocker.`;
345
+ }
346
+ if (reason === 'loop_threshold') {
347
+ return `${base}\n\nYou've completed ${health.loopsSinceProgress} iterations without making file changes.\nPlease either make progress on the task or explain what's blocking you.`;
348
+ }
349
+ if (reason === 'no_events') {
350
+ return `${base}\n\nThe session appears to have stalled. Please respond to confirm you're still working.`;
351
+ }
352
+ if (reason === 'max_duration') {
353
+ return `${base}\n\nThis session has been running for a long time. Please summarize your progress and consider wrapping up or continuing in a new session.`;
354
+ }
355
+ return base;
356
+ }
357
+ /**
358
+ * Kill wrapper and optionally restart
359
+ */
360
+ async killAndMaybeRestart(health, wrapper) {
361
+ if (!health.wrapperId)
362
+ return;
363
+ console.log(`[CircuitBreaker] Killing wrapper ${health.wrapperId}`);
364
+ this.wrapperManager.kill(health.wrapperId);
365
+ // Emit kill alert
366
+ this.onAlert({
367
+ sessionId: health.sessionId,
368
+ wrapperId: health.wrapperId,
369
+ previousState: health.state,
370
+ newState: health.state,
371
+ reason: 'session_killed',
372
+ message: 'Session terminated due to stagnation',
373
+ timestamp: Date.now(),
374
+ });
375
+ if (this.config.autoRestart) {
376
+ setTimeout(async () => {
377
+ console.log(`[CircuitBreaker] Restarting wrapper in ${wrapper.cwd}`);
378
+ try {
379
+ const newId = await this.wrapperManager.spawn({
380
+ cwd: wrapper.cwd,
381
+ args: wrapper.args,
382
+ cols: wrapper.cols,
383
+ rows: wrapper.rows,
384
+ });
385
+ // Emit restart alert
386
+ this.onAlert({
387
+ sessionId: health.sessionId,
388
+ wrapperId: newId,
389
+ previousState: health.state,
390
+ newState: 'CLOSED',
391
+ reason: 'session_restarted',
392
+ message: `Session restarted with new wrapper ${newId}`,
393
+ timestamp: Date.now(),
394
+ });
395
+ console.log(`[CircuitBreaker] New wrapper: ${newId}`);
396
+ }
397
+ catch (err) {
398
+ console.error(`[CircuitBreaker] Failed to restart:`, err);
399
+ }
400
+ }, 2000);
401
+ }
402
+ }
403
+ /**
404
+ * Check if event indicates file modification progress
405
+ */
406
+ isProgressEvent(event) {
407
+ if (event.eventType !== 'tool_result')
408
+ return false;
409
+ // File modification tools indicate progress
410
+ const progressTools = [
411
+ 'Write',
412
+ 'Edit',
413
+ 'MultiEdit',
414
+ 'NotebookEdit',
415
+ 'Bash', // Bash can modify files too
416
+ ];
417
+ const toolName = event.data.toolName || '';
418
+ // Direct match
419
+ if (progressTools.includes(toolName)) {
420
+ // For Bash, only count as progress if it looks like a write operation
421
+ if (toolName === 'Bash') {
422
+ const command = String(event.data.toolInput || '');
423
+ const writePatterns = [
424
+ /\b(echo|cat|printf)\s+.*>/,
425
+ /\b(cp|mv|rm|mkdir|touch)\b/,
426
+ /\bgit\s+(commit|add|push)\b/,
427
+ /\bnpm\s+(install|update)\b/,
428
+ ];
429
+ return writePatterns.some((p) => p.test(command));
430
+ }
431
+ return true;
432
+ }
433
+ return false;
434
+ }
435
+ /**
436
+ * Get recommendation based on current health state
437
+ */
438
+ getRecommendation(health) {
439
+ switch (health.state) {
440
+ case 'CLOSED':
441
+ return 'continue';
442
+ case 'HALF_OPEN':
443
+ return 'warn';
444
+ case 'OPEN':
445
+ return 'intervene';
446
+ default:
447
+ return 'continue';
448
+ }
449
+ }
450
+ /**
451
+ * Get or create health tracking for a session
452
+ */
453
+ getOrCreateHealth(sessionId, wrapperId) {
454
+ let health = this.sessionHealth.get(sessionId);
455
+ if (!health) {
456
+ const now = Date.now();
457
+ health = {
458
+ sessionId,
459
+ wrapperId,
460
+ state: 'CLOSED',
461
+ lastEventTime: now,
462
+ lastProgressTime: now,
463
+ sessionStartTime: now,
464
+ loopsSinceProgress: 0,
465
+ consecutiveErrors: 0,
466
+ lastError: null,
467
+ totalEvents: 0,
468
+ totalErrors: 0,
469
+ filesModified: 0,
470
+ recommendation: 'continue',
471
+ };
472
+ this.sessionHealth.set(sessionId, health);
473
+ }
474
+ // Update wrapper ID if provided and not set
475
+ if (wrapperId && !health.wrapperId) {
476
+ health.wrapperId = wrapperId;
477
+ }
478
+ return health;
479
+ }
480
+ /**
481
+ * Get health status for all sessions (for UI)
482
+ */
483
+ getAllHealth() {
484
+ return Array.from(this.sessionHealth.values());
485
+ }
486
+ /**
487
+ * Get health for a specific session
488
+ */
489
+ getSessionHealth(sessionId) {
490
+ return this.sessionHealth.get(sessionId);
491
+ }
492
+ /**
493
+ * Reset circuit for a session (manual intervention)
494
+ */
495
+ resetCircuit(sessionId) {
496
+ const health = this.sessionHealth.get(sessionId);
497
+ if (health) {
498
+ const now = Date.now();
499
+ const prevState = health.state;
500
+ health.state = 'CLOSED';
501
+ health.lastProgressTime = now;
502
+ health.loopsSinceProgress = 0;
503
+ health.consecutiveErrors = 0;
504
+ health.recommendation = 'continue';
505
+ // Cancel any pending kill
506
+ const pendingKill = this.pendingKills.get(sessionId);
507
+ if (pendingKill) {
508
+ clearTimeout(pendingKill);
509
+ this.pendingKills.delete(sessionId);
510
+ }
511
+ this.onAlert({
512
+ sessionId,
513
+ wrapperId: health.wrapperId,
514
+ previousState: prevState,
515
+ newState: 'CLOSED',
516
+ reason: 'manual_reset',
517
+ message: 'Circuit manually reset by user',
518
+ timestamp: now,
519
+ });
520
+ console.log(`[CircuitBreaker] Circuit reset for session ${sessionId}`);
521
+ }
522
+ }
523
+ /**
524
+ * Associate a wrapper with a session (called when correlation is detected)
525
+ */
526
+ associateWrapper(sessionId, wrapperId) {
527
+ const health = this.sessionHealth.get(sessionId);
528
+ if (health && !health.wrapperId) {
529
+ health.wrapperId = wrapperId;
530
+ console.log(`[CircuitBreaker] Associated session ${sessionId} with wrapper ${wrapperId}`);
531
+ }
532
+ }
533
+ /**
534
+ * Remove health tracking for a session (on session end)
535
+ */
536
+ removeSession(sessionId) {
537
+ // Cancel any pending kill
538
+ const pendingKill = this.pendingKills.get(sessionId);
539
+ if (pendingKill) {
540
+ clearTimeout(pendingKill);
541
+ this.pendingKills.delete(sessionId);
542
+ }
543
+ this.sessionHealth.delete(sessionId);
544
+ }
545
+ /**
546
+ * Check if circuit breaker is enabled
547
+ */
548
+ isEnabled() {
549
+ return this.config.enabled;
550
+ }
551
+ }
552
+ exports.CircuitBreaker = CircuitBreaker;
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Daemon CLI Entry Point
4
+ *
5
+ * This is the entry point for running the daemon as a standalone process.
6
+ * Used when starting the daemon in detached mode.
7
+ */
8
+ export {};
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * Daemon CLI Entry Point
5
+ *
6
+ * This is the entry point for running the daemon as a standalone process.
7
+ * Used when starting the daemon in detached mode.
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ const fs = __importStar(require("fs"));
44
+ const commander_1 = require("commander");
45
+ const index_js_1 = require("./index.js");
46
+ const program = new commander_1.Command();
47
+ program
48
+ .name('contextuate-daemon')
49
+ .description('Contextuate Monitor Daemon')
50
+ .option('-c, --config <path>', 'Path to config file')
51
+ .parse(process.argv);
52
+ const options = program.opts();
53
+ async function main() {
54
+ if (!options.config) {
55
+ console.error('[Error] Config file path is required (--config)');
56
+ process.exit(1);
57
+ }
58
+ // Load configuration
59
+ let config;
60
+ try {
61
+ const configContent = await fs.promises.readFile(options.config, 'utf-8');
62
+ config = JSON.parse(configContent);
63
+ }
64
+ catch (err) {
65
+ console.error(`[Error] Failed to load config: ${err.message}`);
66
+ process.exit(1);
67
+ }
68
+ // Start daemon
69
+ const daemon = await (0, index_js_1.startDaemon)(config);
70
+ // Handle shutdown signals
71
+ const shutdown = async () => {
72
+ console.log('\n[Info] Shutting down daemon...');
73
+ await daemon.stop();
74
+ process.exit(0);
75
+ };
76
+ process.on('SIGINT', shutdown);
77
+ process.on('SIGTERM', shutdown);
78
+ }
79
+ main().catch((err) => {
80
+ console.error('[Error] Fatal error:', err);
81
+ process.exit(1);
82
+ });