agents-library 1.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.

Potentially problematic release.


This version of agents-library might be problematic. Click here for more details.

Files changed (89) hide show
  1. package/dist/base-agent.d.ts +172 -0
  2. package/dist/base-agent.d.ts.map +1 -0
  3. package/dist/base-agent.js +255 -0
  4. package/dist/base-agent.js.map +1 -0
  5. package/dist/base-bot.d.ts +282 -0
  6. package/dist/base-bot.d.ts.map +1 -0
  7. package/dist/base-bot.js +375 -0
  8. package/dist/base-bot.js.map +1 -0
  9. package/dist/common/result.d.ts +51 -0
  10. package/dist/common/result.d.ts.map +1 -0
  11. package/dist/common/result.js +45 -0
  12. package/dist/common/result.js.map +1 -0
  13. package/dist/common/types.d.ts +57 -0
  14. package/dist/common/types.d.ts.map +1 -0
  15. package/dist/common/types.js +42 -0
  16. package/dist/common/types.js.map +1 -0
  17. package/dist/index.d.ts +94 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +108 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/kadi-event-publisher.d.ts +163 -0
  22. package/dist/kadi-event-publisher.d.ts.map +1 -0
  23. package/dist/kadi-event-publisher.js +286 -0
  24. package/dist/kadi-event-publisher.js.map +1 -0
  25. package/dist/memory/arcadedb-adapter.d.ts +159 -0
  26. package/dist/memory/arcadedb-adapter.d.ts.map +1 -0
  27. package/dist/memory/arcadedb-adapter.js +314 -0
  28. package/dist/memory/arcadedb-adapter.js.map +1 -0
  29. package/dist/memory/file-storage-adapter.d.ts +122 -0
  30. package/dist/memory/file-storage-adapter.d.ts.map +1 -0
  31. package/dist/memory/file-storage-adapter.js +352 -0
  32. package/dist/memory/file-storage-adapter.js.map +1 -0
  33. package/dist/memory/memory-service.d.ts +208 -0
  34. package/dist/memory/memory-service.d.ts.map +1 -0
  35. package/dist/memory/memory-service.js +410 -0
  36. package/dist/memory/memory-service.js.map +1 -0
  37. package/dist/memory/types.d.ts +126 -0
  38. package/dist/memory/types.d.ts.map +1 -0
  39. package/dist/memory/types.js +41 -0
  40. package/dist/memory/types.js.map +1 -0
  41. package/dist/producer-tool-utils.d.ts +474 -0
  42. package/dist/producer-tool-utils.d.ts.map +1 -0
  43. package/dist/producer-tool-utils.js +664 -0
  44. package/dist/producer-tool-utils.js.map +1 -0
  45. package/dist/providers/anthropic-provider.d.ts +160 -0
  46. package/dist/providers/anthropic-provider.d.ts.map +1 -0
  47. package/dist/providers/anthropic-provider.js +527 -0
  48. package/dist/providers/anthropic-provider.js.map +1 -0
  49. package/dist/providers/model-manager-provider.d.ts +91 -0
  50. package/dist/providers/model-manager-provider.d.ts.map +1 -0
  51. package/dist/providers/model-manager-provider.js +355 -0
  52. package/dist/providers/model-manager-provider.js.map +1 -0
  53. package/dist/providers/provider-manager.d.ts +111 -0
  54. package/dist/providers/provider-manager.d.ts.map +1 -0
  55. package/dist/providers/provider-manager.js +337 -0
  56. package/dist/providers/provider-manager.js.map +1 -0
  57. package/dist/providers/types.d.ts +145 -0
  58. package/dist/providers/types.d.ts.map +1 -0
  59. package/dist/providers/types.js +23 -0
  60. package/dist/providers/types.js.map +1 -0
  61. package/dist/shadow-agent-factory.d.ts +623 -0
  62. package/dist/shadow-agent-factory.d.ts.map +1 -0
  63. package/dist/shadow-agent-factory.js +1117 -0
  64. package/dist/shadow-agent-factory.js.map +1 -0
  65. package/dist/types/agent-config.d.ts +307 -0
  66. package/dist/types/agent-config.d.ts.map +1 -0
  67. package/dist/types/agent-config.js +15 -0
  68. package/dist/types/agent-config.js.map +1 -0
  69. package/dist/types/event-schemas.d.ts +358 -0
  70. package/dist/types/event-schemas.d.ts.map +1 -0
  71. package/dist/types/event-schemas.js +188 -0
  72. package/dist/types/event-schemas.js.map +1 -0
  73. package/dist/types/tool-schemas.d.ts +498 -0
  74. package/dist/types/tool-schemas.d.ts.map +1 -0
  75. package/dist/types/tool-schemas.js +457 -0
  76. package/dist/types/tool-schemas.js.map +1 -0
  77. package/dist/utils/logger.d.ts +135 -0
  78. package/dist/utils/logger.d.ts.map +1 -0
  79. package/dist/utils/logger.js +205 -0
  80. package/dist/utils/logger.js.map +1 -0
  81. package/dist/utils/timer.d.ts +186 -0
  82. package/dist/utils/timer.d.ts.map +1 -0
  83. package/dist/utils/timer.js +211 -0
  84. package/dist/utils/timer.js.map +1 -0
  85. package/dist/worker-agent-factory.d.ts +688 -0
  86. package/dist/worker-agent-factory.d.ts.map +1 -0
  87. package/dist/worker-agent-factory.js +1517 -0
  88. package/dist/worker-agent-factory.js.map +1 -0
  89. package/package.json +38 -0
@@ -0,0 +1,1117 @@
1
+ /**
2
+ * Shadow Agent Factory
3
+ * =====================
4
+ *
5
+ * Factory for creating shadow agents (backup/monitoring agents) with
6
+ * configuration-driven instantiation and shared infrastructure.
7
+ *
8
+ * Architecture Pattern: **Composition over Inheritance**
9
+ * - BaseShadowAgent COMPOSES with BaseBot (does NOT extend)
10
+ * - Uses delegation pattern to access BaseBot's circuit breaker and retry logic
11
+ * - This avoids tight coupling and allows flexible behavior customization
12
+ *
13
+ * Design Principles:
14
+ * - Factory pattern for consistent agent creation
15
+ * - Composition over inheritance for flexibility
16
+ * - Template method pattern for lifecycle management (start/stop)
17
+ * - Observer pattern for filesystem and git ref watching
18
+ *
19
+ * Shadow Agent Responsibilities:
20
+ * - Monitor worker agent worktrees for file changes
21
+ * - Create granular backup commits in shadow worktrees
22
+ * - Mirror worker commits to shadow branch
23
+ * - Publish backup events to KĀDI broker
24
+ *
25
+ * @module shadow-agent-factory
26
+ */
27
+ import { KadiClient, z } from '@kadi.build/core';
28
+ import chokidar from 'chokidar';
29
+ import fs from 'fs';
30
+ import path from 'path';
31
+ import { execSync } from 'child_process';
32
+ import { logger, MODULE_AGENT } from './utils/logger.js';
33
+ import { timer } from './utils/timer.js';
34
+ // ============================================================================
35
+ // BaseShadowAgent Class
36
+ // ============================================================================
37
+ /**
38
+ * Base class for shadow agents (backup/monitoring agents)
39
+ *
40
+ * **CIRCUIT BREAKER PATTERN**: This class implements its own circuit breaker for git operations.
41
+ * - Tracks consecutive failures and opens circuit after threshold
42
+ * - Prevents cascading failures from overwhelming the system
43
+ * - Auto-resets circuit after timeout period
44
+ * - Provides resilient git operations with retry logic
45
+ *
46
+ * Why Not Use BaseBot?
47
+ * - BaseBot is designed for chat bots and requires Anthropic API key
48
+ * - Shadow agents don't need Claude integration, just git operations
49
+ * - Simpler, focused circuit breaker implementation for git-specific needs
50
+ *
51
+ * Shadow Agent Architecture:
52
+ * 1. **Filesystem Watcher**: Monitors worker worktree for file operations (create/modify/delete)
53
+ * 2. **Git Ref Watcher**: Monitors worker commits to mirror them in shadow worktree
54
+ * 3. **Atomic Git Operations**: add + commit for each monitored event
55
+ * 4. **Circuit Breaker**: Error handling with retry and fallback via BaseBot
56
+ * 5. **Event Publishing**: Publishes backup completion/failure events
57
+ *
58
+ * Lifecycle:
59
+ * 1. Constructor: Initialize configuration and compose utilities
60
+ * 2. start(): Connect to broker, initialize watchers, subscribe to events
61
+ * 3. [Monitoring happens asynchronously via filesystem/git watchers]
62
+ * 4. stop(): Cleanup watchers, unsubscribe, disconnect from broker
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * const config: ShadowAgentConfig = {
67
+ * role: 'artist',
68
+ * workerWorktreePath: 'C:/p4/Personal/SD/agent-playground-artist',
69
+ * shadowWorktreePath: 'C:/p4/Personal/SD/shadow-agent-playground-artist',
70
+ * workerBranch: 'agent-artist',
71
+ * shadowBranch: 'shadow-agent-artist',
72
+ * brokerUrl: 'ws://localhost:8080/kadi',
73
+ * networks: ['kadi'],
74
+ * debounceMs: 1000
75
+ * };
76
+ *
77
+ * const agent = new BaseShadowAgent(config);
78
+ * await agent.start();
79
+ * // Agent now monitors worker worktree and creates shadow backups
80
+ * ```
81
+ */
82
+ export class BaseShadowAgent {
83
+ /**
84
+ * KĀDI client for broker communication
85
+ *
86
+ * Used for:
87
+ * - Subscribing to events
88
+ * - Publishing backup completion/failure events
89
+ * - Accessing broker protocol for tool invocation
90
+ */
91
+ client;
92
+ /**
93
+ * Agent role (matches corresponding worker agent)
94
+ *
95
+ * Used for:
96
+ * - Event payload agent identification (shadow-agent-{role})
97
+ * - Logging and identification
98
+ */
99
+ role;
100
+ /**
101
+ * Absolute path to worker agent's git worktree
102
+ *
103
+ * Shadow agent watches this directory for file changes.
104
+ * This is the source of truth for file operations.
105
+ *
106
+ * @example 'C:/p4/Personal/SD/agent-playground-artist'
107
+ */
108
+ workerWorktreePath;
109
+ /**
110
+ * Absolute path to shadow agent's git worktree
111
+ *
112
+ * Shadow agent creates mirror commits in this directory.
113
+ * Must be a separate git repository from worker worktree.
114
+ *
115
+ * @example 'C:/p4/Personal/SD/shadow-agent-playground-artist'
116
+ */
117
+ shadowWorktreePath;
118
+ /**
119
+ * Git branch name in worker worktree to monitor
120
+ *
121
+ * Shadow agent watches for commits on this branch.
122
+ *
123
+ * @example 'agent-artist'
124
+ */
125
+ workerBranch;
126
+ /**
127
+ * Git branch name in shadow worktree for mirror commits
128
+ *
129
+ * Shadow agent creates commits on this branch.
130
+ *
131
+ * @example 'shadow-agent-artist'
132
+ */
133
+ shadowBranch;
134
+ /**
135
+ * Debounce delay in milliseconds for file change events
136
+ *
137
+ * Prevents creating multiple commits for rapid file changes.
138
+ * Shadow agent waits this duration after last change before creating commit.
139
+ *
140
+ * @default 1000
141
+ */
142
+ debounceMs;
143
+ /**
144
+ * Circuit breaker state for git operations
145
+ *
146
+ * Tracks consecutive failures and blocks operations when threshold exceeded.
147
+ * Prevents cascading failures and allows system to recover.
148
+ */
149
+ gitCircuitOpen = false;
150
+ /**
151
+ * Consecutive git operation failure count
152
+ *
153
+ * Incremented on each failure, reset to 0 on success.
154
+ * Circuit opens when count reaches MAX_GIT_FAILURES threshold.
155
+ */
156
+ gitFailureCount = 0;
157
+ /**
158
+ * Maximum git failures before circuit opens
159
+ *
160
+ * @default 5
161
+ */
162
+ MAX_GIT_FAILURES = 5;
163
+ /**
164
+ * Circuit breaker reset timeout in milliseconds
165
+ *
166
+ * After this duration, circuit automatically closes and retries are allowed.
167
+ *
168
+ * @default 60000 (1 minute)
169
+ */
170
+ CIRCUIT_RESET_TIME = 60000;
171
+ /**
172
+ * Full agent configuration
173
+ *
174
+ * Stored for reference and potential reconfiguration.
175
+ * Currently unused but reserved for future features (e.g., hot-reloading config).
176
+ */
177
+ // @ts-expect-error - Reserved for future use (hot-reloading config)
178
+ config;
179
+ /**
180
+ * Whether this agent delegates connection management to a BaseAgent instance.
181
+ * When true, start() skips broker connection and stop() skips disconnection.
182
+ */
183
+ usesBaseAgent;
184
+ /**
185
+ * Filesystem watcher instance for monitoring worker worktree
186
+ *
187
+ * Monitors file operations (create, modify, delete) in worker worktree.
188
+ * Null until start() is called.
189
+ */
190
+ fsWatcher = null;
191
+ /**
192
+ * Git ref watcher instance for monitoring worker branch commits
193
+ *
194
+ * Watches .git/refs/heads/{workerBranch} file for commit SHA changes.
195
+ * Uses fs.watch (not chokidar) for lightweight ref monitoring.
196
+ * Null until start() is called.
197
+ */
198
+ refWatcher = null;
199
+ /**
200
+ * Previous commit SHA from worker branch
201
+ *
202
+ * Stores last known commit SHA to detect actual commit changes.
203
+ * Used to differentiate real commits from other ref updates.
204
+ * Null until first commit is detected.
205
+ */
206
+ previousCommitSha = null;
207
+ /**
208
+ * Set of changed file paths awaiting backup processing
209
+ *
210
+ * Stores relative file paths from worker worktree for batch processing.
211
+ * Debounced to avoid rapid-fire commits for the same file.
212
+ *
213
+ * Key: Absolute file path
214
+ * Value: Debounce timeout handle
215
+ */
216
+ debounceMap = new Map();
217
+ /**
218
+ * Debounce timeout for git ref watcher
219
+ *
220
+ * Stores timeout handle for debouncing ref change events.
221
+ * Prevents processing rapid ref updates.
222
+ */
223
+ refDebounceTimeout = null;
224
+ /**
225
+ * Create a new BaseShadowAgent instance
226
+ *
227
+ * Initializes all configuration properties and composes utility classes
228
+ * (BaseBot, KadiEventPublisher). Does NOT connect to broker or start watchers yet -
229
+ * call start() to begin monitoring.
230
+ *
231
+ * @param config - Shadow agent configuration with all required fields
232
+ *
233
+ * @example
234
+ * ```typescript
235
+ * const agent = new BaseShadowAgent({
236
+ * role: 'artist',
237
+ * workerWorktreePath: 'C:/p4/Personal/SD/agent-playground-artist',
238
+ * shadowWorktreePath: 'C:/p4/Personal/SD/shadow-agent-playground-artist',
239
+ * workerBranch: 'agent-artist',
240
+ * shadowBranch: 'shadow-agent-artist',
241
+ * brokerUrl: 'ws://localhost:8080/kadi',
242
+ * networks: ['kadi'],
243
+ * debounceMs: 1000
244
+ * });
245
+ * ```
246
+ */
247
+ constructor(config, baseAgent) {
248
+ // Start timer for performance tracking
249
+ timer.start('shadow-factory');
250
+ // Store configuration
251
+ this.config = config;
252
+ this.role = config.role;
253
+ this.workerWorktreePath = config.workerWorktreePath;
254
+ this.shadowWorktreePath = config.shadowWorktreePath;
255
+ this.workerBranch = config.workerBranch;
256
+ this.shadowBranch = config.shadowBranch;
257
+ this.debounceMs = config.debounceMs || 1000;
258
+ // Initialize KĀDI client — delegate to BaseAgent if provided, else create own
259
+ if (baseAgent) {
260
+ this.client = baseAgent.client;
261
+ this.usesBaseAgent = true;
262
+ logger.info(MODULE_AGENT, ' ✅ Using BaseAgent client (connection managed externally)', timer.elapsed('shadow-factory'));
263
+ }
264
+ else {
265
+ this.client = new KadiClient({
266
+ name: `shadow-agent-${config.role}`,
267
+ version: '1.0.0',
268
+ brokers: {
269
+ default: { url: config.brokerUrl, networks: config.networks }
270
+ },
271
+ defaultBroker: 'default',
272
+ });
273
+ this.usesBaseAgent = false;
274
+ }
275
+ logger.info(MODULE_AGENT, `🔧 BaseShadowAgent initialized for role: ${this.role}`, timer.elapsed('shadow-factory'));
276
+ logger.info(MODULE_AGENT, ` Worker worktree: ${this.workerWorktreePath}`, timer.elapsed('shadow-factory'));
277
+ logger.info(MODULE_AGENT, ` Shadow worktree: ${this.shadowWorktreePath}`, timer.elapsed('shadow-factory'));
278
+ logger.info(MODULE_AGENT, ` Worker branch: ${this.workerBranch}`, timer.elapsed('shadow-factory'));
279
+ logger.info(MODULE_AGENT, ` Shadow branch: ${this.shadowBranch}`, timer.elapsed('shadow-factory'));
280
+ logger.info(MODULE_AGENT, ` Debounce delay: ${this.debounceMs}ms`, timer.elapsed('shadow-factory'));
281
+ }
282
+ /**
283
+ * Start the shadow agent
284
+ *
285
+ * Performs initialization sequence:
286
+ * 1. Connect to KĀDI broker
287
+ * 2. Initialize broker protocol
288
+ * 3. Connect event publisher
289
+ * 4. Setup filesystem watcher for worker worktree
290
+ * 5. Setup git ref watcher for worker branch
291
+ * 6. Enter monitoring loop (non-blocking)
292
+ *
293
+ * After start() completes, the agent is ready to monitor file changes and create backups.
294
+ *
295
+ * @throws {Error} If broker connection fails after all retries
296
+ *
297
+ * @example
298
+ * ```typescript
299
+ * const agent = new BaseShadowAgent(config);
300
+ * await agent.start();
301
+ * console.log('Shadow agent is now monitoring worker worktree');
302
+ * ```
303
+ */
304
+ async start() {
305
+ logger.info(MODULE_AGENT, `🚀 Starting shadow agent for role: ${this.role}`, timer.elapsed('shadow-factory'));
306
+ // Connect to KĀDI broker (skip if BaseAgent manages connection)
307
+ if (this.usesBaseAgent) {
308
+ logger.info(MODULE_AGENT, ' ✅ Broker connection managed by BaseAgent (skipping)', timer.elapsed('shadow-factory'));
309
+ }
310
+ else {
311
+ logger.info(MODULE_AGENT, ' → Connecting to KĀDI broker...', timer.elapsed('shadow-factory'));
312
+ try {
313
+ await this.client.connect();
314
+ logger.info(MODULE_AGENT, ' ✅ Connected to KĀDI broker', timer.elapsed('shadow-factory'));
315
+ }
316
+ catch (error) {
317
+ logger.error(MODULE_AGENT, '❌ Broker connection error', timer.elapsed('shadow-factory'), error);
318
+ process.exit(1);
319
+ }
320
+ }
321
+ // Setup filesystem watcher for worker worktree
322
+ await this.setupFilesystemWatcher();
323
+ logger.info(MODULE_AGENT, '✅ Filesystem watcher initialized', timer.elapsed('shadow-factory'));
324
+ // Setup git ref watcher for worker branch
325
+ await this.setupGitRefWatcher();
326
+ logger.info(MODULE_AGENT, '✅ Git ref watcher initialized', timer.elapsed('shadow-factory'));
327
+ logger.info(MODULE_AGENT, '✅ Shadow agent started and monitoring', timer.elapsed('shadow-factory'));
328
+ }
329
+ /**
330
+ * Stop the shadow agent
331
+ *
332
+ * Performs cleanup sequence:
333
+ * 1. Stop filesystem watcher
334
+ * 2. Stop git ref watcher
335
+ * 3. Clear debounce timers
336
+ * 4. Disconnect event publisher
337
+ * 5. Disconnect KĀDI client
338
+ * 6. Clear protocol reference
339
+ *
340
+ * After stop() completes, the agent is fully shut down and can be safely destroyed.
341
+ *
342
+ * @example
343
+ * ```typescript
344
+ * await agent.stop();
345
+ * console.log('Shadow agent has been stopped');
346
+ * ```
347
+ */
348
+ async stop() {
349
+ logger.info(MODULE_AGENT, `🛑 Stopping shadow agent for role: ${this.role}`, timer.elapsed('shadow-factory'));
350
+ // Stop filesystem watcher
351
+ if (this.fsWatcher) {
352
+ logger.info(MODULE_AGENT, '🛑 Stopping filesystem watcher...', timer.elapsed('shadow-factory'));
353
+ await this.fsWatcher.close();
354
+ this.fsWatcher = null;
355
+ logger.info(MODULE_AGENT, '✅ Filesystem watcher stopped', timer.elapsed('shadow-factory'));
356
+ }
357
+ // Stop git ref watcher
358
+ if (this.refWatcher) {
359
+ logger.info(MODULE_AGENT, '🛑 Stopping git ref watcher...', timer.elapsed('shadow-factory'));
360
+ this.refWatcher.close();
361
+ this.refWatcher = null;
362
+ logger.info(MODULE_AGENT, '✅ Git ref watcher stopped', timer.elapsed('shadow-factory'));
363
+ }
364
+ // Clear ref debounce timeout
365
+ if (this.refDebounceTimeout) {
366
+ logger.info(MODULE_AGENT, '🛑 Clearing ref debounce timeout...', timer.elapsed('shadow-factory'));
367
+ clearTimeout(this.refDebounceTimeout);
368
+ this.refDebounceTimeout = null;
369
+ logger.info(MODULE_AGENT, '✅ Ref debounce timeout cleared', timer.elapsed('shadow-factory'));
370
+ }
371
+ // Clear all pending debounce timers
372
+ if (this.debounceMap.size > 0) {
373
+ logger.info(MODULE_AGENT, `🛑 Clearing ${this.debounceMap.size} pending debounce timers...`, timer.elapsed('shadow-factory'));
374
+ for (const timeout of this.debounceMap.values()) {
375
+ clearTimeout(timeout);
376
+ }
377
+ this.debounceMap.clear();
378
+ logger.info(MODULE_AGENT, '✅ Debounce timers cleared', timer.elapsed('shadow-factory'));
379
+ }
380
+ // Disconnect KĀDI client (skip if BaseAgent manages connection)
381
+ if (this.usesBaseAgent) {
382
+ logger.info(MODULE_AGENT, ' ✅ Broker disconnection managed by BaseAgent (skipping)', timer.elapsed('shadow-factory'));
383
+ }
384
+ else {
385
+ logger.info(MODULE_AGENT, ' → Disconnecting from KĀDI broker...', timer.elapsed('shadow-factory'));
386
+ await this.client.disconnect();
387
+ logger.info(MODULE_AGENT, ' ✅ Disconnected from KĀDI broker', timer.elapsed('shadow-factory'));
388
+ }
389
+ logger.info(MODULE_AGENT, '✅ Shadow agent stopped', timer.elapsed('shadow-factory'));
390
+ }
391
+ /**
392
+ * Setup filesystem watcher for worker worktree
393
+ *
394
+ * Monitors worker worktree for file operations (create, modify, delete) using chokidar.
395
+ * File changes are debounced and stored for batch backup processing.
396
+ *
397
+ * Configuration:
398
+ * - Watches: config.workerWorktreePath
399
+ * - Excludes: .git directory, node_modules, .env files
400
+ * - Debounce: config.debounceMs (default: 1000ms)
401
+ * - Stability threshold: Waits for file writes to complete
402
+ *
403
+ * Event Handling:
404
+ * - 'add': File created in worktree
405
+ * - 'change': Existing file modified
406
+ * - 'unlink': File deleted from worktree
407
+ * - 'error': Watcher errors (logged but non-fatal)
408
+ * - 'ready': Watcher initialization complete
409
+ *
410
+ * Debouncing Strategy:
411
+ * - Stores timeout handle in debounceMap for each file
412
+ * - Clears previous timeout if file changes again before debounce completes
413
+ * - Only processes file after debounceMs of inactivity
414
+ * - Prevents rapid-fire commits for the same file
415
+ *
416
+ * @throws {Error} If watcher initialization fails
417
+ *
418
+ * @example
419
+ * ```typescript
420
+ * await this.setupFilesystemWatcher();
421
+ * // Watcher is now monitoring worker worktree for file changes
422
+ * ```
423
+ */
424
+ async setupFilesystemWatcher() {
425
+ logger.info(MODULE_AGENT, `👁️ Setting up filesystem watcher: ${this.workerWorktreePath}`, timer.elapsed('shadow-factory'));
426
+ // Create chokidar watcher with configuration
427
+ this.fsWatcher = chokidar.watch(this.workerWorktreePath, {
428
+ persistent: true,
429
+ ignoreInitial: true, // Don't trigger for existing files on startup
430
+ ignored: [
431
+ '**/node_modules/**',
432
+ '**/.git/**',
433
+ '**/.git', // Also ignore .git file (for worktrees)
434
+ '**/.env',
435
+ '**/.env.*'
436
+ ],
437
+ awaitWriteFinish: {
438
+ stabilityThreshold: this.debounceMs,
439
+ pollInterval: 100
440
+ }
441
+ });
442
+ // Event: File created
443
+ this.fsWatcher.on('add', (filePath) => {
444
+ logger.info(MODULE_AGENT, `➕ File created: ${filePath}`, timer.elapsed('shadow-factory'));
445
+ // Debounce to avoid rapid-fire commits
446
+ if (this.debounceMap.has(filePath)) {
447
+ clearTimeout(this.debounceMap.get(filePath));
448
+ }
449
+ const timeout = setTimeout(async () => {
450
+ logger.info(MODULE_AGENT, `📝 Processing created file: ${filePath}`, timer.elapsed('shadow-factory'));
451
+ const relativePath = path.relative(this.workerWorktreePath, filePath);
452
+ await this.createShadowBackup('Created', relativePath);
453
+ this.debounceMap.delete(filePath);
454
+ }, this.debounceMs);
455
+ this.debounceMap.set(filePath, timeout);
456
+ });
457
+ // Event: File modified
458
+ this.fsWatcher.on('change', (filePath) => {
459
+ logger.info(MODULE_AGENT, `✏️ File modified: ${filePath}`, timer.elapsed('shadow-factory'));
460
+ // Debounce to avoid rapid-fire commits
461
+ if (this.debounceMap.has(filePath)) {
462
+ clearTimeout(this.debounceMap.get(filePath));
463
+ }
464
+ const timeout = setTimeout(async () => {
465
+ logger.info(MODULE_AGENT, `📝 Processing modified file: ${filePath}`, timer.elapsed('shadow-factory'));
466
+ const relativePath = path.relative(this.workerWorktreePath, filePath);
467
+ await this.createShadowBackup('Modified', relativePath);
468
+ this.debounceMap.delete(filePath);
469
+ }, this.debounceMs);
470
+ this.debounceMap.set(filePath, timeout);
471
+ });
472
+ // Event: File deleted
473
+ this.fsWatcher.on('unlink', (filePath) => {
474
+ logger.info(MODULE_AGENT, `🗑️ File deleted: ${filePath}`, timer.elapsed('shadow-factory'));
475
+ // Debounce to avoid rapid-fire commits
476
+ if (this.debounceMap.has(filePath)) {
477
+ clearTimeout(this.debounceMap.get(filePath));
478
+ }
479
+ const timeout = setTimeout(async () => {
480
+ logger.info(MODULE_AGENT, `📝 Processing deleted file: ${filePath}`, timer.elapsed('shadow-factory'));
481
+ const relativePath = path.relative(this.workerWorktreePath, filePath);
482
+ await this.createShadowBackup('Deleted', relativePath);
483
+ this.debounceMap.delete(filePath);
484
+ }, this.debounceMs);
485
+ this.debounceMap.set(filePath, timeout);
486
+ });
487
+ // Event: Watcher error
488
+ this.fsWatcher.on('error', (error) => {
489
+ logger.error(MODULE_AGENT, '❌ Filesystem watcher error', timer.elapsed('shadow-factory'), error);
490
+ // Non-fatal - watcher continues operating
491
+ });
492
+ // Event: Watcher ready
493
+ this.fsWatcher.on('ready', () => {
494
+ logger.info(MODULE_AGENT, '✅ Filesystem watcher ready', timer.elapsed('shadow-factory'));
495
+ });
496
+ }
497
+ /**
498
+ * Setup git ref watcher for worker branch commits
499
+ *
500
+ * Monitors worker branch ref file (.git/refs/heads/{workerBranch}) for commit SHA changes
501
+ * using fs.watch. Detects new commits and triggers createShadowBackup after debounce period.
502
+ *
503
+ * Architecture:
504
+ * - Uses fs.watch (not chokidar) for lightweight ref monitoring
505
+ * - Reads commit SHA from ref file on each change
506
+ * - Compares with previousCommitSha to detect actual commits
507
+ * - Debounces to handle rapid ref updates (e.g., during rebase)
508
+ * - Triggers backup only for real commit changes
509
+ *
510
+ * Ref File Location:
511
+ * - {workerWorktreePath}/.git/refs/heads/{workerBranch}
512
+ * - Contains commit SHA as plain text (40 hex characters)
513
+ * - Updated by git on each commit to branch
514
+ *
515
+ * Change Detection Strategy:
516
+ * 1. fs.watch fires on any ref file modification
517
+ * 2. Read current SHA from ref file
518
+ * 3. Compare with previousCommitSha
519
+ * 4. If different, debounce and trigger backup
520
+ * 5. Update previousCommitSha for next comparison
521
+ *
522
+ * Debouncing:
523
+ * - Uses config.debounceMs delay (default: 1000ms)
524
+ * - Clears previous timeout if ref changes again
525
+ * - Prevents multiple backups during rapid commits
526
+ *
527
+ * @throws {Error} If ref file doesn't exist or can't be watched
528
+ *
529
+ * @example
530
+ * ```typescript
531
+ * await this.setupGitRefWatcher();
532
+ * // Watcher is now monitoring worker branch for commits
533
+ * ```
534
+ */
535
+ async setupGitRefWatcher() {
536
+ // Construct path to worker branch ref file
537
+ // Handle both regular repos and git worktrees
538
+ let gitDir = path.join(this.workerWorktreePath, '.git');
539
+ // Check if .git is a file (worktree) or directory (regular repo)
540
+ if (fs.existsSync(gitDir)) {
541
+ const gitStat = fs.statSync(gitDir);
542
+ if (gitStat.isFile()) {
543
+ // This is a worktree - read the .git file to get actual git directory
544
+ const gitFileContent = fs.readFileSync(gitDir, 'utf-8').trim();
545
+ const match = gitFileContent.match(/^gitdir:\s*(.+)$/);
546
+ if (match) {
547
+ gitDir = match[1].trim();
548
+ logger.info(MODULE_AGENT, `📁 Detected git worktree, actual git dir: ${gitDir}`, timer.elapsed('shadow-factory'));
549
+ // For worktrees, refs are stored in the common (main) git directory
550
+ // Read the commondir file to get the path to the main git directory
551
+ const commondirPath = path.join(gitDir, 'commondir');
552
+ if (fs.existsSync(commondirPath)) {
553
+ const commondirContent = fs.readFileSync(commondirPath, 'utf-8').trim();
554
+ // commondir contains a relative path to the main git directory
555
+ const mainGitDir = path.resolve(gitDir, commondirContent);
556
+ logger.info(MODULE_AGENT, `📁 Worktree refs stored in common dir: ${mainGitDir}`, timer.elapsed('shadow-factory'));
557
+ gitDir = mainGitDir;
558
+ }
559
+ }
560
+ }
561
+ }
562
+ const refFilePath = path.join(gitDir, 'refs/heads', this.workerBranch);
563
+ logger.info(MODULE_AGENT, `👁️ Setting up git ref watcher: ${refFilePath}`, timer.elapsed('shadow-factory'));
564
+ // Verify ref file exists before watching
565
+ if (!fs.existsSync(refFilePath)) {
566
+ logger.warn(MODULE_AGENT, `⚠️ Ref file not found: ${refFilePath}`, timer.elapsed('shadow-factory'));
567
+ logger.warn(MODULE_AGENT, ` Worker branch may not exist yet. Skipping ref watcher setup.`, timer.elapsed('shadow-factory'));
568
+ return;
569
+ }
570
+ // Read initial commit SHA
571
+ try {
572
+ this.previousCommitSha = fs.readFileSync(refFilePath, 'utf-8').trim();
573
+ logger.info(MODULE_AGENT, `📋 Initial commit SHA: ${this.previousCommitSha.substring(0, 7)}`, timer.elapsed('shadow-factory'));
574
+ }
575
+ catch (error) {
576
+ logger.error(MODULE_AGENT, `❌ Failed to read initial commit SHA: ${error.message}`, timer.elapsed('shadow-factory'), error);
577
+ this.previousCommitSha = null;
578
+ }
579
+ // Setup fs.watch for ref file
580
+ try {
581
+ this.refWatcher = fs.watch(refFilePath, (eventType, _filename) => {
582
+ // Handle 'change' and 'rename' events (rename can occur during git operations)
583
+ if (eventType !== 'change' && eventType !== 'rename') {
584
+ return;
585
+ }
586
+ logger.info(MODULE_AGENT, `🔄 Git ref change detected: ${eventType}`, timer.elapsed('shadow-factory'));
587
+ // Clear previous debounce timeout
588
+ if (this.refDebounceTimeout) {
589
+ clearTimeout(this.refDebounceTimeout);
590
+ }
591
+ // Debounce to handle rapid ref updates
592
+ this.refDebounceTimeout = setTimeout(async () => {
593
+ try {
594
+ // Read current commit SHA from ref file
595
+ const currentSha = fs.readFileSync(refFilePath, 'utf-8').trim();
596
+ // Check if SHA actually changed (ignore non-commit ref updates)
597
+ if (currentSha === this.previousCommitSha) {
598
+ logger.info(MODULE_AGENT, `ℹ️ Ref updated but SHA unchanged - skipping`, timer.elapsed('shadow-factory'));
599
+ return;
600
+ }
601
+ logger.info(MODULE_AGENT, `🔄 Worker commit detected on ${this.workerBranch}`, timer.elapsed('shadow-factory'));
602
+ logger.info(MODULE_AGENT, ` Previous SHA: ${this.previousCommitSha?.substring(0, 7) || 'none'}`, timer.elapsed('shadow-factory'));
603
+ logger.info(MODULE_AGENT, ` Current SHA: ${currentSha.substring(0, 7)}`, timer.elapsed('shadow-factory'));
604
+ // Update tracked SHA
605
+ this.previousCommitSha = currentSha;
606
+ // Trigger shadow backup for commit
607
+ await this.createShadowBackup('COMMIT', `Commit ${currentSha.substring(0, 7)}`);
608
+ }
609
+ catch (error) {
610
+ logger.error(MODULE_AGENT, `❌ Failed to process ref change: ${error.message}`, timer.elapsed('shadow-factory'), error);
611
+ // Non-fatal - watcher continues operating
612
+ }
613
+ }, this.debounceMs);
614
+ });
615
+ logger.info(MODULE_AGENT, '✅ Git ref watcher ready', timer.elapsed('shadow-factory'));
616
+ }
617
+ catch (error) {
618
+ logger.error(MODULE_AGENT, `❌ Failed to setup git ref watcher: ${error.message}`, timer.elapsed('shadow-factory'), error);
619
+ this.refWatcher = null;
620
+ // Non-fatal - agent continues with filesystem watching only
621
+ }
622
+ // Handle watcher errors
623
+ this.refWatcher?.on('error', (error) => {
624
+ logger.error(MODULE_AGENT, '❌ Git ref watcher error', timer.elapsed('shadow-factory'), error);
625
+ // Non-fatal - watcher may auto-recover
626
+ });
627
+ }
628
+ /**
629
+ * Create shadow backup commit
630
+ *
631
+ * Creates a mirror commit in shadow worktree by:
632
+ * 1. Parsing latest commit from worker worktree (git log)
633
+ * 2. Getting list of changed files (git diff)
634
+ * 3. Copying changed files from worker to shadow worktree
635
+ * 4. Creating mirror commit with format: Shadow: {operation} {fileName}
636
+ *
637
+ * Uses circuit breaker pattern to prevent cascading failures on git errors.
638
+ * Publishes backup completion/failure events to KĀDI broker.
639
+ *
640
+ * @param operation - Type of operation (e.g., 'Created', 'Modified', 'Deleted', 'COMMIT')
641
+ * @param fileName - File name or commit description
642
+ *
643
+ * @example
644
+ * ```typescript
645
+ * await this.createShadowBackup('Created', 'artwork.png');
646
+ * await this.createShadowBackup('COMMIT', 'Commit abc1234');
647
+ * ```
648
+ */
649
+ async createShadowBackup(operation, fileName) {
650
+ logger.info(MODULE_AGENT, `📦 Creating shadow backup: ${operation} - ${fileName}`, timer.elapsed('shadow-factory'));
651
+ // Check circuit breaker state before attempting git operations
652
+ if (this.checkCircuitBreaker()) {
653
+ logger.warn(MODULE_AGENT, `⚠️ Circuit breaker open - skipping backup operation`, timer.elapsed('shadow-factory'));
654
+ return;
655
+ }
656
+ try {
657
+ // For COMMIT operations, parse worker commit and copy changed files
658
+ if (operation === 'COMMIT') {
659
+ logger.info(MODULE_AGENT, `📋 Processing worker commit mirror...`, timer.elapsed('shadow-factory'));
660
+ // Step 1: Get latest commit hash from worker worktree
661
+ const commitHash = execSync('git log -1 --format=%H', {
662
+ cwd: this.workerWorktreePath,
663
+ encoding: 'utf-8'
664
+ }).trim();
665
+ logger.info(MODULE_AGENT, ` Worker commit SHA: ${commitHash.substring(0, 7)}`, timer.elapsed('shadow-factory'));
666
+ // Step 2: Get commit message from worker
667
+ const commitMessage = execSync('git log -1 --format=%B', {
668
+ cwd: this.workerWorktreePath,
669
+ encoding: 'utf-8'
670
+ }).trim();
671
+ logger.info(MODULE_AGENT, ` Worker commit message: ${commitMessage}`, timer.elapsed('shadow-factory'));
672
+ // Step 3: Get list of changed files using git diff
673
+ let changedFiles = [];
674
+ try {
675
+ const diffOutput = execSync('git diff --name-only HEAD~1 HEAD', {
676
+ cwd: this.workerWorktreePath,
677
+ encoding: 'utf-8'
678
+ }).trim();
679
+ changedFiles = diffOutput ? diffOutput.split('\n').filter(f => f.trim()) : [];
680
+ logger.info(MODULE_AGENT, ` Changed files: ${changedFiles.length} file(s)`, timer.elapsed('shadow-factory'));
681
+ }
682
+ catch (diffError) {
683
+ // Handle case where there's no parent commit (initial commit)
684
+ if (diffError.message.includes('unknown revision')) {
685
+ logger.info(MODULE_AGENT, ` Initial commit detected - getting all files`, timer.elapsed('shadow-factory'));
686
+ const allFilesOutput = execSync('git ls-tree -r HEAD --name-only', {
687
+ cwd: this.workerWorktreePath,
688
+ encoding: 'utf-8'
689
+ }).trim();
690
+ changedFiles = allFilesOutput ? allFilesOutput.split('\n').filter(f => f.trim()) : [];
691
+ }
692
+ else {
693
+ throw diffError;
694
+ }
695
+ }
696
+ // Step 4: Copy changed files from worker to shadow worktree
697
+ for (const file of changedFiles) {
698
+ const srcPath = path.join(this.workerWorktreePath, file);
699
+ const destPath = path.join(this.shadowWorktreePath, file);
700
+ // Create destination directory if needed
701
+ const destDir = path.dirname(destPath);
702
+ if (!fs.existsSync(destDir)) {
703
+ fs.mkdirSync(destDir, { recursive: true });
704
+ }
705
+ // Copy file
706
+ try {
707
+ fs.copyFileSync(srcPath, destPath);
708
+ logger.info(MODULE_AGENT, ` ✓ Copied: ${file}`, timer.elapsed('shadow-factory'));
709
+ }
710
+ catch (copyError) {
711
+ // File may have been deleted - that's ok, git will handle it
712
+ logger.info(MODULE_AGENT, ` ℹ️ Could not copy ${file}: ${copyError.message}`, timer.elapsed('shadow-factory'));
713
+ }
714
+ }
715
+ // Step 5: Stage all changes in shadow worktree
716
+ execSync('git add -A', {
717
+ cwd: this.shadowWorktreePath,
718
+ encoding: 'utf-8'
719
+ });
720
+ // Step 5.5: Check if there are staged changes (FS watcher may have already committed)
721
+ try {
722
+ execSync('git diff --cached --quiet', { cwd: this.shadowWorktreePath });
723
+ // Exit code 0 = nothing staged — FS watcher already backed up this change
724
+ logger.info(MODULE_AGENT, `ℹ️ No new changes to commit (already backed up by filesystem watcher)`, timer.elapsed('shadow-factory'));
725
+ this.recordGitSuccess();
726
+ await this.publishBackupStatus(true, changedFiles, 'mirror-commit-skipped');
727
+ return;
728
+ }
729
+ catch {
730
+ // Exit code 1 = staged changes exist — proceed with commit
731
+ }
732
+ // Step 6: Create mirror commit in shadow worktree
733
+ const shadowCommitMessage = `Shadow: ${operation} ${fileName}\n\nMirror of: ${commitMessage}\nOriginal SHA: ${commitHash}`;
734
+ execSync(`git commit -m "${shadowCommitMessage.replace(/"/g, '\\"')}"`, {
735
+ cwd: this.shadowWorktreePath,
736
+ encoding: 'utf-8'
737
+ });
738
+ // Get shadow commit SHA
739
+ const shadowCommitHash = execSync('git log -1 --format=%H', {
740
+ cwd: this.shadowWorktreePath,
741
+ encoding: 'utf-8'
742
+ }).trim();
743
+ logger.info(MODULE_AGENT, `✅ Shadow commit created: ${shadowCommitHash.substring(0, 7)}`, timer.elapsed('shadow-factory'));
744
+ // Record success and reset failure count
745
+ this.recordGitSuccess();
746
+ // Publish backup success event using standardized method
747
+ await this.publishBackupStatus(true, changedFiles, 'mirror-commit');
748
+ }
749
+ else {
750
+ // For file operations (Created, Modified, Deleted), handle individual file
751
+ logger.info(MODULE_AGENT, `📋 Processing file operation: ${operation} - ${fileName}`, timer.elapsed('shadow-factory'));
752
+ const srcPath = path.join(this.workerWorktreePath, fileName);
753
+ const destPath = path.join(this.shadowWorktreePath, fileName);
754
+ // Create destination directory if needed
755
+ const destDir = path.dirname(destPath);
756
+ if (!fs.existsSync(destDir)) {
757
+ fs.mkdirSync(destDir, { recursive: true });
758
+ }
759
+ // Copy file if it exists (for Created/Modified operations)
760
+ if (operation !== 'Deleted' && fs.existsSync(srcPath)) {
761
+ fs.copyFileSync(srcPath, destPath);
762
+ logger.info(MODULE_AGENT, ` ✓ Copied: ${fileName}`, timer.elapsed('shadow-factory'));
763
+ }
764
+ // Stage changes in shadow worktree
765
+ if (operation === 'Deleted') {
766
+ // For deletions, remove the file and stage the deletion
767
+ if (fs.existsSync(destPath)) {
768
+ fs.unlinkSync(destPath);
769
+ }
770
+ execSync(`git add "${fileName}"`, {
771
+ cwd: this.shadowWorktreePath,
772
+ encoding: 'utf-8'
773
+ });
774
+ }
775
+ else {
776
+ // For additions/modifications, stage the file
777
+ execSync(`git add "${fileName}"`, {
778
+ cwd: this.shadowWorktreePath,
779
+ encoding: 'utf-8'
780
+ });
781
+ }
782
+ // Check if there are actually staged changes (FS watcher may fire duplicate events)
783
+ try {
784
+ execSync('git diff --cached --quiet', { cwd: this.shadowWorktreePath });
785
+ // Exit code 0 = nothing staged — content is identical to HEAD
786
+ logger.info(MODULE_AGENT, `ℹ️ No new changes to commit (file unchanged)`, timer.elapsed('shadow-factory'));
787
+ this.recordGitSuccess();
788
+ await this.publishBackupStatus(true, [fileName], `file-${operation.toLowerCase()}-skipped`);
789
+ return;
790
+ }
791
+ catch {
792
+ // Exit code 1 = staged changes exist — proceed with commit
793
+ }
794
+ // Create backup commit
795
+ const commitMessage = `Shadow: ${operation} ${fileName}`;
796
+ execSync(`git commit -m "${commitMessage}"`, {
797
+ cwd: this.shadowWorktreePath,
798
+ encoding: 'utf-8'
799
+ });
800
+ // Get commit SHA
801
+ const commitHash = execSync('git log -1 --format=%H', {
802
+ cwd: this.shadowWorktreePath,
803
+ encoding: 'utf-8'
804
+ }).trim();
805
+ logger.info(MODULE_AGENT, `✅ Shadow backup commit created: ${commitHash.substring(0, 7)}`, timer.elapsed('shadow-factory'));
806
+ // Record success and reset failure count
807
+ this.recordGitSuccess();
808
+ // Publish backup success event using standardized method
809
+ await this.publishBackupStatus(true, [fileName], `file-${operation.toLowerCase()}`);
810
+ }
811
+ }
812
+ catch (error) {
813
+ logger.error(MODULE_AGENT, `❌ Shadow backup failed: ${error.message}`, timer.elapsed('shadow-factory'), error);
814
+ // Record failure and potentially open circuit breaker
815
+ this.recordGitFailure('createShadowBackup', error);
816
+ // Only publish failure event if circuit is not open (avoid spam)
817
+ if (!this.checkCircuitBreaker()) {
818
+ await this.publishBackupStatus(false, [], operation === 'COMMIT' ? 'mirror-commit' : `file-${operation.toLowerCase()}`, error);
819
+ }
820
+ }
821
+ }
822
+ /**
823
+ * Publish backup status event to KĀDI broker
824
+ *
825
+ * Publishes standardized BackupEvent with schema compliance for both success
826
+ * and failure scenarios. Events follow generic topic pattern: backup.{completed|failed} with agent identity in payload.
827
+ *
828
+ * Uses KadiEventPublisher for resilient event publishing with connection retry logic.
829
+ * Handles publishing failures gracefully without throwing errors.
830
+ *
831
+ * @param success - True for backup success, false for failure
832
+ * @param filesBackedUp - Array of file paths that were backed up
833
+ * @param operation - Backup operation type (e.g., 'mirror-commit', 'file-create')
834
+ * @param error - Error object if backup failed (optional)
835
+ *
836
+ * @example
837
+ * ```typescript
838
+ * // Success case
839
+ * await this.publishBackupStatus(true, ['artwork.png', 'logo.svg'], 'mirror-commit');
840
+ *
841
+ * // Failure case
842
+ * await this.publishBackupStatus(false, [], 'mirror-commit', new Error('Git operation failed'));
843
+ * ```
844
+ */
845
+ async publishBackupStatus(success, filesBackedUp, operation, error) {
846
+ // Generic topic — agent identity is in the payload, not the topic
847
+ const topic = `backup.${success ? 'completed' : 'failed'}`;
848
+ // Create payload matching BackupEvent schema
849
+ const payload = {
850
+ agent: `shadow-agent-${this.role}`,
851
+ role: this.role,
852
+ operation,
853
+ status: success ? 'success' : 'failure',
854
+ filesBackedUp,
855
+ timestamp: new Date().toISOString()
856
+ };
857
+ // Add error message if failure
858
+ if (!success && error) {
859
+ payload.error = error.message;
860
+ }
861
+ // Publish event using KadiClient
862
+ await this.client.publish(topic, payload, { broker: 'default', network: 'global' });
863
+ logger.info(MODULE_AGENT, `📤 Published backup ${success ? 'success' : 'failure'} event to ${topic}`, timer.elapsed('shadow-factory'));
864
+ }
865
+ /**
866
+ * Check circuit breaker state for git operations
867
+ *
868
+ * Returns true if circuit is open (blocking requests).
869
+ * Circuit opens after MAX_GIT_FAILURES consecutive failures and auto-resets after CIRCUIT_RESET_TIME.
870
+ *
871
+ * @returns True if circuit is open, false if closed
872
+ *
873
+ * @example
874
+ * ```typescript
875
+ * if (this.checkCircuitBreaker()) {
876
+ * console.log('Circuit open - skipping backup operation');
877
+ * return;
878
+ * }
879
+ * ```
880
+ */
881
+ checkCircuitBreaker() {
882
+ return this.gitCircuitOpen;
883
+ }
884
+ /**
885
+ * Record git operation failure and potentially open circuit breaker
886
+ *
887
+ * Increments failure count and opens circuit if threshold exceeded.
888
+ * Auto-resets circuit after timeout period.
889
+ *
890
+ * @param operation - Operation name for logging
891
+ * @param error - Error that occurred
892
+ */
893
+ recordGitFailure(operation, error) {
894
+ this.gitFailureCount++;
895
+ logger.error(MODULE_AGENT, `❌ Git operation failed (${this.gitFailureCount}/${this.MAX_GIT_FAILURES}): ${operation}`, timer.elapsed('shadow-factory'), error);
896
+ if (this.gitFailureCount >= this.MAX_GIT_FAILURES) {
897
+ this.gitCircuitOpen = true;
898
+ logger.error(MODULE_AGENT, `🚨 Circuit breaker opened - too many git failures`, timer.elapsed('shadow-factory'));
899
+ // Auto-reset circuit after timeout
900
+ setTimeout(() => {
901
+ this.gitCircuitOpen = false;
902
+ this.gitFailureCount = 0;
903
+ logger.info(MODULE_AGENT, `🔄 Circuit breaker reset - retrying git operations`, timer.elapsed('shadow-factory'));
904
+ }, this.CIRCUIT_RESET_TIME);
905
+ }
906
+ }
907
+ /**
908
+ * Record git operation success and reset failure count
909
+ *
910
+ * Resets failure counter on successful operation.
911
+ */
912
+ recordGitSuccess() {
913
+ this.gitFailureCount = 0;
914
+ }
915
+ }
916
+ // ============================================================================
917
+ // Shadow Agent Configuration Schema
918
+ // ============================================================================
919
+ /**
920
+ * Zod schema for ShadowAgentConfig validation
921
+ *
922
+ * Validates all required configuration fields for shadow agent instantiation.
923
+ * Ensures type safety and provides descriptive error messages for invalid configurations.
924
+ *
925
+ * Required Fields:
926
+ * - role: Agent role type (non-empty string)
927
+ * - workerWorktreePath: Absolute path to worker agent's git worktree (non-empty string)
928
+ * - shadowWorktreePath: Absolute path to shadow agent's git worktree (non-empty string)
929
+ * - workerBranch: Git branch name in worker worktree (non-empty string)
930
+ * - shadowBranch: Git branch name in shadow worktree (non-empty string)
931
+ * - brokerUrl: KĀDI broker WebSocket URL (non-empty string)
932
+ * - networks: Array of network names (at least one network required)
933
+ *
934
+ * Optional Fields:
935
+ * - debounceMs: Debounce delay in milliseconds (positive number, default: 1000)
936
+ *
937
+ * @example
938
+ * ```typescript
939
+ * const validConfig = {
940
+ * role: 'artist',
941
+ * workerWorktreePath: 'C:/p4/Personal/SD/agent-playground-artist',
942
+ * shadowWorktreePath: 'C:/p4/Personal/SD/shadow-artist-backup',
943
+ * workerBranch: 'main',
944
+ * shadowBranch: 'shadow-main',
945
+ * brokerUrl: 'ws://localhost:8080/kadi',
946
+ * networks: ['kadi'],
947
+ * debounceMs: 2000 // optional
948
+ * };
949
+ *
950
+ * ShadowAgentConfigSchema.parse(validConfig); // ✅ Passes validation
951
+ * ```
952
+ */
953
+ export const ShadowAgentConfigSchema = z.object({
954
+ role: z.string().min(1, 'Role is required and cannot be empty'),
955
+ workerWorktreePath: z.string().min(1, 'Worker worktree path is required and cannot be empty'),
956
+ shadowWorktreePath: z.string().min(1, 'Shadow worktree path is required and cannot be empty'),
957
+ workerBranch: z.string().min(1, 'Worker branch is required and cannot be empty'),
958
+ shadowBranch: z.string().min(1, 'Shadow branch is required and cannot be empty'),
959
+ brokerUrl: z.string().min(1, 'Broker URL is required and cannot be empty'),
960
+ networks: z.array(z.string()).min(1, 'At least one network is required'),
961
+ debounceMs: z.number().positive('Debounce delay must be a positive number').optional()
962
+ });
963
+ // ============================================================================
964
+ // Shadow Agent Factory
965
+ // ============================================================================
966
+ /**
967
+ * Factory class for creating shadow agents
968
+ *
969
+ * Provides a clean API for shadow agent instantiation with validated configuration.
970
+ * Follows the same factory pattern as WorkerAgentFactory for consistency.
971
+ *
972
+ * The factory performs Zod schema validation on configuration before creating agents,
973
+ * ensuring type safety and providing descriptive error messages for invalid configurations.
974
+ *
975
+ * Usage Pattern:
976
+ * 1. Call ShadowAgentFactory.createAgent(config) with your configuration
977
+ * 2. If validation passes, receive a configured BaseShadowAgent instance
978
+ * 3. Call agent.start() to begin monitoring and backup operations
979
+ * 4. Call agent.stop() when done to cleanup resources
980
+ *
981
+ * @example Minimal Configuration
982
+ * ```typescript
983
+ * const agent = ShadowAgentFactory.createAgent({
984
+ * role: 'artist',
985
+ * workerWorktreePath: 'C:/p4/Personal/SD/agent-playground-artist',
986
+ * shadowWorktreePath: 'C:/p4/Personal/SD/shadow-artist-backup',
987
+ * workerBranch: 'main',
988
+ * shadowBranch: 'shadow-main',
989
+ * brokerUrl: 'ws://localhost:8080/kadi',
990
+ * networks: ['kadi']
991
+ * });
992
+ *
993
+ * await agent.start();
994
+ * console.log('Shadow agent is now monitoring worker worktree');
995
+ * ```
996
+ *
997
+ * @example With Optional Debounce Configuration
998
+ * ```typescript
999
+ * const agent = ShadowAgentFactory.createAgent({
1000
+ * role: 'designer',
1001
+ * workerWorktreePath: 'C:/p4/Personal/SD/agent-playground-designer',
1002
+ * shadowWorktreePath: 'C:/p4/Personal/SD/shadow-designer-backup',
1003
+ * workerBranch: 'develop',
1004
+ * shadowBranch: 'shadow-develop',
1005
+ * brokerUrl: 'ws://localhost:8080/kadi',
1006
+ * networks: ['kadi', 'production'],
1007
+ * debounceMs: 2000 // Wait 2 seconds after last change
1008
+ * });
1009
+ *
1010
+ * await agent.start();
1011
+ * // Agent monitors for 2 seconds after each change before creating backup
1012
+ * ```
1013
+ *
1014
+ * @example Error Handling with Validation
1015
+ * ```typescript
1016
+ * try {
1017
+ * const agent = ShadowAgentFactory.createAgent({
1018
+ * role: '', // ❌ Empty role will fail validation
1019
+ * workerWorktreePath: 'C:/p4/Personal/SD/agent-playground-artist',
1020
+ * shadowWorktreePath: 'C:/p4/Personal/SD/shadow-artist-backup',
1021
+ * workerBranch: 'main',
1022
+ * shadowBranch: 'shadow-main',
1023
+ * brokerUrl: 'ws://localhost:8080/kadi',
1024
+ * networks: [] // ❌ Empty networks array will fail validation
1025
+ * });
1026
+ * } catch (error) {
1027
+ * console.error('Configuration validation failed:', error.message);
1028
+ * // Output: "Role is required and cannot be empty"
1029
+ * }
1030
+ * ```
1031
+ */
1032
+ export class ShadowAgentFactory {
1033
+ /**
1034
+ * Create a shadow agent with validated configuration
1035
+ *
1036
+ * Static factory method for instantiating shadow agents with Zod schema validation.
1037
+ * Validates all required configuration fields and provides descriptive error messages
1038
+ * for invalid configurations.
1039
+ *
1040
+ * The method performs the following steps:
1041
+ * 1. Validates configuration using ShadowAgentConfigSchema
1042
+ * 2. If validation passes, creates BaseShadowAgent instance
1043
+ * 3. Returns fully configured agent ready for start()
1044
+ *
1045
+ * Note: This method does NOT automatically start the agent. Caller must explicitly
1046
+ * call agent.start() to begin monitoring and backup operations.
1047
+ *
1048
+ * @param config - Shadow agent configuration to validate and use
1049
+ * @returns Configured BaseShadowAgent instance ready for start()
1050
+ * @throws {ZodError} If configuration validation fails with detailed error messages
1051
+ *
1052
+ * @example Minimal Configuration
1053
+ * ```typescript
1054
+ * const agent = ShadowAgentFactory.createAgent({
1055
+ * role: 'artist',
1056
+ * workerWorktreePath: 'C:/p4/Personal/SD/agent-playground-artist',
1057
+ * shadowWorktreePath: 'C:/p4/Personal/SD/shadow-artist-backup',
1058
+ * workerBranch: 'main',
1059
+ * shadowBranch: 'shadow-main',
1060
+ * brokerUrl: 'ws://localhost:8080/kadi',
1061
+ * networks: ['kadi']
1062
+ * });
1063
+ *
1064
+ * await agent.start();
1065
+ * ```
1066
+ *
1067
+ * @example With Optional Configuration
1068
+ * ```typescript
1069
+ * const agent = ShadowAgentFactory.createAgent({
1070
+ * role: 'programmer',
1071
+ * workerWorktreePath: 'C:/p4/Personal/SD/agent-playground-programmer',
1072
+ * shadowWorktreePath: 'C:/p4/Personal/SD/shadow-programmer-backup',
1073
+ * workerBranch: 'feature/new-api',
1074
+ * shadowBranch: 'shadow-feature',
1075
+ * brokerUrl: 'ws://localhost:8080/kadi',
1076
+ * networks: ['kadi', 'staging'],
1077
+ * debounceMs: 3000 // Custom debounce delay
1078
+ * });
1079
+ *
1080
+ * await agent.start();
1081
+ * ```
1082
+ */
1083
+ static createAgent(config, baseAgent) {
1084
+ // Validate configuration with Zod schema
1085
+ // Throws ZodError with descriptive messages if validation fails
1086
+ const validatedConfig = ShadowAgentConfigSchema.parse(config);
1087
+ // Create and return BaseShadowAgent instance with validated config
1088
+ return new BaseShadowAgent(validatedConfig, baseAgent);
1089
+ }
1090
+ }
1091
+ /**
1092
+ * Create a shadow agent with configuration
1093
+ *
1094
+ * Convenience function for instantiating shadow agents.
1095
+ * Delegates to ShadowAgentFactory.createAgent().
1096
+ *
1097
+ * @param config - Shadow agent configuration
1098
+ * @returns Configured BaseShadowAgent instance
1099
+ *
1100
+ * @example
1101
+ * ```typescript
1102
+ * const agent = createShadowAgent({
1103
+ * role: 'artist',
1104
+ * workerWorktreePath: 'C:/p4/Personal/SD/agent-playground-artist',
1105
+ * shadowWorktreePath: 'C:/p4/Personal/SD/shadow-agent-playground-artist',
1106
+ * workerBranch: 'agent-artist',
1107
+ * shadowBranch: 'shadow-agent-artist',
1108
+ * brokerUrl: 'ws://localhost:8080/kadi',
1109
+ * networks: ['kadi']
1110
+ * });
1111
+ * await agent.start();
1112
+ * ```
1113
+ */
1114
+ export function createShadowAgent(config, baseAgent) {
1115
+ return ShadowAgentFactory.createAgent(config, baseAgent);
1116
+ }
1117
+ //# sourceMappingURL=shadow-agent-factory.js.map