@monotykamary/pi-supervisor 0.5.9

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 (62) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/LICENSE +21 -0
  3. package/README.md +341 -0
  4. package/media/demo.mp4 +0 -0
  5. package/media/screenshot.png +0 -0
  6. package/package.json +87 -0
  7. package/src/compaction/brief.ts +841 -0
  8. package/src/compaction/build-sections.ts +340 -0
  9. package/src/compaction/causal-keys.ts +138 -0
  10. package/src/compaction/content.ts +68 -0
  11. package/src/compaction/extract/commits.ts +78 -0
  12. package/src/compaction/extract/goals.ts +79 -0
  13. package/src/compaction/extract/preferences.ts +52 -0
  14. package/src/compaction/extract/shared-symbols.ts +376 -0
  15. package/src/compaction/filter-noise.ts +47 -0
  16. package/src/compaction/format.ts +89 -0
  17. package/src/compaction/index.ts +38 -0
  18. package/src/compaction/normalize.ts +73 -0
  19. package/src/compaction/sanitize.ts +5 -0
  20. package/src/compaction/sections.ts +19 -0
  21. package/src/compaction/skill-collapse.ts +35 -0
  22. package/src/compaction/tool-args.ts +14 -0
  23. package/src/compaction/types.ts +26 -0
  24. package/src/core/analyzer.ts +58 -0
  25. package/src/core/index.ts +8 -0
  26. package/src/core/inference.ts +77 -0
  27. package/src/core/prompt-builder.ts +137 -0
  28. package/src/core/prompt-loader.ts +125 -0
  29. package/src/core/reframe.ts +27 -0
  30. package/src/fabric-provider.ts +115 -0
  31. package/src/global-config.ts +65 -0
  32. package/src/index.ts +514 -0
  33. package/src/session/client.ts +46 -0
  34. package/src/session/response-parser.ts +37 -0
  35. package/src/session/supervisor-session.ts +102 -0
  36. package/src/state/manager.ts +133 -0
  37. package/src/state/mid-run-signals.ts +103 -0
  38. package/src/state/patterns.ts +82 -0
  39. package/src/state/reframe.ts +27 -0
  40. package/src/subagent-detector.ts +94 -0
  41. package/src/types.ts +42 -0
  42. package/src/ui/animations.ts +95 -0
  43. package/src/ui/model-picker.ts +72 -0
  44. package/src/ui/model-settings-selector.ts +440 -0
  45. package/src/ui/model-sort.ts +101 -0
  46. package/src/ui/renderer.ts +314 -0
  47. package/src/ui/types.ts +48 -0
  48. package/tests/compaction.test.ts +507 -0
  49. package/tests/engine.test.ts +622 -0
  50. package/tests/ephemeral-supervision.test.ts +347 -0
  51. package/tests/fabric-provider.test.ts +55 -0
  52. package/tests/full-fidelity-snapshot.test.ts +250 -0
  53. package/tests/global-config.test.ts +74 -0
  54. package/tests/model-sort.test.ts +157 -0
  55. package/tests/parsing.test.ts +303 -0
  56. package/tests/state.test.ts +474 -0
  57. package/tests/status-widget.test.ts +539 -0
  58. package/tests/subagent-detector.test.ts +191 -0
  59. package/tests/supervise-command.test.ts +363 -0
  60. package/tests/supervise-model-command.test.ts +184 -0
  61. package/tsconfig.json +14 -0
  62. package/vitest.config.ts +15 -0
@@ -0,0 +1,474 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { SupervisorStateManager } from '../src/state/manager.js';
3
+ import { detectMidRunSignals } from '../src/state/mid-run-signals.js';
4
+ import type { Message } from '@earendil-works/pi-ai';
5
+
6
+ function createMockApi() {
7
+ return {
8
+ appendEntry: vi.fn(),
9
+ on: vi.fn(),
10
+ registerCommand: vi.fn(),
11
+ registerTool: vi.fn(),
12
+ sendUserMessage: vi.fn(),
13
+ sendMessage: vi.fn(),
14
+ events: { emit: vi.fn(), on: vi.fn() },
15
+ } as any;
16
+ }
17
+
18
+ function makeUserMessage(text: string): Message {
19
+ return { role: 'user', content: text } as Message;
20
+ }
21
+
22
+ function makeAssistantMessage(text: string): Message {
23
+ return {
24
+ role: 'assistant',
25
+ content: [{ type: 'text', text }],
26
+ } as unknown as Message;
27
+ }
28
+
29
+ function makeToolCallMessage(name: string, args: Record<string, unknown> = {}): Message {
30
+ return {
31
+ role: 'assistant',
32
+ content: [{ type: 'toolCall', name, arguments: args }],
33
+ } as unknown as Message;
34
+ }
35
+
36
+ function makeToolResultMessage(name: string, text: string, isError = false): Message {
37
+ return {
38
+ role: 'toolResult',
39
+ toolName: name,
40
+ content: text,
41
+ isError,
42
+ } as unknown as Message;
43
+ }
44
+
45
+ describe('SupervisorStateManager', () => {
46
+ describe('reframe tier management', () => {
47
+ it('initializes reframe tier to 0 on start', () => {
48
+ const api = createMockApi();
49
+ const state = new SupervisorStateManager(api);
50
+ state.start('Test goal', 'anthropic', 'claude-haiku');
51
+
52
+ expect(state.getReframeTier()).toBe(0);
53
+ expect(state.getState()!.reframeTier).toBe(0);
54
+ });
55
+
56
+ it('escalates reframe tier up to max of 4', () => {
57
+ const api = createMockApi();
58
+ const state = new SupervisorStateManager(api);
59
+ state.start('Test goal', 'anthropic', 'claude-haiku');
60
+
61
+ state.escalateReframeTier();
62
+ expect(state.getReframeTier()).toBe(1);
63
+
64
+ state.escalateReframeTier();
65
+ expect(state.getReframeTier()).toBe(2);
66
+
67
+ state.escalateReframeTier();
68
+ expect(state.getReframeTier()).toBe(3);
69
+
70
+ state.escalateReframeTier();
71
+ expect(state.getReframeTier()).toBe(4);
72
+
73
+ state.escalateReframeTier();
74
+ expect(state.getReframeTier()).toBe(4);
75
+ });
76
+
77
+ it('resets reframe tier to 0', () => {
78
+ const api = createMockApi();
79
+ const state = new SupervisorStateManager(api);
80
+ state.start('Test goal', 'anthropic', 'claude-haiku');
81
+
82
+ state.escalateReframeTier();
83
+ state.escalateReframeTier();
84
+ state.resetReframeTier();
85
+ expect(state.getReframeTier()).toBe(0);
86
+ });
87
+
88
+ it('persists reframe tier changes', () => {
89
+ const api = createMockApi();
90
+ const state = new SupervisorStateManager(api);
91
+ state.start('Test goal', 'anthropic', 'claude-haiku');
92
+
93
+ state.escalateReframeTier();
94
+
95
+ expect(api.appendEntry).toHaveBeenLastCalledWith(
96
+ 'supervisor-state',
97
+ expect.objectContaining({ reframeTier: 1 })
98
+ );
99
+ });
100
+ });
101
+
102
+ describe('ineffective pattern detection', () => {
103
+ it('returns no pattern with less than 2 interventions', () => {
104
+ const api = createMockApi();
105
+ const state = new SupervisorStateManager(api);
106
+ state.start('Test goal', 'anthropic', 'claude-haiku');
107
+
108
+ const pattern = state.detectIneffectivePattern();
109
+ expect(pattern.detected).toBe(false);
110
+ });
111
+
112
+ it('detects similar messages', () => {
113
+ const api = createMockApi();
114
+ const state = new SupervisorStateManager(api);
115
+ state.start('Test goal', 'anthropic', 'claude-haiku');
116
+
117
+ state.addIntervention({
118
+ message: 'Please implement the auth middleware',
119
+ reasoning: 'Not done yet',
120
+ timestamp: Date.now(),
121
+ });
122
+
123
+ state.addIntervention({
124
+ message: 'Please implement the auth middleware now',
125
+ reasoning: 'Still not done',
126
+ timestamp: Date.now(),
127
+ });
128
+
129
+ const pattern = state.detectIneffectivePattern();
130
+ expect(pattern.detected).toBe(true);
131
+ expect(pattern.similarCount).toBe(2);
132
+ });
133
+
134
+ it('detects stagnation (no steer in a while)', () => {
135
+ const api = createMockApi();
136
+ const state = new SupervisorStateManager(api);
137
+ state.start('Test goal', 'anthropic', 'claude-haiku');
138
+
139
+ state.addIntervention({
140
+ message: 'Focus on X',
141
+ reasoning: 'Test',
142
+ timestamp: Date.now() - 120_000,
143
+ });
144
+
145
+ const pattern = state.detectIneffectivePattern();
146
+ expect(pattern.detected).toBe(true);
147
+ expect(pattern.secondsSinceLastSteer).toBeGreaterThanOrEqual(60);
148
+ });
149
+
150
+ it('detects dissimilar messages as different', () => {
151
+ const api = createMockApi();
152
+ const state = new SupervisorStateManager(api);
153
+ state.start('Test goal', 'anthropic', 'claude-haiku');
154
+
155
+ state.addIntervention({
156
+ message: 'Implement the database layer',
157
+ reasoning: 'Need DB',
158
+ timestamp: Date.now(),
159
+ });
160
+
161
+ state.addIntervention({
162
+ message: 'Now create the API endpoints',
163
+ reasoning: 'Need API',
164
+ timestamp: Date.now(),
165
+ });
166
+
167
+ const pattern = state.detectIneffectivePattern();
168
+ expect(pattern.detected).toBe(false);
169
+ });
170
+ });
171
+
172
+ describe('basic lifecycle', () => {
173
+ it('starts inactive', () => {
174
+ const api = createMockApi();
175
+ const state = new SupervisorStateManager(api);
176
+ expect(state.isActive()).toBe(false);
177
+ expect(state.getState()).toBeNull();
178
+ });
179
+
180
+ it('starts supervision with correct initial state', () => {
181
+ const api = createMockApi();
182
+ const state = new SupervisorStateManager(api);
183
+
184
+ state.start('Test goal', 'anthropic', 'claude-haiku');
185
+
186
+ const s = state.getState()!;
187
+ expect(s.active).toBe(true);
188
+ expect(s.outcome).toBe('Test goal');
189
+ expect(s.provider).toBe('anthropic');
190
+ expect(s.modelId).toBe('claude-haiku');
191
+ expect(s.interventions).toEqual([]);
192
+ expect(s.idleSteers).toBe(0);
193
+ });
194
+
195
+ it('stops supervision and marks inactive', () => {
196
+ const api = createMockApi();
197
+ const state = new SupervisorStateManager(api);
198
+
199
+ state.start('Test goal', 'anthropic', 'claude-haiku');
200
+ state.stop();
201
+ expect(state.isActive()).toBe(false);
202
+ expect(state.getState()!.active).toBe(false);
203
+ });
204
+ });
205
+
206
+ describe('interventions', () => {
207
+ it('adds intervention', () => {
208
+ const api = createMockApi();
209
+ const state = new SupervisorStateManager(api);
210
+ state.start('Test goal', 'anthropic', 'claude-haiku');
211
+
212
+ state.addIntervention({
213
+ message: 'Please focus on X',
214
+ reasoning: 'Agent drifted',
215
+ timestamp: Date.now(),
216
+ });
217
+
218
+ const s = state.getState()!;
219
+ expect(s.interventions).toHaveLength(1);
220
+ });
221
+
222
+ it('does not add intervention when not active', () => {
223
+ const api = createMockApi();
224
+ const state = new SupervisorStateManager(api);
225
+
226
+ state.addIntervention({ message: 'Steer', reasoning: 'Test', timestamp: Date.now() });
227
+ expect(state.getState()).toBeNull();
228
+ });
229
+ });
230
+
231
+ describe('persistence', () => {
232
+ it('persists state data', () => {
233
+ const api = createMockApi();
234
+ const state = new SupervisorStateManager(api);
235
+ state.start('Test goal', 'anthropic', 'claude-haiku');
236
+
237
+ state.addIntervention({ message: 'Steer', reasoning: 'Test', timestamp: Date.now() });
238
+
239
+ const lastCall = api.appendEntry.mock.calls[api.appendEntry.mock.calls.length - 1];
240
+ const persistedData = lastCall[1];
241
+ expect(persistedData.outcome).toBe('Test goal');
242
+ });
243
+ });
244
+
245
+ describe('idle steers', () => {
246
+ it('tracks idle steers count', () => {
247
+ const api = createMockApi();
248
+ const state = new SupervisorStateManager(api);
249
+ state.start('Test goal', 'anthropic', 'claude-haiku');
250
+
251
+ expect(state.getIdleSteers()).toBe(0);
252
+ state.incrementIdleSteers();
253
+ expect(state.getIdleSteers()).toBe(1);
254
+ state.incrementIdleSteers();
255
+ expect(state.getIdleSteers()).toBe(2);
256
+ });
257
+
258
+ it('resets idle steers', () => {
259
+ const api = createMockApi();
260
+ const state = new SupervisorStateManager(api);
261
+ state.start('Test goal', 'anthropic', 'claude-haiku');
262
+
263
+ state.incrementIdleSteers();
264
+ state.incrementIdleSteers();
265
+ state.resetIdleSteers();
266
+ expect(state.getIdleSteers()).toBe(0);
267
+ });
268
+ });
269
+ });
270
+
271
+ describe('detectMidRunSignals', () => {
272
+ it('returns null for empty messages', () => {
273
+ const signal = detectMidRunSignals([]);
274
+ expect(signal).toBeNull();
275
+ });
276
+
277
+ it('returns null for normal conversation with no signals', () => {
278
+ const messages: Message[] = [
279
+ makeUserMessage('Fix the bug'),
280
+ makeAssistantMessage('Let me check the code'),
281
+ makeToolCallMessage('Read', { file_path: 'src/auth.ts' }),
282
+ makeToolResultMessage('Read', 'export function login() {}'),
283
+ makeAssistantMessage('I see the issue'),
284
+ makeToolCallMessage('Edit', { file_path: 'src/auth.ts' }),
285
+ makeToolResultMessage('Edit', 'ok'),
286
+ ];
287
+
288
+ const signal = detectMidRunSignals(messages);
289
+ expect(signal).toBeNull();
290
+ });
291
+
292
+ it('does not trigger on a single tool error', () => {
293
+ const messages: Message[] = [
294
+ makeToolCallMessage('bash', { command: 'npm test' }),
295
+ makeToolResultMessage('bash', 'Command failed with exit code 1', true),
296
+ ];
297
+
298
+ const signal = detectMidRunSignals(messages);
299
+ expect(signal).toBeNull();
300
+ });
301
+
302
+ it('does not trigger on two consecutive tool errors', () => {
303
+ const messages: Message[] = [
304
+ makeToolCallMessage('Edit', { file_path: 'src/auth.ts' }),
305
+ makeToolResultMessage('Edit', 'file not found', true),
306
+ makeToolCallMessage('Edit', { file_path: 'src/auth.ts' }),
307
+ makeToolResultMessage('Edit', 'file not found', true),
308
+ ];
309
+
310
+ const signal = detectMidRunSignals(messages);
311
+ expect(signal).toBeNull();
312
+ });
313
+
314
+ it('does not trigger on four consecutive tool errors', () => {
315
+ const messages: Message[] = [
316
+ makeToolCallMessage('Edit', { file_path: 'src/auth.ts' }),
317
+ makeToolResultMessage('Edit', 'file not found', true),
318
+ makeToolCallMessage('Edit', { file_path: 'src/auth.ts' }),
319
+ makeToolResultMessage('Edit', 'file not found', true),
320
+ makeToolCallMessage('Edit', { file_path: 'src/auth.ts' }),
321
+ makeToolResultMessage('Edit', 'file not found', true),
322
+ makeToolCallMessage('Edit', { file_path: 'src/auth.ts' }),
323
+ makeToolResultMessage('Edit', 'file not found', true),
324
+ ];
325
+
326
+ const signal = detectMidRunSignals(messages);
327
+ expect(signal).toBeNull();
328
+ });
329
+
330
+ it('detects five consecutive tool errors', () => {
331
+ const messages: Message[] = [
332
+ makeToolCallMessage('Edit', { file_path: 'src/auth.ts' }),
333
+ makeToolResultMessage('Edit', 'file not found', true),
334
+ makeToolCallMessage('Edit', { file_path: 'src/auth.ts' }),
335
+ makeToolResultMessage('Edit', 'file not found', true),
336
+ makeToolCallMessage('Edit', { file_path: 'src/auth.ts' }),
337
+ makeToolResultMessage('Edit', 'file not found', true),
338
+ makeToolCallMessage('Edit', { file_path: 'src/auth.ts' }),
339
+ makeToolResultMessage('Edit', 'file not found', true),
340
+ makeToolCallMessage('Edit', { file_path: 'src/auth.ts' }),
341
+ makeToolResultMessage('Edit', 'file not found', true),
342
+ ];
343
+
344
+ const signal = detectMidRunSignals(messages);
345
+ expect(signal).not.toBeNull();
346
+ expect(signal!.type).toBe('tool_error');
347
+ });
348
+
349
+ it('does not trigger when a successful result breaks the error streak', () => {
350
+ const messages: Message[] = [
351
+ makeToolCallMessage('Edit', { file_path: 'src/auth.ts' }),
352
+ makeToolResultMessage('Edit', 'file not found', true),
353
+ makeToolCallMessage('Edit', { file_path: 'src/auth.ts' }),
354
+ makeToolResultMessage('Edit', 'file not found', true),
355
+ makeToolCallMessage('Edit', { file_path: 'src/auth.ts' }),
356
+ makeToolResultMessage('Edit', 'file not found', true),
357
+ makeToolCallMessage('Edit', { file_path: 'src/auth.ts' }),
358
+ makeToolResultMessage('Edit', 'file not found', true),
359
+ // Successful read breaks the streak
360
+ makeToolCallMessage('Read', { file_path: 'src/auth.ts' }),
361
+ makeToolResultMessage('Read', 'file content'),
362
+ makeToolCallMessage('Edit', { file_path: 'src/auth.ts' }),
363
+ makeToolResultMessage('Edit', 'failed again', true),
364
+ ];
365
+
366
+ // Only 1 consecutive error at the tail — the successful Read broke the streak
367
+ const signal = detectMidRunSignals(messages);
368
+ expect(signal).toBeNull();
369
+ });
370
+
371
+ it('detects file read loop', () => {
372
+ const messages: Message[] = [];
373
+ for (let i = 0; i < 5; i++) {
374
+ messages.push(makeToolCallMessage('Read', { file_path: 'src/auth.ts' }));
375
+ messages.push(makeToolResultMessage('Read', 'content'));
376
+ }
377
+
378
+ const signal = detectMidRunSignals(messages);
379
+ expect(signal).not.toBeNull();
380
+ expect(signal!.type).toBe('file_read_loop');
381
+ expect(signal!.detail).toContain('src/auth.ts');
382
+ });
383
+
384
+ it('does not trigger file read loop at 4 reads', () => {
385
+ const messages: Message[] = [];
386
+ for (let i = 0; i < 4; i++) {
387
+ messages.push(makeToolCallMessage('Read', { file_path: 'src/auth.ts' }));
388
+ messages.push(makeToolResultMessage('Read', 'content'));
389
+ }
390
+
391
+ const signal = detectMidRunSignals(messages);
392
+ expect(signal).toBeNull();
393
+ });
394
+
395
+ it('resets read loop counter when file is edited', () => {
396
+ const messages: Message[] = [];
397
+ for (let i = 0; i < 4; i++) {
398
+ messages.push(makeToolCallMessage('Read', { file_path: 'src/auth.ts' }));
399
+ messages.push(makeToolResultMessage('Read', 'content'));
400
+ }
401
+ // Edit resets the counter for this file
402
+ messages.push(makeToolCallMessage('Edit', { file_path: 'src/auth.ts' }));
403
+ messages.push(makeToolResultMessage('Edit', 'ok'));
404
+ // More reads after edit — counter restarts from 0
405
+ for (let i = 0; i < 4; i++) {
406
+ messages.push(makeToolCallMessage('Read', { file_path: 'src/auth.ts' }));
407
+ messages.push(makeToolResultMessage('Read', 'content'));
408
+ }
409
+
410
+ const signal = detectMidRunSignals(messages);
411
+ expect(signal).toBeNull();
412
+ });
413
+
414
+ it('does not trigger file read loop when reading same file with different offsets', () => {
415
+ const messages: Message[] = [];
416
+ // Read same file 5 times but each time with a different offset (pagination)
417
+ for (let i = 0; i < 5; i++) {
418
+ messages.push(
419
+ makeToolCallMessage('Read', { file_path: 'src/auth.ts', offset: i * 50 + 1, limit: 50 })
420
+ );
421
+ messages.push(makeToolResultMessage('Read', 'content'));
422
+ }
423
+
424
+ const signal = detectMidRunSignals(messages);
425
+ expect(signal).toBeNull();
426
+ });
427
+
428
+ it('detects file read loop when reading same file with same offset repeatedly', () => {
429
+ const messages: Message[] = [];
430
+ // Read same file 5 times with the same offset — actual loop
431
+ for (let i = 0; i < 5; i++) {
432
+ messages.push(
433
+ makeToolCallMessage('Read', { file_path: 'src/auth.ts', offset: 1, limit: 50 })
434
+ );
435
+ messages.push(makeToolResultMessage('Read', 'content'));
436
+ }
437
+
438
+ const signal = detectMidRunSignals(messages);
439
+ expect(signal).not.toBeNull();
440
+ expect(signal!.type).toBe('file_read_loop');
441
+ expect(signal!.detail).toContain('src/auth.ts');
442
+ });
443
+
444
+ it('detects file read loop when reading same file without offset repeatedly', () => {
445
+ const messages: Message[] = [];
446
+ // Read same file 5 times without any offset — same as before the change
447
+ for (let i = 0; i < 5; i++) {
448
+ messages.push(makeToolCallMessage('Read', { file_path: 'src/auth.ts' }));
449
+ messages.push(makeToolResultMessage('Read', 'content'));
450
+ }
451
+
452
+ const signal = detectMidRunSignals(messages);
453
+ expect(signal).not.toBeNull();
454
+ expect(signal!.type).toBe('file_read_loop');
455
+ expect(signal!.detail).toContain('src/auth.ts');
456
+ });
457
+
458
+ it('prioritizes consecutive tool_error over file_read_loop', () => {
459
+ const messages: Message[] = [];
460
+ for (let i = 0; i < 6; i++) {
461
+ messages.push(makeToolCallMessage('Read', { file_path: 'src/a.ts' }));
462
+ messages.push(makeToolResultMessage('Read', 'content'));
463
+ }
464
+ // 5 consecutive errors at the tail
465
+ for (let i = 0; i < 5; i++) {
466
+ messages.push(makeToolCallMessage('Read', { file_path: 'src/a.ts' }));
467
+ messages.push(makeToolResultMessage('Read', 'error', true));
468
+ }
469
+
470
+ const signal = detectMidRunSignals(messages);
471
+ expect(signal).not.toBeNull();
472
+ expect(signal!.type).toBe('tool_error');
473
+ });
474
+ });