agents-library 0.1.0 → 0.1.2

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 (110) hide show
  1. package/README.md +178 -0
  2. package/dist/base-agent.d.ts +10 -8
  3. package/dist/base-agent.d.ts.map +1 -1
  4. package/dist/base-agent.js +30 -26
  5. package/dist/base-agent.js.map +1 -1
  6. package/dist/base-bot.d.ts +0 -0
  7. package/dist/base-bot.d.ts.map +0 -0
  8. package/dist/base-bot.js +5 -5
  9. package/dist/base-bot.js.map +1 -1
  10. package/dist/common/result.d.ts +0 -0
  11. package/dist/common/result.d.ts.map +0 -0
  12. package/dist/common/result.js +0 -0
  13. package/dist/common/result.js.map +0 -0
  14. package/dist/common/types.d.ts +0 -0
  15. package/dist/common/types.d.ts.map +0 -0
  16. package/dist/common/types.js +0 -0
  17. package/dist/common/types.js.map +0 -0
  18. package/dist/index.d.ts +12 -6
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +7 -3
  21. package/dist/index.js.map +1 -1
  22. package/dist/kadi-event-publisher.d.ts +0 -0
  23. package/dist/kadi-event-publisher.d.ts.map +0 -0
  24. package/dist/kadi-event-publisher.js +1 -1
  25. package/dist/kadi-event-publisher.js.map +1 -1
  26. package/dist/memory/arcadedb-adapter.d.ts +8 -0
  27. package/dist/memory/arcadedb-adapter.d.ts.map +1 -1
  28. package/dist/memory/arcadedb-adapter.js +8 -0
  29. package/dist/memory/arcadedb-adapter.js.map +1 -1
  30. package/dist/memory/entity-extractor.d.ts +110 -0
  31. package/dist/memory/entity-extractor.d.ts.map +1 -0
  32. package/dist/memory/entity-extractor.js +259 -0
  33. package/dist/memory/entity-extractor.js.map +1 -0
  34. package/dist/memory/file-storage-adapter.d.ts +0 -0
  35. package/dist/memory/file-storage-adapter.d.ts.map +0 -0
  36. package/dist/memory/file-storage-adapter.js +0 -0
  37. package/dist/memory/file-storage-adapter.js.map +0 -0
  38. package/dist/memory/memory-service.d.ts +123 -13
  39. package/dist/memory/memory-service.d.ts.map +1 -1
  40. package/dist/memory/memory-service.js +428 -72
  41. package/dist/memory/memory-service.js.map +1 -1
  42. package/dist/memory/types.d.ts +0 -0
  43. package/dist/memory/types.d.ts.map +0 -0
  44. package/dist/memory/types.js +0 -0
  45. package/dist/memory/types.js.map +0 -0
  46. package/dist/producer-tool-utils.d.ts +0 -0
  47. package/dist/producer-tool-utils.d.ts.map +0 -0
  48. package/dist/producer-tool-utils.js +16 -16
  49. package/dist/producer-tool-utils.js.map +1 -1
  50. package/dist/providers/anthropic-provider.d.ts +0 -0
  51. package/dist/providers/anthropic-provider.d.ts.map +0 -0
  52. package/dist/providers/anthropic-provider.js +0 -0
  53. package/dist/providers/anthropic-provider.js.map +0 -0
  54. package/dist/providers/model-manager-provider.d.ts +0 -0
  55. package/dist/providers/model-manager-provider.d.ts.map +0 -0
  56. package/dist/providers/model-manager-provider.js +0 -0
  57. package/dist/providers/model-manager-provider.js.map +0 -0
  58. package/dist/providers/provider-manager.d.ts +0 -0
  59. package/dist/providers/provider-manager.d.ts.map +1 -1
  60. package/dist/providers/provider-manager.js +6 -2
  61. package/dist/providers/provider-manager.js.map +1 -1
  62. package/dist/providers/types.d.ts +0 -0
  63. package/dist/providers/types.d.ts.map +0 -0
  64. package/dist/providers/types.js +0 -0
  65. package/dist/providers/types.js.map +0 -0
  66. package/dist/shadow-agent-factory.d.ts +23 -97
  67. package/dist/shadow-agent-factory.d.ts.map +1 -1
  68. package/dist/shadow-agent-factory.js +116 -306
  69. package/dist/shadow-agent-factory.js.map +1 -1
  70. package/dist/types/agent-config.d.ts +62 -1
  71. package/dist/types/agent-config.d.ts.map +1 -1
  72. package/dist/types/agent-config.js +0 -0
  73. package/dist/types/agent-config.js.map +0 -0
  74. package/dist/types/event-schemas.d.ts +194 -0
  75. package/dist/types/event-schemas.d.ts.map +1 -1
  76. package/dist/types/event-schemas.js +77 -2
  77. package/dist/types/event-schemas.js.map +1 -1
  78. package/dist/types/tool-schemas.d.ts +0 -0
  79. package/dist/types/tool-schemas.d.ts.map +0 -0
  80. package/dist/types/tool-schemas.js +0 -0
  81. package/dist/types/tool-schemas.js.map +0 -0
  82. package/dist/utils/config.d.ts +48 -0
  83. package/dist/utils/config.d.ts.map +1 -0
  84. package/dist/utils/config.js +163 -0
  85. package/dist/utils/config.js.map +1 -0
  86. package/dist/utils/logger.d.ts +11 -1
  87. package/dist/utils/logger.d.ts.map +1 -1
  88. package/dist/utils/logger.js +26 -1
  89. package/dist/utils/logger.js.map +1 -1
  90. package/dist/utils/path-utils.d.ts +22 -0
  91. package/dist/utils/path-utils.d.ts.map +1 -0
  92. package/dist/utils/path-utils.js +51 -0
  93. package/dist/utils/path-utils.js.map +1 -0
  94. package/dist/utils/read-config.d.ts +43 -0
  95. package/dist/utils/read-config.d.ts.map +1 -0
  96. package/dist/utils/read-config.js +97 -0
  97. package/dist/utils/read-config.js.map +1 -0
  98. package/dist/utils/timer.d.ts +0 -0
  99. package/dist/utils/timer.d.ts.map +0 -0
  100. package/dist/utils/timer.js +0 -0
  101. package/dist/utils/timer.js.map +0 -0
  102. package/dist/utils/vault.d.ts +29 -0
  103. package/dist/utils/vault.d.ts.map +1 -0
  104. package/dist/utils/vault.js +79 -0
  105. package/dist/utils/vault.js.map +1 -0
  106. package/dist/worker-agent-factory.d.ts +37 -38
  107. package/dist/worker-agent-factory.d.ts.map +1 -1
  108. package/dist/worker-agent-factory.js +355 -212
  109. package/dist/worker-agent-factory.js.map +1 -1
  110. package/package.json +5 -3
@@ -25,7 +25,6 @@
25
25
  * @module shadow-agent-factory
26
26
  */
27
27
  import { KadiClient, z } from '@kadi.build/core';
28
- import chokidar from 'chokidar';
29
28
  import fs from 'fs';
30
29
  import path from 'path';
31
30
  import { execSync } from 'child_process';
@@ -182,28 +181,15 @@ export class BaseShadowAgent {
182
181
  */
183
182
  usesBaseAgent;
184
183
  /**
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.
184
+ * Handler reference for broker file.changed events.
185
+ * Stored so we can unsubscribe on stop().
189
186
  */
190
- fsWatcher = null;
187
+ fileEventHandler = null;
191
188
  /**
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.
189
+ * Native ability-file-local instance for in-process file operations.
190
+ * Set via setNativeFileLocal() after loadNative in the consuming agent.
205
191
  */
206
- previousCommitSha = null;
192
+ nativeFileLocal = null;
207
193
  /**
208
194
  * Set of changed file paths awaiting backup processing
209
195
  *
@@ -214,13 +200,6 @@ export class BaseShadowAgent {
214
200
  * Value: Debounce timeout handle
215
201
  */
216
202
  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
203
  /**
225
204
  * Create a new BaseShadowAgent instance
226
205
  *
@@ -259,7 +238,7 @@ export class BaseShadowAgent {
259
238
  if (baseAgent) {
260
239
  this.client = baseAgent.client;
261
240
  this.usesBaseAgent = true;
262
- logger.info(MODULE_AGENT, ' Using BaseAgent client (connection managed externally)', timer.elapsed('shadow-factory'));
241
+ logger.info(MODULE_AGENT, ' Using BaseAgent client (connection managed externally)', timer.elapsed('shadow-factory'));
263
242
  }
264
243
  else {
265
244
  this.client = new KadiClient({
@@ -272,13 +251,20 @@ export class BaseShadowAgent {
272
251
  });
273
252
  this.usesBaseAgent = false;
274
253
  }
275
- logger.info(MODULE_AGENT, `🔧 BaseShadowAgent initialized for role: ${this.role}`, timer.elapsed('shadow-factory'));
254
+ logger.info(MODULE_AGENT, `BaseShadowAgent initialized for role: ${this.role}`, timer.elapsed('shadow-factory'));
276
255
  logger.info(MODULE_AGENT, ` Worker worktree: ${this.workerWorktreePath}`, timer.elapsed('shadow-factory'));
277
256
  logger.info(MODULE_AGENT, ` Shadow worktree: ${this.shadowWorktreePath}`, timer.elapsed('shadow-factory'));
278
257
  logger.info(MODULE_AGENT, ` Worker branch: ${this.workerBranch}`, timer.elapsed('shadow-factory'));
279
258
  logger.info(MODULE_AGENT, ` Shadow branch: ${this.shadowBranch}`, timer.elapsed('shadow-factory'));
280
259
  logger.info(MODULE_AGENT, ` Debounce delay: ${this.debounceMs}ms`, timer.elapsed('shadow-factory'));
281
260
  }
261
+ /**
262
+ * Set native ability-file-local for in-process file operations.
263
+ * Call this after loadNative('ability-file-local') in the consuming agent.
264
+ */
265
+ setNativeFileLocal(native) {
266
+ this.nativeFileLocal = native;
267
+ }
282
268
  /**
283
269
  * Start the shadow agent
284
270
  *
@@ -302,29 +288,26 @@ export class BaseShadowAgent {
302
288
  * ```
303
289
  */
304
290
  async start() {
305
- logger.info(MODULE_AGENT, `🚀 Starting shadow agent for role: ${this.role}`, timer.elapsed('shadow-factory'));
291
+ logger.info(MODULE_AGENT, `Starting shadow agent for role: ${this.role}`, timer.elapsed('shadow-factory'));
306
292
  // Connect to KĀDI broker (skip if BaseAgent manages connection)
307
293
  if (this.usesBaseAgent) {
308
- logger.info(MODULE_AGENT, ' Broker connection managed by BaseAgent (skipping)', timer.elapsed('shadow-factory'));
294
+ logger.info(MODULE_AGENT, ' Broker connection managed by BaseAgent (skipping)', timer.elapsed('shadow-factory'));
309
295
  }
310
296
  else {
311
297
  logger.info(MODULE_AGENT, ' → Connecting to KĀDI broker...', timer.elapsed('shadow-factory'));
312
298
  try {
313
299
  await this.client.connect();
314
- logger.info(MODULE_AGENT, ' Connected to KĀDI broker', timer.elapsed('shadow-factory'));
300
+ logger.info(MODULE_AGENT, ' Connected to KĀDI broker', timer.elapsed('shadow-factory'));
315
301
  }
316
302
  catch (error) {
317
- logger.error(MODULE_AGENT, 'Broker connection error', timer.elapsed('shadow-factory'), error);
303
+ logger.error(MODULE_AGENT, 'Broker connection error', timer.elapsed('shadow-factory'), error);
318
304
  process.exit(1);
319
305
  }
320
306
  }
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'));
307
+ // Subscribe to file.changed broker events (emitted by ability-file-local)
308
+ await this.setupBrokerFileEventSubscription();
309
+ logger.info(MODULE_AGENT, 'Broker file event subscription active', timer.elapsed('shadow-factory'));
310
+ logger.info(MODULE_AGENT, 'Shadow agent started and monitoring via broker events', timer.elapsed('shadow-factory'));
328
311
  }
329
312
  /**
330
313
  * Stop the shadow agent
@@ -346,284 +329,111 @@ export class BaseShadowAgent {
346
329
  * ```
347
330
  */
348
331
  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'));
332
+ logger.info(MODULE_AGENT, `Stopping shadow agent for role: ${this.role}`, timer.elapsed('shadow-factory'));
333
+ // Unsubscribe from broker file events
334
+ if (this.fileEventHandler) {
335
+ logger.info(MODULE_AGENT, 'Unsubscribing from file.changed events...', timer.elapsed('shadow-factory'));
336
+ try {
337
+ await this.client.unsubscribe('file.changed', this.fileEventHandler);
338
+ }
339
+ catch {
340
+ // Best-effort unsubscribe may not be supported in all KadiClient versions
341
+ }
342
+ this.fileEventHandler = null;
343
+ logger.info(MODULE_AGENT, 'File event subscription removed', timer.elapsed('shadow-factory'));
370
344
  }
371
345
  // Clear all pending debounce timers
372
346
  if (this.debounceMap.size > 0) {
373
- logger.info(MODULE_AGENT, `🛑 Clearing ${this.debounceMap.size} pending debounce timers...`, timer.elapsed('shadow-factory'));
347
+ logger.info(MODULE_AGENT, `Clearing ${this.debounceMap.size} pending debounce timers...`, timer.elapsed('shadow-factory'));
374
348
  for (const timeout of this.debounceMap.values()) {
375
349
  clearTimeout(timeout);
376
350
  }
377
351
  this.debounceMap.clear();
378
- logger.info(MODULE_AGENT, 'Debounce timers cleared', timer.elapsed('shadow-factory'));
352
+ logger.info(MODULE_AGENT, 'Debounce timers cleared', timer.elapsed('shadow-factory'));
379
353
  }
380
354
  // Disconnect KĀDI client (skip if BaseAgent manages connection)
381
355
  if (this.usesBaseAgent) {
382
- logger.info(MODULE_AGENT, ' Broker disconnection managed by BaseAgent (skipping)', timer.elapsed('shadow-factory'));
356
+ logger.info(MODULE_AGENT, ' Broker disconnection managed by BaseAgent (skipping)', timer.elapsed('shadow-factory'));
383
357
  }
384
358
  else {
385
359
  logger.info(MODULE_AGENT, ' → Disconnecting from KĀDI broker...', timer.elapsed('shadow-factory'));
386
360
  await this.client.disconnect();
387
- logger.info(MODULE_AGENT, ' Disconnected from KĀDI broker', timer.elapsed('shadow-factory'));
361
+ logger.info(MODULE_AGENT, ' Disconnected from KĀDI broker', timer.elapsed('shadow-factory'));
388
362
  }
389
- logger.info(MODULE_AGENT, 'Shadow agent stopped', timer.elapsed('shadow-factory'));
363
+ logger.info(MODULE_AGENT, 'Shadow agent stopped', timer.elapsed('shadow-factory'));
390
364
  }
391
365
  /**
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
366
+ * Subscribe to file.changed broker events from ability-file-local
402
367
  *
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
368
+ * Instead of using chokidar directly (unreliable on /mnt/c/ in WSL containers),
369
+ * the shadow agent subscribes to `file.changed` events published by ability-file-local
370
+ * which runs on the host where filesystem events work natively.
409
371
  *
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
372
+ * Event payload from ability-file-local:
373
+ * { watchId: string, event: 'add'|'change'|'unlink', path: string }
415
374
  *
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
- * ```
375
+ * The shadow agent filters events to only process those matching its workerWorktreePath,
376
+ * then debounces and creates shadow backup commits.
423
377
  */
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'));
378
+ async setupBrokerFileEventSubscription() {
379
+ logger.info(MODULE_AGENT, `Subscribing to file.changed broker events for: ${this.workerWorktreePath}`, timer.elapsed('shadow-factory'));
380
+ const handler = async (event) => {
381
+ try {
382
+ const data = (event?.data || event);
383
+ if (!data.path || !data.event)
384
+ return;
385
+ const filePath = data.path;
386
+ // Filter: only process events for our worker worktree
387
+ // Normalize both paths for comparison (handle Windows/Unix path differences)
388
+ const normalizedFile = filePath.replace(/\\/g, '/').toLowerCase();
389
+ const normalizedWorktree = this.workerWorktreePath.replace(/\\/g, '/').toLowerCase();
390
+ if (!normalizedFile.startsWith(normalizedWorktree))
391
+ return;
392
+ // Skip .git, node_modules, .env files
481
393
  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') {
394
+ if (relativePath.startsWith('.git') ||
395
+ relativePath.includes('node_modules') ||
396
+ relativePath.startsWith('.env'))
584
397
  return;
398
+ const eventType = data.event; // 'add' | 'change' | 'unlink'
399
+ const operation = eventType === 'add' ? 'Created'
400
+ : eventType === 'unlink' ? 'Deleted'
401
+ : 'Modified';
402
+ logger.info(MODULE_AGENT, `📁 Broker file event: ${operation} ${relativePath}`, timer.elapsed('shadow-factory'));
403
+ // Debounce to avoid rapid-fire commits
404
+ if (this.debounceMap.has(filePath)) {
405
+ clearTimeout(this.debounceMap.get(filePath));
585
406
  }
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
- }
407
+ const timeout = setTimeout(async () => {
408
+ logger.info(MODULE_AGENT, `Processing ${operation.toLowerCase()} file: ${relativePath}`, timer.elapsed('shadow-factory'));
409
+ await this.createShadowBackup(operation, relativePath);
410
+ this.debounceMap.delete(filePath);
613
411
  }, this.debounceMs);
614
- });
615
- logger.info(MODULE_AGENT, '✅ Git ref watcher ready', timer.elapsed('shadow-factory'));
412
+ this.debounceMap.set(filePath, timeout);
413
+ }
414
+ catch (err) {
415
+ logger.error(MODULE_AGENT, `Error processing file.changed event: ${err.message}`, timer.elapsed('shadow-factory'), err);
416
+ }
417
+ };
418
+ this.fileEventHandler = handler;
419
+ await this.client.subscribe('file.changed', handler);
420
+ logger.info(MODULE_AGENT, 'Subscribed to file.changed broker events', timer.elapsed('shadow-factory'));
421
+ // Invoke ability-file-local's watch_folder to start emitting file.changed events
422
+ if (this.nativeFileLocal) {
423
+ try {
424
+ await this.nativeFileLocal.invoke('watch_folder', {
425
+ folderPath: this.workerWorktreePath,
426
+ watchId: `shadow-${this.role}`,
427
+ });
428
+ logger.info(MODULE_AGENT, `Started file watcher via native ability-file-local for: ${this.workerWorktreePath}`, timer.elapsed('shadow-factory'));
429
+ }
430
+ catch (err) {
431
+ logger.warn(MODULE_AGENT, `Could not start native file watcher: ${err.message}`, timer.elapsed('shadow-factory'));
432
+ }
616
433
  }
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
434
+ else {
435
+ logger.warn(MODULE_AGENT, 'ability-file-local not loaded natively file watcher disabled (broker fallback not available)', timer.elapsed('shadow-factory'));
621
436
  }
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
437
  }
628
438
  /**
629
439
  * Create shadow backup commit
@@ -650,13 +460,13 @@ export class BaseShadowAgent {
650
460
  logger.info(MODULE_AGENT, `📦 Creating shadow backup: ${operation} - ${fileName}`, timer.elapsed('shadow-factory'));
651
461
  // Check circuit breaker state before attempting git operations
652
462
  if (this.checkCircuitBreaker()) {
653
- logger.warn(MODULE_AGENT, `⚠️ Circuit breaker open - skipping backup operation`, timer.elapsed('shadow-factory'));
463
+ logger.warn(MODULE_AGENT, `Circuit breaker open - skipping backup operation`, timer.elapsed('shadow-factory'));
654
464
  return;
655
465
  }
656
466
  try {
657
467
  // For COMMIT operations, parse worker commit and copy changed files
658
468
  if (operation === 'COMMIT') {
659
- logger.info(MODULE_AGENT, `📋 Processing worker commit mirror...`, timer.elapsed('shadow-factory'));
469
+ logger.info(MODULE_AGENT, `Processing worker commit mirror...`, timer.elapsed('shadow-factory'));
660
470
  // Step 1: Get latest commit hash from worker worktree
661
471
  const commitHash = execSync('git log -1 --format=%H', {
662
472
  cwd: this.workerWorktreePath,
@@ -709,7 +519,7 @@ export class BaseShadowAgent {
709
519
  }
710
520
  catch (copyError) {
711
521
  // 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'));
522
+ logger.info(MODULE_AGENT, ` Could not copy ${file}: ${copyError.message}`, timer.elapsed('shadow-factory'));
713
523
  }
714
524
  }
715
525
  // Step 5: Stage all changes in shadow worktree
@@ -721,7 +531,7 @@ export class BaseShadowAgent {
721
531
  try {
722
532
  execSync('git diff --cached --quiet', { cwd: this.shadowWorktreePath });
723
533
  // 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'));
534
+ logger.info(MODULE_AGENT, `No new changes to commit (already backed up by filesystem watcher)`, timer.elapsed('shadow-factory'));
725
535
  this.recordGitSuccess();
726
536
  await this.publishBackupStatus(true, changedFiles, 'mirror-commit-skipped');
727
537
  return;
@@ -740,7 +550,7 @@ export class BaseShadowAgent {
740
550
  cwd: this.shadowWorktreePath,
741
551
  encoding: 'utf-8'
742
552
  }).trim();
743
- logger.info(MODULE_AGENT, `✅ Shadow commit created: ${shadowCommitHash.substring(0, 7)}`, timer.elapsed('shadow-factory'));
553
+ logger.info(MODULE_AGENT, `Shadow commit created: ${shadowCommitHash.substring(0, 7)}`, timer.elapsed('shadow-factory'));
744
554
  // Record success and reset failure count
745
555
  this.recordGitSuccess();
746
556
  // Publish backup success event using standardized method
@@ -748,7 +558,7 @@ export class BaseShadowAgent {
748
558
  }
749
559
  else {
750
560
  // For file operations (Created, Modified, Deleted), handle individual file
751
- logger.info(MODULE_AGENT, `📋 Processing file operation: ${operation} - ${fileName}`, timer.elapsed('shadow-factory'));
561
+ logger.info(MODULE_AGENT, `Processing file operation: ${operation} - ${fileName}`, timer.elapsed('shadow-factory'));
752
562
  const srcPath = path.join(this.workerWorktreePath, fileName);
753
563
  const destPath = path.join(this.shadowWorktreePath, fileName);
754
564
  // Create destination directory if needed
@@ -783,7 +593,7 @@ export class BaseShadowAgent {
783
593
  try {
784
594
  execSync('git diff --cached --quiet', { cwd: this.shadowWorktreePath });
785
595
  // 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'));
596
+ logger.info(MODULE_AGENT, `No new changes to commit (file unchanged)`, timer.elapsed('shadow-factory'));
787
597
  this.recordGitSuccess();
788
598
  await this.publishBackupStatus(true, [fileName], `file-${operation.toLowerCase()}-skipped`);
789
599
  return;
@@ -802,7 +612,7 @@ export class BaseShadowAgent {
802
612
  cwd: this.shadowWorktreePath,
803
613
  encoding: 'utf-8'
804
614
  }).trim();
805
- logger.info(MODULE_AGENT, `✅ Shadow backup commit created: ${commitHash.substring(0, 7)}`, timer.elapsed('shadow-factory'));
615
+ logger.info(MODULE_AGENT, `Shadow backup commit created: ${commitHash.substring(0, 7)}`, timer.elapsed('shadow-factory'));
806
616
  // Record success and reset failure count
807
617
  this.recordGitSuccess();
808
618
  // Publish backup success event using standardized method
@@ -810,7 +620,7 @@ export class BaseShadowAgent {
810
620
  }
811
621
  }
812
622
  catch (error) {
813
- logger.error(MODULE_AGENT, `❌ Shadow backup failed: ${error.message}`, timer.elapsed('shadow-factory'), error);
623
+ logger.error(MODULE_AGENT, `Shadow backup failed: ${error.message}`, timer.elapsed('shadow-factory'), error);
814
624
  // Record failure and potentially open circuit breaker
815
625
  this.recordGitFailure('createShadowBackup', error);
816
626
  // Only publish failure event if circuit is not open (avoid spam)
@@ -860,7 +670,7 @@ export class BaseShadowAgent {
860
670
  }
861
671
  // Publish event using KadiClient
862
672
  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'));
673
+ logger.info(MODULE_AGENT, `Published backup ${success ? 'success' : 'failure'} event to ${topic}`, timer.elapsed('shadow-factory'));
864
674
  }
865
675
  /**
866
676
  * Check circuit breaker state for git operations
@@ -892,7 +702,7 @@ export class BaseShadowAgent {
892
702
  */
893
703
  recordGitFailure(operation, error) {
894
704
  this.gitFailureCount++;
895
- logger.error(MODULE_AGENT, `❌ Git operation failed (${this.gitFailureCount}/${this.MAX_GIT_FAILURES}): ${operation}`, timer.elapsed('shadow-factory'), error);
705
+ logger.error(MODULE_AGENT, `Git operation failed (${this.gitFailureCount}/${this.MAX_GIT_FAILURES}): ${operation}`, timer.elapsed('shadow-factory'), error);
896
706
  if (this.gitFailureCount >= this.MAX_GIT_FAILURES) {
897
707
  this.gitCircuitOpen = true;
898
708
  logger.error(MODULE_AGENT, `🚨 Circuit breaker opened - too many git failures`, timer.elapsed('shadow-factory'));
@@ -900,7 +710,7 @@ export class BaseShadowAgent {
900
710
  setTimeout(() => {
901
711
  this.gitCircuitOpen = false;
902
712
  this.gitFailureCount = 0;
903
- logger.info(MODULE_AGENT, `🔄 Circuit breaker reset - retrying git operations`, timer.elapsed('shadow-factory'));
713
+ logger.info(MODULE_AGENT, `Circuit breaker reset - retrying git operations`, timer.elapsed('shadow-factory'));
904
714
  }, this.CIRCUIT_RESET_TIME);
905
715
  }
906
716
  }
@@ -947,7 +757,7 @@ export class BaseShadowAgent {
947
757
  * debounceMs: 2000 // optional
948
758
  * };
949
759
  *
950
- * ShadowAgentConfigSchema.parse(validConfig); // Passes validation
760
+ * ShadowAgentConfigSchema.parse(validConfig); // Passes validation
951
761
  * ```
952
762
  */
953
763
  export const ShadowAgentConfigSchema = z.object({
@@ -1015,13 +825,13 @@ export const ShadowAgentConfigSchema = z.object({
1015
825
  * ```typescript
1016
826
  * try {
1017
827
  * const agent = ShadowAgentFactory.createAgent({
1018
- * role: '', // Empty role will fail validation
828
+ * role: '', // Empty role will fail validation
1019
829
  * workerWorktreePath: 'C:/p4/Personal/SD/agent-playground-artist',
1020
830
  * shadowWorktreePath: 'C:/p4/Personal/SD/shadow-artist-backup',
1021
831
  * workerBranch: 'main',
1022
832
  * shadowBranch: 'shadow-main',
1023
833
  * brokerUrl: 'ws://localhost:8080/kadi',
1024
- * networks: [] // Empty networks array will fail validation
834
+ * networks: [] // Empty networks array will fail validation
1025
835
  * });
1026
836
  * } catch (error) {
1027
837
  * console.error('Configuration validation failed:', error.message);