@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,507 @@
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
2
+ import { SupervisorStateManager } from '../src/state/manager.js';
3
+
4
+ // Mock dependencies
5
+ vi.mock('../src/core/analyzer.js', () => ({
6
+ analyze: vi.fn(),
7
+ }));
8
+
9
+ vi.mock('../src/core/prompt-loader.js', () => ({
10
+ loadSystemPrompt: vi.fn().mockReturnValue({ prompt: 'test prompt', source: 'built-in' }),
11
+ }));
12
+
13
+ vi.mock('../src/ui/renderer.js', () => ({
14
+ updateUI: vi.fn(),
15
+ toggleWidget: vi.fn(),
16
+ }));
17
+
18
+ vi.mock('../src/session/client.js', () => ({
19
+ disposeSession: vi.fn(),
20
+ }));
21
+
22
+ import { analyze } from '../src/core/analyzer.js';
23
+ import { updateUI } from '../src/ui/renderer.js';
24
+
25
+ function createMockApi() {
26
+ return {
27
+ appendEntry: vi.fn(),
28
+ on: vi.fn(),
29
+ registerCommand: vi.fn(),
30
+ registerTool: vi.fn(),
31
+ sendUserMessage: vi.fn(),
32
+ sendMessage: vi.fn(),
33
+ events: { emit: vi.fn(), on: vi.fn() },
34
+ } as any;
35
+ }
36
+
37
+ function createMockContext(entries: any[] = [], isIdle = true) {
38
+ return {
39
+ ui: {
40
+ notify: vi.fn(),
41
+ setStatus: vi.fn(),
42
+ setWidget: vi.fn(),
43
+ setWorkingMessage: vi.fn(),
44
+ },
45
+ hasUI: true,
46
+ cwd: '/test',
47
+ sessionManager: {
48
+ getBranch: vi.fn().mockReturnValue(entries),
49
+ },
50
+ modelRegistry: {},
51
+ model: undefined,
52
+ isIdle: vi.fn().mockReturnValue(isIdle),
53
+ abort: vi.fn(),
54
+ hasPendingMessages: vi.fn().mockReturnValue(false),
55
+ shutdown: vi.fn(),
56
+ getContextUsage: vi.fn(),
57
+ compact: vi.fn(),
58
+ getSystemPrompt: vi.fn().mockReturnValue('test'),
59
+ } as any;
60
+ }
61
+
62
+ describe('SupervisorStateManager - compaction survival', () => {
63
+ describe('loadFromSession after compaction', () => {
64
+ it('restores state from custom entry in session', () => {
65
+ const api = createMockApi();
66
+ const state = new SupervisorStateManager(api);
67
+
68
+ const sessionEntries = [
69
+ { type: 'message', message: { role: 'user', content: 'Hello' } },
70
+ {
71
+ type: 'custom',
72
+ customType: 'supervisor-state',
73
+ data: {
74
+ active: true,
75
+ outcome: 'Test goal',
76
+ provider: 'anthropic',
77
+ modelId: 'claude-haiku',
78
+ interventions: [],
79
+ startedAt: Date.now(),
80
+ reframeTier: 2,
81
+ },
82
+ },
83
+ ];
84
+
85
+ const ctx = createMockContext(sessionEntries);
86
+ state.loadFromSession(ctx);
87
+
88
+ expect(state.isActive()).toBe(true);
89
+ expect(state.getState()?.outcome).toBe('Test goal');
90
+ expect(state.getState()?.provider).toBe('anthropic');
91
+ expect(state.getReframeTier()).toBe(2);
92
+ });
93
+
94
+ it('restores null state when no supervisor-state entry exists', () => {
95
+ const api = createMockApi();
96
+ const state = new SupervisorStateManager(api);
97
+
98
+ const sessionEntries = [
99
+ { type: 'message', message: { role: 'user', content: 'Hello' } },
100
+ { type: 'compaction', summary: 'Summary of old messages' },
101
+ ];
102
+
103
+ const ctx = createMockContext(sessionEntries);
104
+ state.loadFromSession(ctx);
105
+
106
+ expect(state.isActive()).toBe(false);
107
+ expect(state.getState()).toBeNull();
108
+ });
109
+
110
+ it('restores ephemeral fields with defaults after compaction', () => {
111
+ const api = createMockApi();
112
+ const state = new SupervisorStateManager(api);
113
+
114
+ const sessionEntries = [
115
+ {
116
+ type: 'custom',
117
+ customType: 'supervisor-state',
118
+ data: {
119
+ active: true,
120
+ outcome: 'Test goal',
121
+ provider: 'anthropic',
122
+ modelId: 'claude-haiku',
123
+ interventions: [{ message: 'Focus', reasoning: 'Test', timestamp: 123 }],
124
+ startedAt: 1000,
125
+ reframeTier: 1,
126
+ },
127
+ },
128
+ ];
129
+
130
+ const ctx = createMockContext(sessionEntries);
131
+ state.loadFromSession(ctx);
132
+
133
+ // Non-ephemeral fields should be preserved
134
+ expect(state.getState()?.reframeTier).toBe(1);
135
+ expect(state.getState()?.interventions).toHaveLength(1);
136
+ });
137
+
138
+ it('uses the most recent supervisor-state entry when multiple exist', () => {
139
+ const api = createMockApi();
140
+ const state = new SupervisorStateManager(api);
141
+
142
+ const sessionEntries = [
143
+ {
144
+ type: 'custom',
145
+ customType: 'supervisor-state',
146
+ data: {
147
+ active: false,
148
+ outcome: 'Old goal',
149
+ provider: 'openai',
150
+ modelId: 'gpt-4o',
151
+ interventions: [],
152
+ startedAt: 1000,
153
+ },
154
+ },
155
+ { type: 'message', message: { role: 'user', content: 'Continue' } },
156
+ {
157
+ type: 'custom',
158
+ customType: 'supervisor-state',
159
+ data: {
160
+ active: true,
161
+ outcome: 'New goal',
162
+ provider: 'anthropic',
163
+ modelId: 'claude-haiku',
164
+ interventions: [],
165
+ startedAt: 2000,
166
+ },
167
+ },
168
+ ];
169
+
170
+ const ctx = createMockContext(sessionEntries);
171
+ state.loadFromSession(ctx);
172
+
173
+ expect(state.isActive()).toBe(true);
174
+ expect(state.getState()?.outcome).toBe('New goal');
175
+ expect(state.getState()?.provider).toBe('anthropic');
176
+ });
177
+ });
178
+
179
+ describe('persist() behavior', () => {
180
+ it('persists active state to session via appendEntry', () => {
181
+ const api = createMockApi();
182
+ const state = new SupervisorStateManager(api);
183
+
184
+ state.start('Test goal', 'anthropic', 'claude-haiku');
185
+
186
+ expect(api.appendEntry).toHaveBeenCalledWith(
187
+ 'supervisor-state',
188
+ expect.objectContaining({
189
+ active: true,
190
+ outcome: 'Test goal',
191
+ provider: 'anthropic',
192
+ modelId: 'claude-haiku',
193
+ })
194
+ );
195
+ });
196
+
197
+ it('persists all state fields', () => {
198
+ const api = createMockApi();
199
+ const state = new SupervisorStateManager(api);
200
+
201
+ state.start('Test goal', 'anthropic', 'claude-haiku');
202
+
203
+ const lastCall = api.appendEntry.mock.calls[api.appendEntry.mock.calls.length - 1];
204
+ const persistedData = lastCall[1];
205
+
206
+ expect(persistedData.outcome).toBe('Test goal');
207
+ expect(persistedData.active).toBe(true);
208
+ });
209
+
210
+ it('can be called manually to re-persist after compaction (public method)', () => {
211
+ const api = createMockApi();
212
+ const state = new SupervisorStateManager(api);
213
+
214
+ state.start('Test goal', 'anthropic', 'claude-haiku');
215
+ expect(api.appendEntry).toHaveBeenCalledTimes(1);
216
+
217
+ state.persist();
218
+ expect(api.appendEntry).toHaveBeenCalledTimes(2);
219
+
220
+ const lastCall = api.appendEntry.mock.calls[api.appendEntry.mock.calls.length - 1];
221
+ expect(lastCall[0]).toBe('supervisor-state');
222
+ expect(lastCall[1]).toMatchObject({
223
+ active: true,
224
+ outcome: 'Test goal',
225
+ provider: 'anthropic',
226
+ modelId: 'claude-haiku',
227
+ });
228
+ });
229
+
230
+ it('does nothing when persist() is called with no active state', () => {
231
+ const api = createMockApi();
232
+ const state = new SupervisorStateManager(api);
233
+
234
+ state.persist();
235
+ expect(api.appendEntry).not.toHaveBeenCalled();
236
+ });
237
+
238
+ it('preserves interventions when re-persisting', () => {
239
+ const api = createMockApi();
240
+ const state = new SupervisorStateManager(api);
241
+
242
+ state.start('Test goal', 'anthropic', 'claude-haiku');
243
+ state.addIntervention({
244
+ message: 'Focus on tests',
245
+ reasoning: 'Drift detected',
246
+ timestamp: 1234567890,
247
+ asi: { why_stuck: 'no tests', strategy_used: 'directive' },
248
+ });
249
+
250
+ api.appendEntry.mockClear();
251
+ state.persist();
252
+
253
+ const lastCall = api.appendEntry.mock.calls[api.appendEntry.mock.calls.length - 1];
254
+ expect(lastCall[1].interventions).toHaveLength(1);
255
+ expect(lastCall[1].interventions[0].message).toBe('Focus on tests');
256
+ expect(lastCall[1].interventions[0].asi).toEqual({
257
+ why_stuck: 'no tests',
258
+ strategy_used: 'directive',
259
+ });
260
+ });
261
+ });
262
+
263
+ describe('compaction survival scenario', () => {
264
+ it('full lifecycle: start -> compact -> reload -> repersist', () => {
265
+ const api = createMockApi();
266
+ const state = new SupervisorStateManager(api);
267
+
268
+ state.start('Implement auth', 'anthropic', 'claude-haiku');
269
+ state.addIntervention({
270
+ message: 'Focus on JWT',
271
+ reasoning: 'Drift',
272
+ timestamp: 1000,
273
+ });
274
+ expect(state.isActive()).toBe(true);
275
+ expect(state.getState()?.interventions).toHaveLength(1);
276
+
277
+ const postCompactionEntries = [
278
+ { type: 'compaction', summary: 'Earlier conversation summarized' },
279
+ { type: 'message', message: { role: 'user', content: 'Continue' } },
280
+ {
281
+ type: 'custom',
282
+ customType: 'supervisor-state',
283
+ data: {
284
+ active: true,
285
+ outcome: 'Implement auth',
286
+ provider: 'anthropic',
287
+ modelId: 'claude-haiku',
288
+ interventions: [{ message: 'Focus on JWT', reasoning: 'Drift', timestamp: 1000 }],
289
+ startedAt: 500,
290
+ reframeTier: 0,
291
+ },
292
+ },
293
+ ];
294
+
295
+ const ctx = createMockContext(postCompactionEntries);
296
+ state.loadFromSession(ctx);
297
+
298
+ expect(state.isActive()).toBe(true);
299
+ expect(state.getState()?.outcome).toBe('Implement auth');
300
+ expect(state.getState()?.interventions).toHaveLength(1);
301
+
302
+ api.appendEntry.mockClear();
303
+ state.persist();
304
+
305
+ expect(api.appendEntry).toHaveBeenCalledWith(
306
+ 'supervisor-state',
307
+ expect.objectContaining({
308
+ active: true,
309
+ outcome: 'Implement auth',
310
+ })
311
+ );
312
+ });
313
+
314
+ it('handles state loss when supervisor-state was summarized away', () => {
315
+ const api = createMockApi();
316
+ const state = new SupervisorStateManager(api);
317
+
318
+ state.start('Lost goal', 'anthropic', 'claude-haiku');
319
+ expect(state.isActive()).toBe(true);
320
+
321
+ const postCompactionEntries = [
322
+ {
323
+ type: 'compaction',
324
+ summary: 'Earlier conversation summarized (including old supervisor-state)',
325
+ },
326
+ { type: 'message', message: { role: 'user', content: 'Recent message' } },
327
+ ];
328
+
329
+ const ctx = createMockContext(postCompactionEntries);
330
+ state.loadFromSession(ctx);
331
+
332
+ expect(state.isActive()).toBe(false);
333
+ expect(state.getState()).toBeNull();
334
+
335
+ api.appendEntry.mockClear();
336
+ state.persist();
337
+ expect(api.appendEntry).not.toHaveBeenCalled();
338
+ });
339
+
340
+ it('recovers from state loss by re-persisting if was active', () => {
341
+ const api = createMockApi();
342
+ const state = new SupervisorStateManager(api);
343
+
344
+ state.start('Goal', 'anthropic', 'claude-haiku');
345
+ expect(state.isActive()).toBe(true);
346
+
347
+ api.appendEntry.mockClear();
348
+ state.persist();
349
+
350
+ expect(api.appendEntry).toHaveBeenCalledWith(
351
+ 'supervisor-state',
352
+ expect.objectContaining({
353
+ active: true,
354
+ outcome: 'Goal',
355
+ })
356
+ );
357
+ });
358
+ });
359
+
360
+ describe('event handler behavior', () => {
361
+ beforeEach(() => {
362
+ vi.clearAllMocks();
363
+ });
364
+
365
+ it('session_before_compact persists state when supervision is active', () => {
366
+ const api = createMockApi();
367
+ const state = new SupervisorStateManager(api);
368
+
369
+ state.start('Test goal', 'anthropic', 'claude-haiku');
370
+ expect(api.appendEntry).toHaveBeenCalledTimes(1);
371
+
372
+ if (state.isActive()) {
373
+ state.persist();
374
+ }
375
+
376
+ expect(api.appendEntry).toHaveBeenCalledTimes(2);
377
+ expect(api.appendEntry).toHaveBeenLastCalledWith(
378
+ 'supervisor-state',
379
+ expect.objectContaining({
380
+ active: true,
381
+ outcome: 'Test goal',
382
+ provider: 'anthropic',
383
+ modelId: 'claude-haiku',
384
+ })
385
+ );
386
+ });
387
+
388
+ it('session_before_compact does nothing when supervision is inactive', () => {
389
+ const api = createMockApi();
390
+ const state = new SupervisorStateManager(api);
391
+
392
+ expect(state.isActive()).toBe(false);
393
+
394
+ if (state.isActive()) {
395
+ state.persist();
396
+ }
397
+
398
+ expect(api.appendEntry).not.toHaveBeenCalled();
399
+ });
400
+
401
+ it('session_compact handler reloads state and updates UI', () => {
402
+ const api = createMockApi();
403
+ const state = new SupervisorStateManager(api);
404
+
405
+ const postCompactionEntries = [
406
+ { type: 'compaction', summary: 'Earlier conversation summarized' },
407
+ {
408
+ type: 'custom',
409
+ customType: 'supervisor-state',
410
+ data: {
411
+ active: true,
412
+ outcome: 'Survived goal',
413
+ provider: 'anthropic',
414
+ modelId: 'claude-haiku',
415
+ interventions: [],
416
+ startedAt: 1000,
417
+ reframeTier: 1,
418
+ },
419
+ },
420
+ ];
421
+
422
+ const ctx = createMockContext(postCompactionEntries, true);
423
+
424
+ state.loadFromSession(ctx);
425
+
426
+ if (state.isActive()) {
427
+ updateUI(ctx, state.getState(), { type: 'watching', reframeTier: state.getReframeTier() });
428
+ } else {
429
+ updateUI(ctx, null);
430
+ }
431
+
432
+ expect(state.isActive()).toBe(true);
433
+ expect(state.getState()?.outcome).toBe('Survived goal');
434
+ expect(updateUI).toHaveBeenCalledWith(
435
+ ctx,
436
+ expect.objectContaining({ outcome: 'Survived goal' }),
437
+ { type: 'watching', reframeTier: 1 }
438
+ );
439
+ });
440
+
441
+ it('session_compact handler clears UI when state is lost', () => {
442
+ const api = createMockApi();
443
+ const state = new SupervisorStateManager(api);
444
+
445
+ const lostStateEntries = [
446
+ {
447
+ type: 'compaction',
448
+ summary: 'Earlier conversation summarized including supervisor-state',
449
+ },
450
+ { type: 'message', message: { role: 'user', content: 'Recent message' } },
451
+ ];
452
+
453
+ const ctx = createMockContext(lostStateEntries, true);
454
+
455
+ state.loadFromSession(ctx);
456
+
457
+ if (state.isActive()) {
458
+ updateUI(ctx, state.getState(), { type: 'watching', reframeTier: state.getReframeTier() });
459
+ } else {
460
+ updateUI(ctx, null);
461
+ }
462
+
463
+ expect(state.isActive()).toBe(false);
464
+ expect(updateUI).toHaveBeenCalledWith(ctx, null);
465
+ });
466
+
467
+ it('session_compact does not steer when agent is busy', async () => {
468
+ const api = createMockApi();
469
+ const state = new SupervisorStateManager(api);
470
+
471
+ const postCompactionEntries = [
472
+ { type: 'compaction', summary: 'Earlier conversation summarized' },
473
+ {
474
+ type: 'custom',
475
+ customType: 'supervisor-state',
476
+ data: {
477
+ active: true,
478
+ outcome: 'Implement feature',
479
+ provider: 'anthropic',
480
+ modelId: 'claude-haiku',
481
+ interventions: [],
482
+ startedAt: 1000,
483
+ reframeTier: 0,
484
+ },
485
+ },
486
+ ];
487
+
488
+ const ctx = createMockContext(postCompactionEntries, false);
489
+
490
+ state.loadFromSession(ctx);
491
+
492
+ if (!state.isActive()) {
493
+ updateUI(ctx, null);
494
+ return;
495
+ }
496
+
497
+ updateUI(ctx, state.getState(), { type: 'watching', reframeTier: state.getReframeTier() });
498
+
499
+ if (ctx.isIdle()) {
500
+ await analyze(ctx, state.getState()!, true);
501
+ }
502
+
503
+ expect(ctx.isIdle).toHaveBeenCalled();
504
+ expect(analyze).not.toHaveBeenCalled();
505
+ });
506
+ });
507
+ });