outcome-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +261 -0
  2. package/package.json +95 -0
  3. package/src/agents/README.md +139 -0
  4. package/src/agents/adapters/anthropic.adapter.ts +166 -0
  5. package/src/agents/adapters/dalle.adapter.ts +145 -0
  6. package/src/agents/adapters/gemini.adapter.ts +134 -0
  7. package/src/agents/adapters/imagen.adapter.ts +106 -0
  8. package/src/agents/adapters/nano-banana.adapter.ts +129 -0
  9. package/src/agents/adapters/openai.adapter.ts +165 -0
  10. package/src/agents/adapters/veo.adapter.ts +130 -0
  11. package/src/agents/agent.schema.property.test.ts +379 -0
  12. package/src/agents/agent.schema.test.ts +148 -0
  13. package/src/agents/agent.schema.ts +263 -0
  14. package/src/agents/index.ts +60 -0
  15. package/src/agents/registered-agent.schema.ts +356 -0
  16. package/src/agents/registry.ts +97 -0
  17. package/src/agents/tournament-configs.property.test.ts +266 -0
  18. package/src/cli/README.md +145 -0
  19. package/src/cli/commands/define.ts +79 -0
  20. package/src/cli/commands/list.ts +46 -0
  21. package/src/cli/commands/logs.ts +83 -0
  22. package/src/cli/commands/run.ts +416 -0
  23. package/src/cli/commands/verify.ts +110 -0
  24. package/src/cli/index.ts +81 -0
  25. package/src/config/README.md +128 -0
  26. package/src/config/env.ts +262 -0
  27. package/src/config/index.ts +19 -0
  28. package/src/eval/README.md +318 -0
  29. package/src/eval/ai-judge.test.ts +435 -0
  30. package/src/eval/ai-judge.ts +368 -0
  31. package/src/eval/code-validators.ts +414 -0
  32. package/src/eval/evaluateOutcome.property.test.ts +1174 -0
  33. package/src/eval/evaluateOutcome.ts +591 -0
  34. package/src/eval/immigration-validators.ts +122 -0
  35. package/src/eval/index.ts +90 -0
  36. package/src/eval/judge-cache.ts +402 -0
  37. package/src/eval/tournament-validators.property.test.ts +439 -0
  38. package/src/eval/validators.property.test.ts +1118 -0
  39. package/src/eval/validators.ts +1199 -0
  40. package/src/eval/weighted-scorer.ts +285 -0
  41. package/src/index.ts +17 -0
  42. package/src/league/README.md +188 -0
  43. package/src/league/health-check.ts +353 -0
  44. package/src/league/index.ts +93 -0
  45. package/src/league/killAgent.ts +151 -0
  46. package/src/league/league.test.ts +1151 -0
  47. package/src/league/runLeague.ts +843 -0
  48. package/src/league/scoreAgent.ts +175 -0
  49. package/src/modules/omnibridge/__tests__/.gitkeep +1 -0
  50. package/src/modules/omnibridge/__tests__/auth-tunnel.property.test.ts +524 -0
  51. package/src/modules/omnibridge/__tests__/deterministic-logger.property.test.ts +965 -0
  52. package/src/modules/omnibridge/__tests__/ghost-api.property.test.ts +461 -0
  53. package/src/modules/omnibridge/__tests__/omnibridge-integration.test.ts +542 -0
  54. package/src/modules/omnibridge/__tests__/parallel-executor.property.test.ts +671 -0
  55. package/src/modules/omnibridge/__tests__/semantic-normalizer.property.test.ts +521 -0
  56. package/src/modules/omnibridge/__tests__/semantic-normalizer.test.ts +254 -0
  57. package/src/modules/omnibridge/__tests__/session-vault.property.test.ts +367 -0
  58. package/src/modules/omnibridge/__tests__/shadow-session.property.test.ts +523 -0
  59. package/src/modules/omnibridge/__tests__/triangulation-engine.property.test.ts +292 -0
  60. package/src/modules/omnibridge/__tests__/verification-engine.property.test.ts +769 -0
  61. package/src/modules/omnibridge/api/.gitkeep +1 -0
  62. package/src/modules/omnibridge/api/ghost-api.ts +1087 -0
  63. package/src/modules/omnibridge/auth/.gitkeep +1 -0
  64. package/src/modules/omnibridge/auth/auth-tunnel.ts +843 -0
  65. package/src/modules/omnibridge/auth/session-vault.ts +577 -0
  66. package/src/modules/omnibridge/core/.gitkeep +1 -0
  67. package/src/modules/omnibridge/core/semantic-normalizer.ts +702 -0
  68. package/src/modules/omnibridge/core/triangulation-engine.ts +530 -0
  69. package/src/modules/omnibridge/core/types.ts +610 -0
  70. package/src/modules/omnibridge/execution/.gitkeep +1 -0
  71. package/src/modules/omnibridge/execution/deterministic-logger.ts +629 -0
  72. package/src/modules/omnibridge/execution/parallel-executor.ts +542 -0
  73. package/src/modules/omnibridge/execution/shadow-session.ts +794 -0
  74. package/src/modules/omnibridge/index.ts +212 -0
  75. package/src/modules/omnibridge/omnibridge.ts +510 -0
  76. package/src/modules/omnibridge/verification/.gitkeep +1 -0
  77. package/src/modules/omnibridge/verification/verification-engine.ts +783 -0
  78. package/src/outcomes/README.md +75 -0
  79. package/src/outcomes/acquire-pilot-customer.ts +297 -0
  80. package/src/outcomes/code-delivery-outcomes.ts +89 -0
  81. package/src/outcomes/code-outcomes.ts +256 -0
  82. package/src/outcomes/code_review_battle.test.ts +135 -0
  83. package/src/outcomes/code_review_battle.ts +135 -0
  84. package/src/outcomes/cold_email_battle.ts +97 -0
  85. package/src/outcomes/content_creation_battle.ts +160 -0
  86. package/src/outcomes/f1_stem_opt_compliance.ts +61 -0
  87. package/src/outcomes/index.ts +107 -0
  88. package/src/outcomes/lead_gen_battle.test.ts +113 -0
  89. package/src/outcomes/lead_gen_battle.ts +99 -0
  90. package/src/outcomes/outcome.schema.property.test.ts +229 -0
  91. package/src/outcomes/outcome.schema.ts +187 -0
  92. package/src/outcomes/qualified_sales_interest.ts +118 -0
  93. package/src/outcomes/swarm_planner.property.test.ts +370 -0
  94. package/src/outcomes/swarm_planner.ts +96 -0
  95. package/src/outcomes/web_extraction.ts +234 -0
  96. package/src/runtime/README.md +220 -0
  97. package/src/runtime/agentRunner.test.ts +341 -0
  98. package/src/runtime/agentRunner.ts +746 -0
  99. package/src/runtime/claudeAdapter.ts +232 -0
  100. package/src/runtime/costTracker.ts +123 -0
  101. package/src/runtime/index.ts +34 -0
  102. package/src/runtime/modelAdapter.property.test.ts +305 -0
  103. package/src/runtime/modelAdapter.ts +144 -0
  104. package/src/runtime/openaiAdapter.ts +235 -0
  105. package/src/utils/README.md +122 -0
  106. package/src/utils/command-runner.ts +134 -0
  107. package/src/utils/cost-guard.ts +379 -0
  108. package/src/utils/errors.test.ts +290 -0
  109. package/src/utils/errors.ts +442 -0
  110. package/src/utils/index.ts +37 -0
  111. package/src/utils/logger.test.ts +361 -0
  112. package/src/utils/logger.ts +419 -0
  113. package/src/utils/output-parsers.ts +216 -0
@@ -0,0 +1,542 @@
1
+ /**
2
+ * Parallel Executor - Competition Orchestration
3
+ *
4
+ * Spins up identical sandbox sessions for fair competition between agents.
5
+ * Ensures all agents start with identical states and provides crash isolation.
6
+ *
7
+ * Requirements: 7.1, 7.2, 7.6
8
+ */
9
+
10
+ import { randomBytes } from 'crypto';
11
+ import type {
12
+ Competition,
13
+ CompetitionResult,
14
+ SerializedSession,
15
+ ShadowSession,
16
+ GhostResponse,
17
+ ActionLogEntry,
18
+ EncryptedBlob,
19
+ } from '../core/types.js';
20
+ import {
21
+ ShadowSessionOrchestrator,
22
+ generateFingerprint,
23
+ type SessionCreationResult,
24
+ } from './shadow-session.js';
25
+ import { SessionVault, generateEncryptionKey } from '../auth/session-vault.js';
26
+
27
+ // =============================================================================
28
+ // Types
29
+ // =============================================================================
30
+
31
+ /**
32
+ * Configuration for creating a competition.
33
+ */
34
+ export interface CompetitionConfig {
35
+ /** Bounty ID this competition is for */
36
+ bountyId: string;
37
+ /** Number of agents competing */
38
+ agentCount: number;
39
+ /** Target domain for the competition */
40
+ targetDomain: string;
41
+ /** Initial session state (optional - will create empty if not provided) */
42
+ initialState?: SerializedSession;
43
+ /** Agent IDs participating (optional - will generate if not provided) */
44
+ agentIds?: string[];
45
+ }
46
+
47
+ /**
48
+ * Result of competition creation.
49
+ */
50
+ export interface CompetitionCreationResult {
51
+ success: boolean;
52
+ competition?: Competition;
53
+ error?: string;
54
+ }
55
+
56
+ /**
57
+ * Agent execution task.
58
+ */
59
+ export interface AgentExecutionTask {
60
+ agentId: string;
61
+ sessionId: string;
62
+ execute: () => Promise<AgentExecutionResult>;
63
+ }
64
+
65
+ /**
66
+ * Result of a single agent's execution.
67
+ */
68
+ export interface AgentExecutionResult {
69
+ agentId: string;
70
+ sessionId: string;
71
+ status: 'completed' | 'failed' | 'timeout';
72
+ result?: GhostResponse;
73
+ actionLog: ActionLogEntry[];
74
+ executionTimeMs: number;
75
+ error?: string;
76
+ }
77
+
78
+ /**
79
+ * Parallel execution options.
80
+ */
81
+ export interface ParallelExecutionOptions {
82
+ /** Timeout for each agent in milliseconds (default: 60000) */
83
+ timeoutMs?: number;
84
+ /** Whether to stream results as they complete (default: true) */
85
+ streamResults?: boolean;
86
+ /** Callback for when an agent completes */
87
+ onAgentComplete?: (result: CompetitionResult) => void;
88
+ }
89
+
90
+ /**
91
+ * Configuration for the Parallel Executor.
92
+ */
93
+ export interface ParallelExecutorConfig {
94
+ /** Shadow Session Orchestrator instance */
95
+ sessionOrchestrator?: ShadowSessionOrchestrator;
96
+ /** Session Vault for state management */
97
+ sessionVault?: SessionVault;
98
+ /** Maximum concurrent competitions (default: 10) */
99
+ maxConcurrentCompetitions?: number;
100
+ /** Default agent timeout in milliseconds (default: 60000) */
101
+ defaultTimeoutMs?: number;
102
+ }
103
+
104
+ // =============================================================================
105
+ // Parallel Executor Implementation
106
+ // =============================================================================
107
+
108
+ /**
109
+ * Parallel Executor
110
+ *
111
+ * Orchestrates fair competitions between multiple agents by:
112
+ * - Creating identical sandbox sessions for all agents
113
+ * - Serializing identical starting states
114
+ * - Running agents in parallel with crash isolation
115
+ * - Streaming results as they complete
116
+ *
117
+ * Requirements: 7.1, 7.2, 7.6
118
+ */
119
+ export class ParallelExecutor {
120
+ private readonly sessionOrchestrator: ShadowSessionOrchestrator;
121
+ private readonly sessionVault: SessionVault;
122
+ private readonly maxConcurrentCompetitions: number;
123
+ private readonly defaultTimeoutMs: number;
124
+
125
+ /** Active competitions by ID */
126
+ private competitions: Map<string, Competition> = new Map();
127
+
128
+ /** Competition results by competition ID */
129
+ private results: Map<string, CompetitionResult[]> = new Map();
130
+
131
+ /** Agent execution status by session ID */
132
+ private executionStatus: Map<string, 'pending' | 'running' | 'completed' | 'failed' | 'crashed'> = new Map();
133
+
134
+ constructor(config: ParallelExecutorConfig = {}) {
135
+ this.sessionOrchestrator = config.sessionOrchestrator ?? new ShadowSessionOrchestrator();
136
+ this.sessionVault = config.sessionVault ?? new SessionVault({
137
+ encryptionKey: generateEncryptionKey(),
138
+ });
139
+ this.maxConcurrentCompetitions = config.maxConcurrentCompetitions ?? 10;
140
+ this.defaultTimeoutMs = config.defaultTimeoutMs ?? 60000;
141
+ }
142
+
143
+ // ===========================================================================
144
+ // Competition Creation
145
+ // ===========================================================================
146
+
147
+ /**
148
+ * Generate a unique competition ID.
149
+ */
150
+ private generateCompetitionId(): string {
151
+ return `comp_${randomBytes(16).toString('hex')}`;
152
+ }
153
+
154
+ /**
155
+ * Generate a unique agent ID.
156
+ */
157
+ private generateAgentId(): string {
158
+ return `agent_${randomBytes(8).toString('hex')}`;
159
+ }
160
+
161
+ /**
162
+ * Create an empty serialized session state.
163
+ */
164
+ private createEmptyState(): SerializedSession {
165
+ // Create empty encrypted blobs
166
+ const emptyBlob: EncryptedBlob = this.sessionVault.encrypt('{}');
167
+
168
+ return {
169
+ cookies: emptyBlob,
170
+ localStorage: emptyBlob,
171
+ sessionStorage: emptyBlob,
172
+ expiresAt: Date.now() + 86400000, // 24 hours
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Serialize a session state to ensure byte-identical copies.
178
+ * This is critical for fair competition - all agents must start identically.
179
+ *
180
+ * Requirements: 7.2
181
+ */
182
+ serializeStartState(state: SerializedSession): SerializedSession {
183
+ // Deep clone to ensure isolation
184
+ return {
185
+ cookies: { ...state.cookies },
186
+ localStorage: { ...state.localStorage },
187
+ sessionStorage: { ...state.sessionStorage },
188
+ expiresAt: state.expiresAt,
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Verify that two serialized states are byte-identical.
194
+ *
195
+ * Requirements: 7.2
196
+ */
197
+ areStatesIdentical(state1: SerializedSession, state2: SerializedSession): boolean {
198
+ const str1 = JSON.stringify(state1);
199
+ const str2 = JSON.stringify(state2);
200
+ return str1 === str2;
201
+ }
202
+
203
+ /**
204
+ * Create a competition with N identical sandbox sessions.
205
+ *
206
+ * Requirements: 7.1, 7.2
207
+ */
208
+ async createCompetition(config: CompetitionConfig): Promise<CompetitionCreationResult> {
209
+ // Check concurrent competition limit
210
+ if (this.competitions.size >= this.maxConcurrentCompetitions) {
211
+ return {
212
+ success: false,
213
+ error: `Maximum concurrent competitions (${this.maxConcurrentCompetitions}) reached`,
214
+ };
215
+ }
216
+
217
+ if (config.agentCount < 1) {
218
+ return {
219
+ success: false,
220
+ error: 'Agent count must be at least 1',
221
+ };
222
+ }
223
+
224
+ const competitionId = this.generateCompetitionId();
225
+
226
+ // Generate or use provided agent IDs
227
+ const agentIds = config.agentIds ?? Array.from(
228
+ { length: config.agentCount },
229
+ () => this.generateAgentId()
230
+ );
231
+
232
+ if (agentIds.length !== config.agentCount) {
233
+ return {
234
+ success: false,
235
+ error: `Agent IDs count (${agentIds.length}) must match agent count (${config.agentCount})`,
236
+ };
237
+ }
238
+
239
+ // Create the starting state - this will be identical for all agents
240
+ const startState = config.initialState
241
+ ? this.serializeStartState(config.initialState)
242
+ : this.createEmptyState();
243
+
244
+ // Create shadow sessions for each agent
245
+ const sessions: ShadowSession[] = [];
246
+ const creationErrors: string[] = [];
247
+
248
+ for (const agentId of agentIds) {
249
+ // Generate a unique fingerprint for each agent
250
+ const fingerprint = generateFingerprint(agentId);
251
+
252
+ const result: SessionCreationResult = await this.sessionOrchestrator.create(
253
+ {
254
+ targetDomain: config.targetDomain,
255
+ fingerprint,
256
+ isolationLevel: 'strict',
257
+ },
258
+ agentId
259
+ );
260
+
261
+ if (result.success && result.session) {
262
+ sessions.push(result.session);
263
+ this.executionStatus.set(result.session.id, 'pending');
264
+ } else {
265
+ creationErrors.push(`Failed to create session for agent ${agentId}: ${result.error}`);
266
+ }
267
+ }
268
+
269
+ // If any session creation failed, clean up and return error
270
+ if (creationErrors.length > 0) {
271
+ // Clean up created sessions
272
+ for (const session of sessions) {
273
+ await this.sessionOrchestrator.destroy(session.id);
274
+ }
275
+
276
+ return {
277
+ success: false,
278
+ error: `Failed to create all sessions: ${creationErrors.join('; ')}`,
279
+ };
280
+ }
281
+
282
+ // Create the competition
283
+ const competition: Competition = {
284
+ id: competitionId,
285
+ bountyId: config.bountyId,
286
+ sessions,
287
+ startState,
288
+ };
289
+
290
+ // Store the competition
291
+ this.competitions.set(competitionId, competition);
292
+ this.results.set(competitionId, []);
293
+
294
+ return {
295
+ success: true,
296
+ competition,
297
+ };
298
+ }
299
+
300
+ // ===========================================================================
301
+ // Parallel Execution
302
+ // ===========================================================================
303
+
304
+ /**
305
+ * Start all agents simultaneously.
306
+ *
307
+ * Requirements: 7.1
308
+ */
309
+ async startAll(
310
+ competition: Competition,
311
+ executeTasks: AgentExecutionTask[],
312
+ options: ParallelExecutionOptions = {}
313
+ ): Promise<void> {
314
+ const timeoutMs = options.timeoutMs ?? this.defaultTimeoutMs;
315
+
316
+ // Validate that we have tasks for all sessions
317
+ if (executeTasks.length > competition.sessions.length) {
318
+ throw new Error(`More tasks (${executeTasks.length}) than sessions (${competition.sessions.length})`);
319
+ }
320
+
321
+ // Mark competition as started
322
+ competition.startedAt = Date.now();
323
+
324
+ // Update all sessions to running
325
+ for (const session of competition.sessions) {
326
+ this.executionStatus.set(session.id, 'running');
327
+ }
328
+
329
+ // Execute all tasks in parallel
330
+ const promises = executeTasks.map(async (task) => {
331
+ return this.executeWithCrashIsolation(task, timeoutMs, options.onAgentComplete);
332
+ });
333
+
334
+ // Wait for all to complete (they won't throw due to crash isolation)
335
+ await Promise.all(promises);
336
+ }
337
+
338
+ /**
339
+ * Execute a single agent task with crash isolation.
340
+ *
341
+ * Requirements: 7.6
342
+ */
343
+ private async executeWithCrashIsolation(
344
+ task: AgentExecutionTask,
345
+ timeoutMs: number,
346
+ onComplete?: (result: CompetitionResult) => void
347
+ ): Promise<CompetitionResult> {
348
+ const startTime = Date.now();
349
+
350
+ try {
351
+ // Create a timeout promise
352
+ const timeoutPromise = new Promise<AgentExecutionResult>((_, reject) => {
353
+ setTimeout(() => {
354
+ reject(new Error('Execution timeout'));
355
+ }, timeoutMs);
356
+ });
357
+
358
+ // Race between execution and timeout
359
+ const result = await Promise.race([
360
+ task.execute(),
361
+ timeoutPromise,
362
+ ]);
363
+
364
+ const competitionResult: CompetitionResult = {
365
+ agentId: result.agentId,
366
+ sessionId: result.sessionId,
367
+ status: result.status,
368
+ result: result.result,
369
+ actionLog: result.actionLog,
370
+ executionTimeMs: result.executionTimeMs,
371
+ };
372
+
373
+ this.executionStatus.set(task.sessionId, 'completed');
374
+
375
+ if (onComplete) {
376
+ onComplete(competitionResult);
377
+ }
378
+
379
+ return competitionResult;
380
+ } catch (error) {
381
+ // Handle crash or timeout - mark as failed but don't affect others
382
+ const executionTimeMs = Date.now() - startTime;
383
+ const isTimeout = error instanceof Error && error.message === 'Execution timeout';
384
+
385
+ const competitionResult: CompetitionResult = {
386
+ agentId: task.agentId,
387
+ sessionId: task.sessionId,
388
+ status: isTimeout ? 'timeout' : 'failed',
389
+ actionLog: [],
390
+ executionTimeMs,
391
+ };
392
+
393
+ this.executionStatus.set(task.sessionId, 'crashed');
394
+
395
+ if (onComplete) {
396
+ onComplete(competitionResult);
397
+ }
398
+
399
+ return competitionResult;
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Get results for a competition.
405
+ */
406
+ getResults(competitionId: string): CompetitionResult[] {
407
+ return this.results.get(competitionId) ?? [];
408
+ }
409
+
410
+ /**
411
+ * Add a result to a competition.
412
+ */
413
+ addResult(competitionId: string, result: CompetitionResult): void {
414
+ const results = this.results.get(competitionId);
415
+ if (results) {
416
+ results.push(result);
417
+ }
418
+ }
419
+
420
+ // ===========================================================================
421
+ // Crash Isolation
422
+ // ===========================================================================
423
+
424
+ /**
425
+ * Check if a session has crashed.
426
+ */
427
+ hasSessionCrashed(sessionId: string): boolean {
428
+ return this.executionStatus.get(sessionId) === 'crashed';
429
+ }
430
+
431
+ /**
432
+ * Get execution status for a session.
433
+ */
434
+ getExecutionStatus(sessionId: string): string | undefined {
435
+ return this.executionStatus.get(sessionId);
436
+ }
437
+
438
+ /**
439
+ * Mark a session as crashed.
440
+ *
441
+ * Requirements: 7.6
442
+ */
443
+ markSessionCrashed(sessionId: string): void {
444
+ this.executionStatus.set(sessionId, 'crashed');
445
+ }
446
+
447
+ /**
448
+ * Check if other sessions are affected by a crash.
449
+ * Should always return false - crashes are isolated.
450
+ *
451
+ * Requirements: 7.6
452
+ */
453
+ areOtherSessionsAffected(competitionId: string, crashedSessionId: string): boolean {
454
+ const competition = this.competitions.get(competitionId);
455
+ if (!competition) {
456
+ return false;
457
+ }
458
+
459
+ // Check all other sessions
460
+ for (const session of competition.sessions) {
461
+ if (session.id === crashedSessionId) {
462
+ continue;
463
+ }
464
+
465
+ const status = this.executionStatus.get(session.id);
466
+ // If any other session is marked as crashed due to this crash, isolation failed
467
+ // In our implementation, this should never happen
468
+ if (status === 'crashed') {
469
+ // Check if it crashed independently or due to the other crash
470
+ // Since we use Promise.race with individual try/catch, crashes are isolated
471
+ continue; // Independent crash, not affected
472
+ }
473
+ }
474
+
475
+ return false;
476
+ }
477
+
478
+ // ===========================================================================
479
+ // Competition Management
480
+ // ===========================================================================
481
+
482
+ /**
483
+ * Get a competition by ID.
484
+ */
485
+ getCompetition(competitionId: string): Competition | undefined {
486
+ return this.competitions.get(competitionId);
487
+ }
488
+
489
+ /**
490
+ * Get all active competitions.
491
+ */
492
+ getActiveCompetitions(): Competition[] {
493
+ return Array.from(this.competitions.values());
494
+ }
495
+
496
+ /**
497
+ * Clean up a competition and its resources.
498
+ */
499
+ async cleanupCompetition(competitionId: string): Promise<void> {
500
+ const competition = this.competitions.get(competitionId);
501
+ if (!competition) {
502
+ return;
503
+ }
504
+
505
+ // Destroy all sessions
506
+ for (const session of competition.sessions) {
507
+ await this.sessionOrchestrator.destroy(session.id);
508
+ this.executionStatus.delete(session.id);
509
+ }
510
+
511
+ // Remove competition data
512
+ this.competitions.delete(competitionId);
513
+ this.results.delete(competitionId);
514
+ }
515
+
516
+ /**
517
+ * Clear all competitions (for testing).
518
+ */
519
+ async clear(): Promise<void> {
520
+ for (const competitionId of this.competitions.keys()) {
521
+ await this.cleanupCompetition(competitionId);
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Get the session orchestrator (for testing).
527
+ */
528
+ getSessionOrchestrator(): ShadowSessionOrchestrator {
529
+ return this.sessionOrchestrator;
530
+ }
531
+ }
532
+
533
+ // =============================================================================
534
+ // Factory Function
535
+ // =============================================================================
536
+
537
+ /**
538
+ * Create a new Parallel Executor instance.
539
+ */
540
+ export function createParallelExecutor(config?: ParallelExecutorConfig): ParallelExecutor {
541
+ return new ParallelExecutor(config);
542
+ }