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.
- package/dist/base-agent.d.ts +172 -0
- package/dist/base-agent.d.ts.map +1 -0
- package/dist/base-agent.js +255 -0
- package/dist/base-agent.js.map +1 -0
- package/dist/base-bot.d.ts +282 -0
- package/dist/base-bot.d.ts.map +1 -0
- package/dist/base-bot.js +375 -0
- package/dist/base-bot.js.map +1 -0
- package/dist/common/result.d.ts +51 -0
- package/dist/common/result.d.ts.map +1 -0
- package/dist/common/result.js +45 -0
- package/dist/common/result.js.map +1 -0
- package/dist/common/types.d.ts +57 -0
- package/dist/common/types.d.ts.map +1 -0
- package/dist/common/types.js +42 -0
- package/dist/common/types.js.map +1 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +108 -0
- package/dist/index.js.map +1 -0
- package/dist/kadi-event-publisher.d.ts +163 -0
- package/dist/kadi-event-publisher.d.ts.map +1 -0
- package/dist/kadi-event-publisher.js +286 -0
- package/dist/kadi-event-publisher.js.map +1 -0
- package/dist/memory/arcadedb-adapter.d.ts +159 -0
- package/dist/memory/arcadedb-adapter.d.ts.map +1 -0
- package/dist/memory/arcadedb-adapter.js +314 -0
- package/dist/memory/arcadedb-adapter.js.map +1 -0
- package/dist/memory/file-storage-adapter.d.ts +122 -0
- package/dist/memory/file-storage-adapter.d.ts.map +1 -0
- package/dist/memory/file-storage-adapter.js +352 -0
- package/dist/memory/file-storage-adapter.js.map +1 -0
- package/dist/memory/memory-service.d.ts +208 -0
- package/dist/memory/memory-service.d.ts.map +1 -0
- package/dist/memory/memory-service.js +410 -0
- package/dist/memory/memory-service.js.map +1 -0
- package/dist/memory/types.d.ts +126 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +41 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/producer-tool-utils.d.ts +474 -0
- package/dist/producer-tool-utils.d.ts.map +1 -0
- package/dist/producer-tool-utils.js +664 -0
- package/dist/producer-tool-utils.js.map +1 -0
- package/dist/providers/anthropic-provider.d.ts +160 -0
- package/dist/providers/anthropic-provider.d.ts.map +1 -0
- package/dist/providers/anthropic-provider.js +527 -0
- package/dist/providers/anthropic-provider.js.map +1 -0
- package/dist/providers/model-manager-provider.d.ts +91 -0
- package/dist/providers/model-manager-provider.d.ts.map +1 -0
- package/dist/providers/model-manager-provider.js +355 -0
- package/dist/providers/model-manager-provider.js.map +1 -0
- package/dist/providers/provider-manager.d.ts +111 -0
- package/dist/providers/provider-manager.d.ts.map +1 -0
- package/dist/providers/provider-manager.js +337 -0
- package/dist/providers/provider-manager.js.map +1 -0
- package/dist/providers/types.d.ts +145 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +23 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/shadow-agent-factory.d.ts +623 -0
- package/dist/shadow-agent-factory.d.ts.map +1 -0
- package/dist/shadow-agent-factory.js +1117 -0
- package/dist/shadow-agent-factory.js.map +1 -0
- package/dist/types/agent-config.d.ts +307 -0
- package/dist/types/agent-config.d.ts.map +1 -0
- package/dist/types/agent-config.js +15 -0
- package/dist/types/agent-config.js.map +1 -0
- package/dist/types/event-schemas.d.ts +358 -0
- package/dist/types/event-schemas.d.ts.map +1 -0
- package/dist/types/event-schemas.js +188 -0
- package/dist/types/event-schemas.js.map +1 -0
- package/dist/types/tool-schemas.d.ts +498 -0
- package/dist/types/tool-schemas.d.ts.map +1 -0
- package/dist/types/tool-schemas.js +457 -0
- package/dist/types/tool-schemas.js.map +1 -0
- package/dist/utils/logger.d.ts +135 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +205 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/timer.d.ts +186 -0
- package/dist/utils/timer.d.ts.map +1 -0
- package/dist/utils/timer.js +211 -0
- package/dist/utils/timer.js.map +1 -0
- package/dist/worker-agent-factory.d.ts +688 -0
- package/dist/worker-agent-factory.d.ts.map +1 -0
- package/dist/worker-agent-factory.js +1517 -0
- package/dist/worker-agent-factory.js.map +1 -0
- 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
|