@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.
- package/dist/claudio.js +5 -5
- package/dist/genie.js +6 -6
- package/dist/term.js +115 -53
- package/package.json +1 -1
- package/src/lib/orchestrator/completion.ts +392 -0
- package/src/lib/orchestrator/event-monitor.ts +442 -0
- package/src/lib/orchestrator/index.ts +12 -0
- package/src/lib/orchestrator/patterns.ts +277 -0
- package/src/lib/orchestrator/state-detector.ts +339 -0
- package/src/lib/version.ts +1 -1
- package/src/lib/worker-registry.ts +229 -0
- package/src/term-commands/close.ts +221 -0
- package/src/term-commands/kill.ts +143 -0
- package/src/term-commands/orchestrate.ts +844 -0
- package/src/term-commands/work.ts +415 -0
- package/src/term-commands/workers.ts +264 -0
- package/src/term.ts +189 -1
package/package.json
CHANGED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Completion detection methods for Claude Code sessions
|
|
3
|
+
*
|
|
4
|
+
* Provides multiple strategies for detecting when Claude Code
|
|
5
|
+
* has finished a task, with metrics for evaluating effectiveness.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EventMonitor, waitForSilence, waitForCompletion } from './event-monitor.js';
|
|
9
|
+
import { ClaudeState, detectState } from './state-detector.js';
|
|
10
|
+
import * as tmux from '../tmux.js';
|
|
11
|
+
|
|
12
|
+
export interface CompletionResult {
|
|
13
|
+
complete: boolean;
|
|
14
|
+
state?: ClaudeState;
|
|
15
|
+
reason: string;
|
|
16
|
+
latencyMs: number;
|
|
17
|
+
method: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CompletionMethodMetrics {
|
|
21
|
+
name: string;
|
|
22
|
+
totalRuns: number;
|
|
23
|
+
avgLatencyMs: number;
|
|
24
|
+
minLatencyMs: number;
|
|
25
|
+
maxLatencyMs: number;
|
|
26
|
+
falsePositives: number;
|
|
27
|
+
falseNegatives: number;
|
|
28
|
+
successRate: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CompletionMethod {
|
|
32
|
+
name: string;
|
|
33
|
+
description: string;
|
|
34
|
+
detect(monitor: EventMonitor, timeoutMs?: number): Promise<CompletionResult>;
|
|
35
|
+
metrics: CompletionMethodMetrics;
|
|
36
|
+
recordResult(latencyMs: number, correct: boolean, falsePositive: boolean): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create a completion method based on silence timeout
|
|
41
|
+
*/
|
|
42
|
+
export function silenceTimeoutMethod(silenceMs: number): CompletionMethod {
|
|
43
|
+
const metrics: CompletionMethodMetrics = {
|
|
44
|
+
name: `silence-${silenceMs}ms`,
|
|
45
|
+
totalRuns: 0,
|
|
46
|
+
avgLatencyMs: 0,
|
|
47
|
+
minLatencyMs: Infinity,
|
|
48
|
+
maxLatencyMs: 0,
|
|
49
|
+
falsePositives: 0,
|
|
50
|
+
falseNegatives: 0,
|
|
51
|
+
successRate: 1,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
name: `silence-${silenceMs}ms`,
|
|
56
|
+
description: `Detect completion when no output for ${silenceMs}ms`,
|
|
57
|
+
metrics,
|
|
58
|
+
|
|
59
|
+
async detect(monitor: EventMonitor, timeoutMs = 120000): Promise<CompletionResult> {
|
|
60
|
+
const startTime = Date.now();
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
await waitForSilence(monitor, silenceMs, timeoutMs);
|
|
64
|
+
const latencyMs = Date.now() - startTime;
|
|
65
|
+
const state = monitor.getCurrentState();
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
complete: true,
|
|
69
|
+
state: state || undefined,
|
|
70
|
+
reason: `silence for ${silenceMs}ms`,
|
|
71
|
+
latencyMs,
|
|
72
|
+
method: this.name,
|
|
73
|
+
};
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
complete: false,
|
|
77
|
+
reason: error instanceof Error ? error.message : 'unknown error',
|
|
78
|
+
latencyMs: Date.now() - startTime,
|
|
79
|
+
method: this.name,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
recordResult(latencyMs: number, correct: boolean, falsePositive: boolean): void {
|
|
85
|
+
metrics.totalRuns++;
|
|
86
|
+
metrics.avgLatencyMs =
|
|
87
|
+
(metrics.avgLatencyMs * (metrics.totalRuns - 1) + latencyMs) / metrics.totalRuns;
|
|
88
|
+
metrics.minLatencyMs = Math.min(metrics.minLatencyMs, latencyMs);
|
|
89
|
+
metrics.maxLatencyMs = Math.max(metrics.maxLatencyMs, latencyMs);
|
|
90
|
+
|
|
91
|
+
if (!correct) {
|
|
92
|
+
if (falsePositive) {
|
|
93
|
+
metrics.falsePositives++;
|
|
94
|
+
} else {
|
|
95
|
+
metrics.falseNegatives++;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
metrics.successRate =
|
|
100
|
+
(metrics.totalRuns - metrics.falsePositives - metrics.falseNegatives) /
|
|
101
|
+
metrics.totalRuns;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create a completion method based on state detection (idle state)
|
|
108
|
+
*/
|
|
109
|
+
export function stateDetectionMethod(): CompletionMethod {
|
|
110
|
+
const metrics: CompletionMethodMetrics = {
|
|
111
|
+
name: 'state-detection',
|
|
112
|
+
totalRuns: 0,
|
|
113
|
+
avgLatencyMs: 0,
|
|
114
|
+
minLatencyMs: Infinity,
|
|
115
|
+
maxLatencyMs: 0,
|
|
116
|
+
falsePositives: 0,
|
|
117
|
+
falseNegatives: 0,
|
|
118
|
+
successRate: 1,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
name: 'state-detection',
|
|
123
|
+
description: 'Detect completion when idle state is detected',
|
|
124
|
+
metrics,
|
|
125
|
+
|
|
126
|
+
async detect(monitor: EventMonitor, timeoutMs = 120000): Promise<CompletionResult> {
|
|
127
|
+
const startTime = Date.now();
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const result = await waitForCompletion(monitor, {
|
|
131
|
+
timeoutMs,
|
|
132
|
+
requireIdle: true,
|
|
133
|
+
silenceMs: 2000,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const latencyMs = Date.now() - startTime;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
complete: true,
|
|
140
|
+
state: result.state,
|
|
141
|
+
reason: result.reason,
|
|
142
|
+
latencyMs,
|
|
143
|
+
method: this.name,
|
|
144
|
+
};
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return {
|
|
147
|
+
complete: false,
|
|
148
|
+
reason: error instanceof Error ? error.message : 'unknown error',
|
|
149
|
+
latencyMs: Date.now() - startTime,
|
|
150
|
+
method: this.name,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
recordResult(latencyMs: number, correct: boolean, falsePositive: boolean): void {
|
|
156
|
+
metrics.totalRuns++;
|
|
157
|
+
metrics.avgLatencyMs =
|
|
158
|
+
(metrics.avgLatencyMs * (metrics.totalRuns - 1) + latencyMs) / metrics.totalRuns;
|
|
159
|
+
metrics.minLatencyMs = Math.min(metrics.minLatencyMs, latencyMs);
|
|
160
|
+
metrics.maxLatencyMs = Math.max(metrics.maxLatencyMs, latencyMs);
|
|
161
|
+
|
|
162
|
+
if (!correct) {
|
|
163
|
+
if (falsePositive) {
|
|
164
|
+
metrics.falsePositives++;
|
|
165
|
+
} else {
|
|
166
|
+
metrics.falseNegatives++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
metrics.successRate =
|
|
171
|
+
(metrics.totalRuns - metrics.falsePositives - metrics.falseNegatives) /
|
|
172
|
+
metrics.totalRuns;
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create a completion method using tmux wait-for channel
|
|
179
|
+
*/
|
|
180
|
+
export function waitForChannelMethod(channel: string): CompletionMethod {
|
|
181
|
+
const metrics: CompletionMethodMetrics = {
|
|
182
|
+
name: `wait-for-${channel}`,
|
|
183
|
+
totalRuns: 0,
|
|
184
|
+
avgLatencyMs: 0,
|
|
185
|
+
minLatencyMs: Infinity,
|
|
186
|
+
maxLatencyMs: 0,
|
|
187
|
+
falsePositives: 0,
|
|
188
|
+
falseNegatives: 0,
|
|
189
|
+
successRate: 1,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
name: `wait-for-${channel}`,
|
|
194
|
+
description: `Wait for tmux wait-for signal on channel "${channel}"`,
|
|
195
|
+
metrics,
|
|
196
|
+
|
|
197
|
+
async detect(_monitor: EventMonitor, timeoutMs = 120000): Promise<CompletionResult> {
|
|
198
|
+
const startTime = Date.now();
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
await Promise.race([
|
|
202
|
+
tmux.executeTmux(`wait-for ${channel}`),
|
|
203
|
+
new Promise((_, reject) =>
|
|
204
|
+
setTimeout(() => reject(new Error('Timeout')), timeoutMs)
|
|
205
|
+
),
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
const latencyMs = Date.now() - startTime;
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
complete: true,
|
|
212
|
+
reason: `signal received on channel "${channel}"`,
|
|
213
|
+
latencyMs,
|
|
214
|
+
method: this.name,
|
|
215
|
+
};
|
|
216
|
+
} catch (error) {
|
|
217
|
+
return {
|
|
218
|
+
complete: false,
|
|
219
|
+
reason: error instanceof Error ? error.message : 'unknown error',
|
|
220
|
+
latencyMs: Date.now() - startTime,
|
|
221
|
+
method: this.name,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
recordResult(latencyMs: number, correct: boolean, falsePositive: boolean): void {
|
|
227
|
+
metrics.totalRuns++;
|
|
228
|
+
metrics.avgLatencyMs =
|
|
229
|
+
(metrics.avgLatencyMs * (metrics.totalRuns - 1) + latencyMs) / metrics.totalRuns;
|
|
230
|
+
metrics.minLatencyMs = Math.min(metrics.minLatencyMs, latencyMs);
|
|
231
|
+
metrics.maxLatencyMs = Math.max(metrics.maxLatencyMs, latencyMs);
|
|
232
|
+
|
|
233
|
+
if (!correct) {
|
|
234
|
+
if (falsePositive) {
|
|
235
|
+
metrics.falsePositives++;
|
|
236
|
+
} else {
|
|
237
|
+
metrics.falseNegatives++;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
metrics.successRate =
|
|
242
|
+
(metrics.totalRuns - metrics.falsePositives - metrics.falseNegatives) /
|
|
243
|
+
metrics.totalRuns;
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Create a hybrid completion method that uses multiple strategies
|
|
250
|
+
*/
|
|
251
|
+
export function hybridMethod(
|
|
252
|
+
primaryMethod: CompletionMethod,
|
|
253
|
+
fallbackMethod: CompletionMethod,
|
|
254
|
+
options: {
|
|
255
|
+
primaryTimeoutMs?: number;
|
|
256
|
+
fallbackTimeoutMs?: number;
|
|
257
|
+
} = {}
|
|
258
|
+
): CompletionMethod {
|
|
259
|
+
const { primaryTimeoutMs = 30000, fallbackTimeoutMs = 90000 } = options;
|
|
260
|
+
|
|
261
|
+
const metrics: CompletionMethodMetrics = {
|
|
262
|
+
name: `hybrid(${primaryMethod.name},${fallbackMethod.name})`,
|
|
263
|
+
totalRuns: 0,
|
|
264
|
+
avgLatencyMs: 0,
|
|
265
|
+
minLatencyMs: Infinity,
|
|
266
|
+
maxLatencyMs: 0,
|
|
267
|
+
falsePositives: 0,
|
|
268
|
+
falseNegatives: 0,
|
|
269
|
+
successRate: 1,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
name: metrics.name,
|
|
274
|
+
description: `Try ${primaryMethod.name} first, fall back to ${fallbackMethod.name}`,
|
|
275
|
+
metrics,
|
|
276
|
+
|
|
277
|
+
async detect(monitor: EventMonitor, timeoutMs = 120000): Promise<CompletionResult> {
|
|
278
|
+
const startTime = Date.now();
|
|
279
|
+
|
|
280
|
+
// Try primary method first
|
|
281
|
+
const primaryResult = await primaryMethod.detect(
|
|
282
|
+
monitor,
|
|
283
|
+
Math.min(primaryTimeoutMs, timeoutMs)
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
if (primaryResult.complete) {
|
|
287
|
+
return {
|
|
288
|
+
...primaryResult,
|
|
289
|
+
method: this.name,
|
|
290
|
+
reason: `primary(${primaryResult.reason})`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Fall back to secondary method
|
|
295
|
+
const remainingTime = timeoutMs - (Date.now() - startTime);
|
|
296
|
+
if (remainingTime <= 0) {
|
|
297
|
+
return {
|
|
298
|
+
complete: false,
|
|
299
|
+
reason: 'timeout after primary method',
|
|
300
|
+
latencyMs: Date.now() - startTime,
|
|
301
|
+
method: this.name,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const fallbackResult = await fallbackMethod.detect(
|
|
306
|
+
monitor,
|
|
307
|
+
Math.min(fallbackTimeoutMs, remainingTime)
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
...fallbackResult,
|
|
312
|
+
method: this.name,
|
|
313
|
+
reason: `fallback(${fallbackResult.reason})`,
|
|
314
|
+
latencyMs: Date.now() - startTime,
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
recordResult(latencyMs: number, correct: boolean, falsePositive: boolean): void {
|
|
319
|
+
metrics.totalRuns++;
|
|
320
|
+
metrics.avgLatencyMs =
|
|
321
|
+
(metrics.avgLatencyMs * (metrics.totalRuns - 1) + latencyMs) / metrics.totalRuns;
|
|
322
|
+
metrics.minLatencyMs = Math.min(metrics.minLatencyMs, latencyMs);
|
|
323
|
+
metrics.maxLatencyMs = Math.max(metrics.maxLatencyMs, latencyMs);
|
|
324
|
+
|
|
325
|
+
if (!correct) {
|
|
326
|
+
if (falsePositive) {
|
|
327
|
+
metrics.falsePositives++;
|
|
328
|
+
} else {
|
|
329
|
+
metrics.falseNegatives++;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
metrics.successRate =
|
|
334
|
+
(metrics.totalRuns - metrics.falsePositives - metrics.falseNegatives) /
|
|
335
|
+
metrics.totalRuns;
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Get the default completion method (hybrid of state detection + silence)
|
|
342
|
+
*/
|
|
343
|
+
export function getDefaultMethod(): CompletionMethod {
|
|
344
|
+
return hybridMethod(stateDetectionMethod(), silenceTimeoutMethod(5000), {
|
|
345
|
+
primaryTimeoutMs: 30000,
|
|
346
|
+
fallbackTimeoutMs: 90000,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Available preset methods
|
|
352
|
+
*/
|
|
353
|
+
export const presetMethods = {
|
|
354
|
+
'silence-3s': () => silenceTimeoutMethod(3000),
|
|
355
|
+
'silence-5s': () => silenceTimeoutMethod(5000),
|
|
356
|
+
'silence-10s': () => silenceTimeoutMethod(10000),
|
|
357
|
+
'state-detection': () => stateDetectionMethod(),
|
|
358
|
+
hybrid: () => getDefaultMethod(),
|
|
359
|
+
'aggressive-hybrid': () =>
|
|
360
|
+
hybridMethod(stateDetectionMethod(), silenceTimeoutMethod(2000), {
|
|
361
|
+
primaryTimeoutMs: 10000,
|
|
362
|
+
fallbackTimeoutMs: 30000,
|
|
363
|
+
}),
|
|
364
|
+
'conservative-hybrid': () =>
|
|
365
|
+
hybridMethod(stateDetectionMethod(), silenceTimeoutMethod(10000), {
|
|
366
|
+
primaryTimeoutMs: 60000,
|
|
367
|
+
fallbackTimeoutMs: 120000,
|
|
368
|
+
}),
|
|
369
|
+
} as const;
|
|
370
|
+
|
|
371
|
+
export type PresetMethodName = keyof typeof presetMethods;
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Get a completion method by name
|
|
375
|
+
*/
|
|
376
|
+
export function getMethod(name: string): CompletionMethod {
|
|
377
|
+
if (name in presetMethods) {
|
|
378
|
+
return presetMethods[name as PresetMethodName]();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Parse custom silence timeout: "silence-Xms" or "silence-Xs"
|
|
382
|
+
const silenceMatch = name.match(/^silence-(\d+)(ms|s)?$/);
|
|
383
|
+
if (silenceMatch) {
|
|
384
|
+
const value = parseInt(silenceMatch[1], 10);
|
|
385
|
+
const unit = silenceMatch[2] || 'ms';
|
|
386
|
+
const ms = unit === 's' ? value * 1000 : value;
|
|
387
|
+
return silenceTimeoutMethod(ms);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Default to hybrid
|
|
391
|
+
return getDefaultMethod();
|
|
392
|
+
}
|