@automagik/genie 0.260202.1607 → 0.260202.1833

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.
@@ -0,0 +1,844 @@
1
+ /**
2
+ * Orchestrate command - Claude Code session orchestration
3
+ *
4
+ * Provides commands for monitoring and controlling Claude Code sessions:
5
+ * - start: Start Claude Code in a session with optional monitoring
6
+ * - send: Send message and track completion
7
+ * - status: Show current Claude state
8
+ * - watch: Real-time event streaming
9
+ * - approve: Handle permission requests
10
+ * - answer: Answer questions
11
+ * - experiment: Test completion detection methods
12
+ */
13
+
14
+ import * as tmux from '../lib/tmux.js';
15
+ import {
16
+ EventMonitor,
17
+ ClaudeEvent,
18
+ ClaudeState,
19
+ detectState,
20
+ extractPermissionDetails,
21
+ extractQuestionOptions,
22
+ extractPlanFile,
23
+ getMethod,
24
+ getDefaultMethod,
25
+ presetMethods,
26
+ PresetMethodName,
27
+ CompletionMethodMetrics,
28
+ stripAnsi,
29
+ } from '../lib/orchestrator/index.js';
30
+
31
+ // ============================================================================
32
+ // Types
33
+ // ============================================================================
34
+
35
+ export interface StartOptions {
36
+ pane?: string;
37
+ monitor?: boolean;
38
+ command?: string;
39
+ json?: boolean;
40
+ }
41
+
42
+ export interface RunOptions {
43
+ pane?: string;
44
+ autoApprove?: boolean;
45
+ timeout?: number;
46
+ json?: boolean;
47
+ }
48
+
49
+ export interface SendOptions {
50
+ method?: string;
51
+ timeout?: number;
52
+ json?: boolean;
53
+ noWait?: boolean;
54
+ pane?: string;
55
+ }
56
+
57
+ export interface StatusOptions {
58
+ json?: boolean;
59
+ pane?: string;
60
+ }
61
+
62
+ export interface WatchOptions {
63
+ json?: boolean;
64
+ poll?: number;
65
+ pane?: string;
66
+ }
67
+
68
+ export interface ApproveOptions {
69
+ pane?: string;
70
+ auto?: boolean;
71
+ deny?: boolean;
72
+ }
73
+
74
+ export interface ExperimentOptions {
75
+ runs?: number;
76
+ task?: string;
77
+ json?: boolean;
78
+ }
79
+
80
+ // ============================================================================
81
+ // Helper Functions
82
+ // ============================================================================
83
+
84
+ async function getSessionPane(
85
+ sessionName: string,
86
+ targetPaneId?: string
87
+ ): Promise<{ session: tmux.TmuxSession; paneId: string }> {
88
+ const session = await tmux.findSessionByName(sessionName);
89
+ if (!session) {
90
+ console.error(`❌ Session "${sessionName}" not found`);
91
+ process.exit(1);
92
+ }
93
+
94
+ // If specific pane ID provided, validate and use it
95
+ if (targetPaneId) {
96
+ // Normalize pane ID (add % prefix if missing)
97
+ const paneId = targetPaneId.startsWith('%') ? targetPaneId : `%${targetPaneId}`;
98
+ return { session, paneId };
99
+ }
100
+
101
+ const windows = await tmux.listWindows(session.id);
102
+ if (!windows || windows.length === 0) {
103
+ console.error(`❌ No windows found in session "${sessionName}"`);
104
+ process.exit(1);
105
+ }
106
+
107
+ const panes = await tmux.listPanes(windows[0].id);
108
+ if (!panes || panes.length === 0) {
109
+ console.error(`❌ No panes found in session "${sessionName}"`);
110
+ process.exit(1);
111
+ }
112
+
113
+ return { session, paneId: panes[0].id };
114
+ }
115
+
116
+ function formatState(state: ClaudeState): string {
117
+ let result = `${state.type}`;
118
+ if (state.detail) {
119
+ result += ` (${state.detail})`;
120
+ }
121
+ if (state.options && state.options.length > 0) {
122
+ result += `\n Options: ${state.options.join(', ')}`;
123
+ }
124
+ result += ` [confidence: ${(state.confidence * 100).toFixed(0)}%]`;
125
+ return result;
126
+ }
127
+
128
+ function formatEvent(event: ClaudeEvent): string {
129
+ const time = new Date(event.timestamp).toISOString().split('T')[1].split('.')[0];
130
+
131
+ switch (event.type) {
132
+ case 'output':
133
+ return `[${time}] OUTPUT: ${(event.output || '').substring(0, 100).replace(/\n/g, '\\n')}`;
134
+ case 'state_change':
135
+ return `[${time}] STATE: ${event.state?.type || 'unknown'}${event.state?.detail ? ` (${event.state.detail})` : ''}`;
136
+ case 'silence':
137
+ return `[${time}] SILENCE: ${event.silenceMs}ms`;
138
+ case 'activity':
139
+ return `[${time}] ACTIVITY`;
140
+ case 'permission':
141
+ return `[${time}] PERMISSION: ${event.state?.detail || 'unknown'}`;
142
+ case 'question':
143
+ return `[${time}] QUESTION: ${event.state?.options?.join(', ') || 'unknown'}`;
144
+ case 'error':
145
+ return `[${time}] ERROR: ${event.state?.detail || 'unknown'}`;
146
+ case 'complete':
147
+ return `[${time}] COMPLETE`;
148
+ default:
149
+ return `[${time}] ${event.type}`;
150
+ }
151
+ }
152
+
153
+ // ============================================================================
154
+ // Commands
155
+ // ============================================================================
156
+
157
+ /**
158
+ * Start Claude Code in a session with optional monitoring
159
+ */
160
+ export async function startSession(
161
+ sessionName: string,
162
+ options: StartOptions = {}
163
+ ): Promise<void> {
164
+ try {
165
+ // Check if session exists
166
+ let session = await tmux.findSessionByName(sessionName);
167
+
168
+ if (!session) {
169
+ // Create new session
170
+ session = await tmux.createSession(sessionName);
171
+ if (!session) {
172
+ console.error(`❌ Failed to create session "${sessionName}"`);
173
+ process.exit(1);
174
+ }
175
+ console.log(`✅ Created session "${sessionName}"`);
176
+ } else {
177
+ console.log(`ℹ️ Session "${sessionName}" already exists`);
178
+ }
179
+
180
+ // Get pane (use specified pane or default to first pane)
181
+ let paneId: string;
182
+ if (options.pane) {
183
+ paneId = options.pane.startsWith('%') ? options.pane : `%${options.pane}`;
184
+ } else {
185
+ const windows = await tmux.listWindows(session.id);
186
+ const panes = await tmux.listPanes(windows[0].id);
187
+ paneId = panes[0].id;
188
+ }
189
+
190
+ // Start Claude Code (or custom command)
191
+ const command = options.command || 'claude';
192
+ await tmux.executeCommand(paneId, command, false, false);
193
+ console.log(`✅ Started "${command}" in session "${sessionName}"`);
194
+
195
+ // Start monitoring if requested
196
+ if (options.monitor) {
197
+ console.log(`ℹ️ Starting event monitor...`);
198
+
199
+ const monitor = new EventMonitor(sessionName, {
200
+ pollIntervalMs: 500,
201
+ paneId: options.pane,
202
+ });
203
+
204
+ monitor.on('event', (event: ClaudeEvent) => {
205
+ if (options.json) {
206
+ console.log(JSON.stringify(event));
207
+ } else {
208
+ console.log(formatEvent(event));
209
+ }
210
+ });
211
+
212
+ monitor.on('poll_error', (error: Error) => {
213
+ console.error(`⚠️ Poll error: ${error.message}`);
214
+ });
215
+
216
+ await monitor.start();
217
+ console.log(`✅ Monitor active. Press Ctrl+C to stop.`);
218
+
219
+ // Keep running until interrupted
220
+ process.on('SIGINT', () => {
221
+ monitor.stop();
222
+ console.log('\n✅ Monitor stopped.');
223
+ process.exit(0);
224
+ });
225
+
226
+ // Keep process alive
227
+ await new Promise(() => {});
228
+ }
229
+ } catch (error: any) {
230
+ console.error(`❌ Error: ${error.message}`);
231
+ process.exit(1);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Send a message to Claude and track completion
237
+ */
238
+ export async function sendMessage(
239
+ sessionName: string,
240
+ message: string,
241
+ options: SendOptions = {}
242
+ ): Promise<void> {
243
+ try {
244
+ const { paneId } = await getSessionPane(sessionName, options.pane);
245
+
246
+ // Send the message
247
+ await tmux.executeCommand(paneId, message, false, false);
248
+
249
+ if (options.noWait) {
250
+ console.log(`✅ Message sent to session "${sessionName}"`);
251
+ return;
252
+ }
253
+
254
+ // Start monitoring for completion
255
+ const monitor = new EventMonitor(sessionName, {
256
+ pollIntervalMs: 250,
257
+ paneId: options.pane,
258
+ });
259
+
260
+ await monitor.start();
261
+
262
+ // Use specified completion method or default
263
+ const method = options.method ? getMethod(options.method) : getDefaultMethod();
264
+ const timeoutMs = options.timeout || 120000;
265
+
266
+ console.log(`ℹ️ Waiting for completion using "${method.name}"...`);
267
+
268
+ try {
269
+ const result = await method.detect(monitor, timeoutMs);
270
+ monitor.stop();
271
+
272
+ if (options.json) {
273
+ console.log(JSON.stringify(result, null, 2));
274
+ } else {
275
+ console.log(`✅ Completion detected: ${result.reason}`);
276
+ console.log(` Latency: ${result.latencyMs}ms`);
277
+ if (result.state) {
278
+ console.log(` State: ${formatState(result.state)}`);
279
+ }
280
+ }
281
+
282
+ // Capture and output the response
283
+ const output = await tmux.capturePaneContent(paneId, 100);
284
+ console.log('\n--- Response ---');
285
+ console.log(stripAnsi(output).trim());
286
+ } catch (error: any) {
287
+ monitor.stop();
288
+ console.error(`❌ Completion detection failed: ${error.message}`);
289
+ process.exit(1);
290
+ }
291
+ } catch (error: any) {
292
+ console.error(`❌ Error: ${error.message}`);
293
+ process.exit(1);
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Show current Claude state
299
+ */
300
+ export async function showStatus(
301
+ sessionName: string,
302
+ options: StatusOptions = {}
303
+ ): Promise<void> {
304
+ try {
305
+ const { paneId } = await getSessionPane(sessionName, options.pane);
306
+
307
+ // Capture current output
308
+ const output = await tmux.capturePaneContent(paneId, 100);
309
+
310
+ // Detect state
311
+ const state = detectState(output);
312
+
313
+ // Get permission/question details if applicable
314
+ let permissionDetails = null;
315
+ let questionOptions: string[] = [];
316
+ let planFile: string | null = null;
317
+
318
+ if (state.type === 'permission') {
319
+ permissionDetails = extractPermissionDetails(output);
320
+ } else if (state.type === 'question') {
321
+ questionOptions = extractQuestionOptions(output);
322
+ // Check for plan file when in plan approval state
323
+ if (state.detail === 'plan_approval') {
324
+ planFile = extractPlanFile(output);
325
+ }
326
+ }
327
+
328
+ if (options.json) {
329
+ console.log(
330
+ JSON.stringify(
331
+ {
332
+ session: sessionName,
333
+ state: state.type,
334
+ detail: state.detail,
335
+ confidence: state.confidence,
336
+ timestamp: state.timestamp,
337
+ permissionDetails,
338
+ questionOptions,
339
+ planFile,
340
+ },
341
+ null,
342
+ 2
343
+ )
344
+ );
345
+ } else {
346
+ console.log(`Session: ${sessionName}`);
347
+ console.log(`State: ${state.type}`);
348
+ if (state.detail) {
349
+ console.log(`Detail: ${state.detail}`);
350
+ }
351
+ console.log(`Confidence: ${(state.confidence * 100).toFixed(0)}%`);
352
+
353
+ if (permissionDetails) {
354
+ console.log(`\nPermission Request:`);
355
+ console.log(` Type: ${permissionDetails.type}`);
356
+ if (permissionDetails.command) {
357
+ console.log(` Command: ${permissionDetails.command}`);
358
+ }
359
+ if (permissionDetails.file) {
360
+ console.log(` File: ${permissionDetails.file}`);
361
+ }
362
+ }
363
+
364
+ if (questionOptions.length > 0) {
365
+ console.log(`\nQuestion Options:`);
366
+ questionOptions.forEach((opt, i) => {
367
+ console.log(` [${i + 1}] ${opt}`);
368
+ });
369
+ }
370
+
371
+ if (planFile) {
372
+ console.log(`\nPlan File: ${planFile}`);
373
+ }
374
+
375
+ // Show last few lines of output
376
+ const lines = stripAnsi(output).trim().split('\n');
377
+ const lastLines = lines.slice(-5);
378
+ console.log(`\nLast output:`);
379
+ lastLines.forEach((line) => console.log(` ${line}`));
380
+ }
381
+ } catch (error: any) {
382
+ console.error(`❌ Error: ${error.message}`);
383
+ process.exit(1);
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Watch session events in real-time
389
+ */
390
+ export async function watchSession(
391
+ sessionName: string,
392
+ options: WatchOptions = {}
393
+ ): Promise<void> {
394
+ try {
395
+ const monitor = new EventMonitor(sessionName, {
396
+ pollIntervalMs: options.poll || 500,
397
+ paneId: options.pane,
398
+ });
399
+
400
+ monitor.on('event', (event: ClaudeEvent) => {
401
+ if (options.json) {
402
+ console.log(JSON.stringify(event));
403
+ } else {
404
+ console.log(formatEvent(event));
405
+ }
406
+ });
407
+
408
+ monitor.on('poll_error', (error: Error) => {
409
+ console.error(`⚠️ Poll error: ${error.message}`);
410
+ });
411
+
412
+ await monitor.start();
413
+ console.log(`✅ Watching session "${sessionName}". Press Ctrl+C to stop.`);
414
+
415
+ // Show initial state
416
+ const state = monitor.getCurrentState();
417
+ if (state) {
418
+ console.log(`Initial state: ${formatState(state)}`);
419
+ }
420
+
421
+ // Handle Ctrl+C
422
+ process.on('SIGINT', () => {
423
+ monitor.stop();
424
+ console.log('\n✅ Watch stopped.');
425
+ process.exit(0);
426
+ });
427
+
428
+ // Keep process alive
429
+ await new Promise(() => {});
430
+ } catch (error: any) {
431
+ console.error(`❌ Error: ${error.message}`);
432
+ process.exit(1);
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Approve a pending permission request
438
+ */
439
+ export async function approvePermission(
440
+ sessionName: string,
441
+ options: ApproveOptions = {}
442
+ ): Promise<void> {
443
+ try {
444
+ const { paneId } = await getSessionPane(sessionName, options.pane);
445
+
446
+ // Check current state
447
+ const output = await tmux.capturePaneContent(paneId, 50);
448
+ const state = detectState(output);
449
+
450
+ if (state.type !== 'permission' && !options.auto) {
451
+ console.log(`ℹ️ No permission request pending (state: ${state.type})`);
452
+ return;
453
+ }
454
+
455
+ // For Claude Code, permissions use a menu with ❯ cursor
456
+ // We need to navigate to the right option and press Enter
457
+ // Option 1 is "Yes" (approve), other options are deny or more specific
458
+ if (options.deny) {
459
+ // Navigate down to "No" option (typically option 2 or 3)
460
+ await tmux.executeTmux(`send-keys -t '${paneId}' Down`);
461
+ await sleep(100);
462
+ }
463
+ // Press Enter to confirm selection
464
+ await tmux.executeTmux(`send-keys -t '${paneId}' Enter`);
465
+
466
+ console.log(`✅ ${options.deny ? 'Denied' : 'Approved'} permission in session "${sessionName}"`);
467
+
468
+ // If auto mode, keep monitoring and approving
469
+ if (options.auto) {
470
+ console.log(`ℹ️ Auto-approve mode enabled. Press Ctrl+C to stop.`);
471
+
472
+ const monitor = new EventMonitor(sessionName, {
473
+ pollIntervalMs: 250,
474
+ paneId: options.pane,
475
+ });
476
+
477
+ monitor.on('permission', async (event: ClaudeEvent) => {
478
+ try {
479
+ const { paneId: currentPaneId } = await getSessionPane(sessionName, options.pane);
480
+ const response = options.deny ? 'n' : 'y';
481
+ await tmux.executeCommand(currentPaneId, response, false, true);
482
+ console.log(`✅ Auto-${options.deny ? 'denied' : 'approved'}: ${event.state?.detail || 'unknown'}`);
483
+ } catch (err: any) {
484
+ console.error(`⚠️ Auto-approve failed: ${err.message}`);
485
+ }
486
+ });
487
+
488
+ await monitor.start();
489
+
490
+ process.on('SIGINT', () => {
491
+ monitor.stop();
492
+ console.log('\n✅ Auto-approve stopped.');
493
+ process.exit(0);
494
+ });
495
+
496
+ await new Promise(() => {});
497
+ }
498
+ } catch (error: any) {
499
+ console.error(`❌ Error: ${error.message}`);
500
+ process.exit(1);
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Answer a question with options
506
+ *
507
+ * For Claude Code menus:
508
+ * - Numeric choice (1-9): Navigate to that option and select
509
+ * - "text:..." prefix: Type text directly (for option 4 "Type here...")
510
+ * - Other: Send as raw keystrokes
511
+ */
512
+ export async function answerQuestion(
513
+ sessionName: string,
514
+ choice: string,
515
+ options: { pane?: string } = {}
516
+ ): Promise<void> {
517
+ try {
518
+ const { paneId } = await getSessionPane(sessionName, options.pane);
519
+
520
+ // Check current state
521
+ const output = await tmux.capturePaneContent(paneId, 50);
522
+ const state = detectState(output);
523
+
524
+ if (state.type !== 'question') {
525
+ console.log(`ℹ️ No question pending (state: ${state.type})`);
526
+ return;
527
+ }
528
+
529
+ // Handle different choice formats
530
+ if (choice.startsWith('text:')) {
531
+ // Send text directly (for "Type here to tell Claude what to change")
532
+ const text = choice.slice(5);
533
+ // First select option 4 (or last option) to get to text input
534
+ await tmux.executeTmux(`send-keys -t '${paneId}' End`);
535
+ await sleep(100);
536
+ await tmux.executeTmux(`send-keys -t '${paneId}' Enter`);
537
+ await sleep(100);
538
+ // Now type the feedback
539
+ await tmux.executeTmux(`send-keys -t '${paneId}' ${shellEscape(text)}`);
540
+ await sleep(100);
541
+ await tmux.executeTmux(`send-keys -t '${paneId}' Enter`);
542
+ console.log(`✅ Sent feedback: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}"`);
543
+ } else if (/^\d+$/.test(choice)) {
544
+ // Numeric choice - navigate using arrow keys to the option
545
+ const targetOption = parseInt(choice, 10);
546
+
547
+ // Find current selection position by looking for ❯
548
+ const cleanOutput = stripAnsi(output);
549
+ const lines = cleanOutput.split('\n');
550
+ let currentOption = 1;
551
+ for (let i = 0; i < lines.length; i++) {
552
+ if (lines[i].match(/^\s*❯\s*\d+\./)) {
553
+ const match = lines[i].match(/❯\s*(\d+)\./);
554
+ if (match) currentOption = parseInt(match[1], 10);
555
+ break;
556
+ }
557
+ }
558
+
559
+ // Navigate to target option
560
+ const diff = targetOption - currentOption;
561
+ if (diff > 0) {
562
+ for (let i = 0; i < diff; i++) {
563
+ await tmux.executeTmux(`send-keys -t '${paneId}' Down`);
564
+ await sleep(50);
565
+ }
566
+ } else if (diff < 0) {
567
+ for (let i = 0; i < Math.abs(diff); i++) {
568
+ await tmux.executeTmux(`send-keys -t '${paneId}' Up`);
569
+ await sleep(50);
570
+ }
571
+ }
572
+
573
+ // Press Enter to select
574
+ await sleep(100);
575
+ await tmux.executeTmux(`send-keys -t '${paneId}' Enter`);
576
+ console.log(`✅ Selected option ${targetOption} in session "${sessionName}"`);
577
+ } else {
578
+ // Raw keystroke (e.g., 'y', 'n', 'Enter')
579
+ await tmux.executeTmux(`send-keys -t '${paneId}' '${choice}'`);
580
+ console.log(`✅ Sent '${choice}' to session "${sessionName}"`);
581
+ }
582
+ } catch (error: any) {
583
+ console.error(`❌ Error: ${error.message}`);
584
+ process.exit(1);
585
+ }
586
+ }
587
+
588
+ // Helper to escape shell arguments
589
+ function shellEscape(str: string): string {
590
+ return `"${str.replace(/"/g, '\\"').replace(/\$/g, '\\$')}"`;
591
+ }
592
+
593
+ // Helper to sleep
594
+ function sleep(ms: number): Promise<void> {
595
+ return new Promise(resolve => setTimeout(resolve, ms));
596
+ }
597
+
598
+ /**
599
+ * Test a completion detection method
600
+ */
601
+ export async function runExperiment(
602
+ methodName: string,
603
+ options: ExperimentOptions = {}
604
+ ): Promise<void> {
605
+ const runs = options.runs || 1;
606
+ const testTask = options.task || 'echo "Hello, World!"';
607
+
608
+ console.log(`🧪 Experiment: Testing "${methodName}" method`);
609
+ console.log(` Runs: ${runs}`);
610
+ console.log(` Task: ${testTask}`);
611
+ console.log('');
612
+
613
+ const method = getMethod(methodName);
614
+ const results: Array<{
615
+ run: number;
616
+ latencyMs: number;
617
+ complete: boolean;
618
+ reason: string;
619
+ }> = [];
620
+
621
+ // Create a test session
622
+ const testSessionName = `orc-experiment-${Date.now()}`;
623
+ let session = await tmux.createSession(testSessionName);
624
+ if (!session) {
625
+ console.error('❌ Failed to create test session');
626
+ process.exit(1);
627
+ }
628
+
629
+ try {
630
+ const windows = await tmux.listWindows(session.id);
631
+ const panes = await tmux.listPanes(windows[0].id);
632
+ const paneId = panes[0].id;
633
+
634
+ for (let i = 0; i < runs; i++) {
635
+ console.log(`\nRun ${i + 1}/${runs}...`);
636
+
637
+ // Clear pane
638
+ await tmux.executeCommand(paneId, 'clear', false, false);
639
+ await new Promise((r) => setTimeout(r, 500));
640
+
641
+ // Start monitor
642
+ const monitor = new EventMonitor(testSessionName, {
643
+ pollIntervalMs: 100,
644
+ });
645
+ await monitor.start();
646
+
647
+ // Execute task
648
+ await tmux.executeCommand(paneId, testTask, false, false);
649
+
650
+ // Detect completion
651
+ const result = await method.detect(monitor, 30000);
652
+ monitor.stop();
653
+
654
+ results.push({
655
+ run: i + 1,
656
+ latencyMs: result.latencyMs,
657
+ complete: result.complete,
658
+ reason: result.reason,
659
+ });
660
+
661
+ console.log(` Complete: ${result.complete}`);
662
+ console.log(` Latency: ${result.latencyMs}ms`);
663
+ console.log(` Reason: ${result.reason}`);
664
+
665
+ // Wait between runs
666
+ if (i < runs - 1) {
667
+ await new Promise((r) => setTimeout(r, 1000));
668
+ }
669
+ }
670
+
671
+ // Calculate statistics
672
+ const successfulRuns = results.filter((r) => r.complete);
673
+ const avgLatency =
674
+ successfulRuns.reduce((sum, r) => sum + r.latencyMs, 0) / successfulRuns.length || 0;
675
+ const minLatency = Math.min(...successfulRuns.map((r) => r.latencyMs)) || 0;
676
+ const maxLatency = Math.max(...successfulRuns.map((r) => r.latencyMs)) || 0;
677
+
678
+ const summary = {
679
+ method: methodName,
680
+ totalRuns: runs,
681
+ successfulRuns: successfulRuns.length,
682
+ successRate: (successfulRuns.length / runs) * 100,
683
+ avgLatencyMs: Math.round(avgLatency),
684
+ minLatencyMs: minLatency,
685
+ maxLatencyMs: maxLatency,
686
+ results,
687
+ };
688
+
689
+ console.log('\n--- Summary ---');
690
+ if (options.json) {
691
+ console.log(JSON.stringify(summary, null, 2));
692
+ } else {
693
+ console.log(`Method: ${summary.method}`);
694
+ console.log(`Success Rate: ${summary.successRate.toFixed(1)}%`);
695
+ console.log(`Avg Latency: ${summary.avgLatencyMs}ms`);
696
+ console.log(`Min Latency: ${summary.minLatencyMs}ms`);
697
+ console.log(`Max Latency: ${summary.maxLatencyMs}ms`);
698
+ }
699
+ } finally {
700
+ // Cleanup test session
701
+ await tmux.killSession(session.id);
702
+ console.log(`\n✅ Cleaned up test session`);
703
+ }
704
+ }
705
+
706
+ /**
707
+ * List available completion methods
708
+ */
709
+ export async function listMethods(): Promise<void> {
710
+ console.log('Available completion methods:');
711
+ console.log('');
712
+
713
+ for (const name of Object.keys(presetMethods) as PresetMethodName[]) {
714
+ const method = presetMethods[name]();
715
+ console.log(` ${name}`);
716
+ console.log(` ${method.description}`);
717
+ console.log('');
718
+ }
719
+
720
+ console.log('Custom methods:');
721
+ console.log(' silence-Xms - Silence timeout (e.g., silence-2000ms)');
722
+ console.log(' silence-Xs - Silence timeout (e.g., silence-5s)');
723
+ }
724
+
725
+ /**
726
+ * Run a task with auto-approval until idle
727
+ *
728
+ * Fire-and-forget: send message, auto-approve permissions, wait for idle
729
+ */
730
+ export async function runTask(
731
+ sessionName: string,
732
+ message: string,
733
+ options: RunOptions = {}
734
+ ): Promise<void> {
735
+ try {
736
+ const { paneId } = await getSessionPane(sessionName, options.pane);
737
+ const timeoutMs = options.timeout || 300000; // 5 minute default
738
+
739
+ // Send the message
740
+ await tmux.executeCommand(paneId, message, false, false);
741
+ console.log(`✅ Sent task: "${message.substring(0, 50)}${message.length > 50 ? '...' : ''}"`);
742
+
743
+ // Start monitoring
744
+ const monitor = new EventMonitor(sessionName, {
745
+ pollIntervalMs: 250,
746
+ paneId: options.pane,
747
+ });
748
+
749
+ await monitor.start();
750
+ const startTime = Date.now();
751
+
752
+ console.log(`ℹ️ Monitoring for completion...${options.autoApprove ? ' (auto-approve enabled)' : ''}`);
753
+
754
+ // Main monitoring loop
755
+ let lastState: string | null = null;
756
+
757
+ const checkState = async (): Promise<boolean> => {
758
+ const output = await tmux.capturePaneContent(paneId, 30);
759
+ const state = detectState(output);
760
+
761
+ // Log state changes
762
+ if (state.type !== lastState) {
763
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
764
+ console.log(`[${elapsed}s] State: ${state.type}${state.detail ? ` (${state.detail})` : ''}`);
765
+ lastState = state.type;
766
+ }
767
+
768
+ // Handle permission requests
769
+ if (state.type === 'permission' && options.autoApprove) {
770
+ console.log(` ↳ Auto-approving permission...`);
771
+ await tmux.executeTmux(`send-keys -t '${paneId}' Enter`);
772
+ await sleep(200);
773
+ return false; // Continue monitoring
774
+ }
775
+
776
+ // Handle question states
777
+ if (state.type === 'question') {
778
+ if (state.detail === 'plan_approval' && options.autoApprove) {
779
+ // Auto-approve plans
780
+ console.log(` ↳ Auto-approving plan...`);
781
+ await tmux.executeTmux(`send-keys -t '${paneId}' Enter`);
782
+ await sleep(200);
783
+ return false; // Continue monitoring
784
+ }
785
+ // Other questions require user input - exit and report
786
+ console.log(`⚠️ Question requires manual input`);
787
+ const questionOptions = extractQuestionOptions(output);
788
+ if (questionOptions.length > 0) {
789
+ console.log(` Options:`);
790
+ questionOptions.forEach((opt, i) => console.log(` [${i + 1}] ${opt}`));
791
+ }
792
+ return true; // Stop monitoring, need user input
793
+ }
794
+
795
+ // Check for idle (task complete)
796
+ if (state.type === 'idle') {
797
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
798
+ console.log(`✅ Task complete (${elapsed}s)`);
799
+ return true; // Done
800
+ }
801
+
802
+ // Check for error
803
+ if (state.type === 'error') {
804
+ console.log(`❌ Task encountered error`);
805
+ return true; // Done (with error)
806
+ }
807
+
808
+ return false; // Continue monitoring
809
+ };
810
+
811
+ // Poll until done or timeout
812
+ const pollInterval = 500;
813
+ const maxIterations = Math.ceil(timeoutMs / pollInterval);
814
+
815
+ for (let i = 0; i < maxIterations; i++) {
816
+ const done = await checkState();
817
+ if (done) break;
818
+
819
+ if (Date.now() - startTime > timeoutMs) {
820
+ console.log(`⚠️ Timeout after ${timeoutMs / 1000}s`);
821
+ break;
822
+ }
823
+
824
+ await sleep(pollInterval);
825
+ }
826
+
827
+ monitor.stop();
828
+
829
+ // Output final state as JSON if requested
830
+ if (options.json) {
831
+ const output = await tmux.capturePaneContent(paneId, 30);
832
+ const state = detectState(output);
833
+ console.log(JSON.stringify({
834
+ session: sessionName,
835
+ state: state.type,
836
+ detail: state.detail,
837
+ elapsedMs: Date.now() - startTime,
838
+ }, null, 2));
839
+ }
840
+ } catch (error: any) {
841
+ console.error(`❌ Error: ${error.message}`);
842
+ process.exit(1);
843
+ }
844
+ }