@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,191 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { exec } from 'node:child_process';
3
+ import { checkChildPiProcesses, waitForSubagents } from '../src/subagent-detector.js';
4
+
5
+ // Mock child_process
6
+ vi.mock('node:child_process', () => ({
7
+ exec: vi.fn(),
8
+ }));
9
+
10
+ describe('subagent-detector', () => {
11
+ const mockExec = vi.mocked(exec);
12
+
13
+ beforeEach(() => {
14
+ vi.clearAllMocks();
15
+ });
16
+
17
+ afterEach(() => {
18
+ vi.restoreAllMocks();
19
+ });
20
+
21
+ describe('checkChildPiProcesses', () => {
22
+ it('returns no subagents when ps output is empty', async () => {
23
+ mockExec.mockImplementation((cmd, callback) => {
24
+ callback(null, { stdout: '', stderr: '' }, null as any);
25
+ return {} as any;
26
+ });
27
+
28
+ const result = await checkChildPiProcesses();
29
+
30
+ expect(result.hasActiveSubagents).toBe(false);
31
+ expect(result.count).toBe(0);
32
+ expect(result.pids).toEqual([]);
33
+ });
34
+
35
+ it('detects child pi processes', async () => {
36
+ const parentPid = process.pid;
37
+ mockExec.mockImplementation((cmd, callback) => {
38
+ callback(
39
+ null,
40
+ {
41
+ stdout: `${parentPid} 12345 pi\n${parentPid} 12346 pi\n99999 12347 other`,
42
+ stderr: '',
43
+ },
44
+ null as any
45
+ );
46
+ return {} as any;
47
+ });
48
+
49
+ const result = await checkChildPiProcesses();
50
+
51
+ expect(result.hasActiveSubagents).toBe(true);
52
+ expect(result.count).toBe(2);
53
+ expect(result.pids).toContain(12345);
54
+ expect(result.pids).toContain(12346);
55
+ expect(result.pids).not.toContain(12347);
56
+ });
57
+
58
+ it('ignores non-pi processes', async () => {
59
+ const parentPid = process.pid;
60
+ mockExec.mockImplementation((cmd, callback) => {
61
+ callback(
62
+ null,
63
+ {
64
+ stdout: `${parentPid} 12345 node\n${parentPid} 12346 bash`,
65
+ stderr: '',
66
+ },
67
+ null as any
68
+ );
69
+ return {} as any;
70
+ });
71
+
72
+ const result = await checkChildPiProcesses();
73
+
74
+ expect(result.hasActiveSubagents).toBe(false);
75
+ expect(result.count).toBe(0);
76
+ });
77
+
78
+ it('ignores pi processes from other parents', async () => {
79
+ mockExec.mockImplementation((cmd, callback) => {
80
+ callback(
81
+ null,
82
+ {
83
+ stdout: '99999 12345 pi\n99999 12346 pi',
84
+ stderr: '',
85
+ },
86
+ null as any
87
+ );
88
+ return {} as any;
89
+ });
90
+
91
+ const result = await checkChildPiProcesses();
92
+
93
+ expect(result.hasActiveSubagents).toBe(false);
94
+ expect(result.count).toBe(0);
95
+ });
96
+
97
+ it('handles exec errors gracefully', async () => {
98
+ mockExec.mockImplementation((cmd, callback) => {
99
+ callback(new Error('ps command failed'), { stdout: '', stderr: '' }, null as any);
100
+ return {} as any;
101
+ });
102
+
103
+ const result = await checkChildPiProcesses();
104
+
105
+ expect(result.hasActiveSubagents).toBe(false);
106
+ expect(result.count).toBe(0);
107
+ expect(result.pids).toEqual([]);
108
+ });
109
+
110
+ it('handles malformed ps output', async () => {
111
+ mockExec.mockImplementation((cmd, callback) => {
112
+ callback(
113
+ null,
114
+ {
115
+ stdout: 'garbage line\n \n123 abc def extra',
116
+ stderr: '',
117
+ },
118
+ null as any
119
+ );
120
+ return {} as any;
121
+ });
122
+
123
+ const result = await checkChildPiProcesses();
124
+
125
+ expect(result.hasActiveSubagents).toBe(false);
126
+ expect(result.count).toBe(0);
127
+ });
128
+ });
129
+
130
+ describe('waitForSubagents', () => {
131
+ it('returns immediately when no subagents', async () => {
132
+ mockExec.mockImplementation((cmd, callback) => {
133
+ callback(null, { stdout: '', stderr: '' }, null as any);
134
+ return {} as any;
135
+ });
136
+
137
+ const result = await waitForSubagents(100, 1000);
138
+
139
+ expect(result.completed).toBe(true);
140
+ expect(result.finalStatus.hasActiveSubagents).toBe(false);
141
+ });
142
+
143
+ it('waits for subagents to complete', async () => {
144
+ const parentPid = process.pid;
145
+ let calls = 0;
146
+ mockExec.mockImplementation((cmd, callback) => {
147
+ calls++;
148
+ if (calls < 3) {
149
+ callback(
150
+ null,
151
+ {
152
+ stdout: `${parentPid} 12345 pi`,
153
+ stderr: '',
154
+ },
155
+ null as any
156
+ );
157
+ } else {
158
+ callback(null, { stdout: '', stderr: '' }, null as any);
159
+ }
160
+ return {} as any;
161
+ });
162
+
163
+ const result = await waitForSubagents(50, 1000);
164
+
165
+ expect(result.completed).toBe(true);
166
+ expect(result.finalStatus.hasActiveSubagents).toBe(false);
167
+ expect(calls).toBe(3);
168
+ });
169
+
170
+ it("times out if subagents don't complete", async () => {
171
+ const parentPid = process.pid;
172
+ mockExec.mockImplementation((cmd, callback) => {
173
+ callback(
174
+ null,
175
+ {
176
+ stdout: `${parentPid} 12345 pi`,
177
+ stderr: '',
178
+ },
179
+ null as any
180
+ );
181
+ return {} as any;
182
+ });
183
+
184
+ const result = await waitForSubagents(50, 100);
185
+
186
+ expect(result.completed).toBe(false);
187
+ expect(result.finalStatus.hasActiveSubagents).toBe(true);
188
+ expect(result.finalStatus.count).toBe(1);
189
+ });
190
+ });
191
+ });
@@ -0,0 +1,363 @@
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/inference.js', () => ({
10
+ inferOutcome: vi.fn(),
11
+ }));
12
+
13
+ vi.mock('../src/core/prompt-loader.js', () => ({
14
+ loadSystemPrompt: vi.fn().mockReturnValue({ prompt: 'test prompt', source: 'built-in' }),
15
+ }));
16
+
17
+ vi.mock('../src/ui/renderer.js', () => ({
18
+ updateUI: vi.fn(),
19
+ toggleWidget: vi.fn(),
20
+ }));
21
+
22
+ vi.mock('../src/ui/model-picker.js', () => ({
23
+ pickModel: vi.fn(),
24
+ }));
25
+
26
+ vi.mock('../src/global-config.js', () => ({
27
+ loadGlobalModel: vi.fn().mockReturnValue(null),
28
+ }));
29
+
30
+ vi.mock('../src/session/client.js', () => ({
31
+ disposeSession: vi.fn(),
32
+ }));
33
+
34
+ vi.mock('../src/subagent-detector.js', () => ({
35
+ checkChildPiProcesses: vi.fn().mockResolvedValue({ hasActiveSubagents: false, count: 0 }),
36
+ waitForSubagents: vi
37
+ .fn()
38
+ .mockResolvedValue({ completed: true, finalStatus: { hasActiveSubagents: false, count: 0 } }),
39
+ }));
40
+
41
+ import { updateUI } from '../src/ui/renderer.js';
42
+ import { pickModel } from '../src/ui/model-picker.js';
43
+ import { loadGlobalModel } from '../src/global-config.js';
44
+ import { inferOutcome } from '../src/core/inference.js';
45
+
46
+ describe('SupervisorStateManager - goal append feature', () => {
47
+ function createMockApi() {
48
+ return {
49
+ appendEntry: vi.fn(),
50
+ on: vi.fn(),
51
+ registerCommand: vi.fn(),
52
+ registerTool: vi.fn(),
53
+ sendUserMessage: vi.fn(),
54
+ sendMessage: vi.fn(),
55
+ events: { emit: vi.fn(), on: vi.fn() },
56
+ } as any;
57
+ }
58
+
59
+ it('appends to existing goal when supervision is already active', () => {
60
+ const api = createMockApi();
61
+ const state = new SupervisorStateManager(api);
62
+
63
+ state.start('Initial goal', 'anthropic', 'claude-haiku');
64
+ expect(state.getState()!.outcome).toBe('Initial goal');
65
+
66
+ const trimmed = 'Additional requirement';
67
+ const existing = state.getState();
68
+ const appendedOutcome = `${existing!.outcome}. Additionally: ${trimmed}`;
69
+ state.updateOutcome(appendedOutcome);
70
+
71
+ expect(state.getState()!.outcome).toBe('Initial goal. Additionally: Additional requirement');
72
+ expect(api.appendEntry).toHaveBeenCalledTimes(2);
73
+ });
74
+
75
+ it('persists appended goal to session', () => {
76
+ const api = createMockApi();
77
+ const state = new SupervisorStateManager(api);
78
+
79
+ state.start('First part', 'anthropic', 'claude-haiku');
80
+ state.updateOutcome('First part. Additionally: Second part');
81
+
82
+ const lastCall = api.appendEntry.mock.calls[api.appendEntry.mock.calls.length - 1];
83
+ expect(lastCall[1].outcome).toBe('First part. Additionally: Second part');
84
+ });
85
+
86
+ it('maintains active state when appending', () => {
87
+ const api = createMockApi();
88
+ const state = new SupervisorStateManager(api);
89
+
90
+ state.start('Original', 'anthropic', 'claude-haiku');
91
+ expect(state.isActive()).toBe(true);
92
+
93
+ state.updateOutcome('Original. Additionally: More');
94
+ expect(state.isActive()).toBe(true);
95
+ });
96
+
97
+ it('keeps other state intact when appending', () => {
98
+ const api = createMockApi();
99
+ const state = new SupervisorStateManager(api);
100
+
101
+ state.start('Goal', 'openai', 'gpt-4o');
102
+ state.addIntervention({
103
+ message: 'Focus',
104
+ reasoning: 'Test',
105
+ timestamp: Date.now(),
106
+ });
107
+
108
+ const originalProvider = state.getState()!.provider;
109
+ const originalModelId = state.getState()!.modelId;
110
+ const originalInterventions = state.getState()!.interventions.length;
111
+
112
+ state.updateOutcome('Goal. Additionally: Extended');
113
+
114
+ expect(state.getState()!.provider).toBe(originalProvider);
115
+ expect(state.getState()!.modelId).toBe(originalModelId);
116
+ expect(state.getState()!.interventions.length).toBe(originalInterventions);
117
+ });
118
+ });
119
+
120
+ describe('Supervise command kickstart behavior', () => {
121
+ function createMockCommandContext(overrides: { isIdle?: boolean; hasUI?: boolean } = {}) {
122
+ return {
123
+ ui: {
124
+ notify: vi.fn(),
125
+ select: vi.fn(),
126
+ input: vi.fn(),
127
+ confirm: vi.fn(),
128
+ setStatus: vi.fn(),
129
+ setWorkingMessage: vi.fn(),
130
+ setWidget: vi.fn(),
131
+ setFooter: vi.fn(),
132
+ setHeader: vi.fn(),
133
+ custom: vi.fn(),
134
+ pasteToEditor: vi.fn(),
135
+ setEditorText: vi.fn(),
136
+ getEditorText: vi.fn().mockReturnValue(''),
137
+ editor: vi.fn(),
138
+ setEditorComponent: vi.fn(),
139
+ theme: {},
140
+ getAllThemes: vi.fn().mockReturnValue([]),
141
+ getTheme: vi.fn(),
142
+ setTheme: vi.fn().mockReturnValue({ success: true }),
143
+ getToolsExpanded: vi.fn().mockReturnValue(false),
144
+ setToolsExpanded: vi.fn(),
145
+ onTerminalInput: vi.fn().mockReturnValue(() => {}),
146
+ },
147
+ hasUI: overrides.hasUI ?? true,
148
+ cwd: '/test',
149
+ sessionManager: {
150
+ getBranch: vi.fn().mockReturnValue([]),
151
+ },
152
+ modelRegistry: {
153
+ getApiKeyForProvider: vi.fn().mockResolvedValue('test-key'),
154
+ },
155
+ model: {
156
+ provider: 'anthropic',
157
+ id: 'claude-haiku',
158
+ },
159
+ isIdle: vi.fn().mockReturnValue(overrides.isIdle ?? true),
160
+ abort: vi.fn(),
161
+ hasPendingMessages: vi.fn().mockReturnValue(false),
162
+ shutdown: vi.fn(),
163
+ getContextUsage: vi.fn(),
164
+ compact: vi.fn(),
165
+ getSystemPrompt: vi.fn().mockReturnValue('test system prompt'),
166
+ } as any;
167
+ }
168
+
169
+ function createMockExtensionAPI() {
170
+ const sendUserMessage = vi.fn();
171
+ return {
172
+ appendEntry: vi.fn(),
173
+ on: vi.fn(),
174
+ registerCommand: vi.fn(),
175
+ registerTool: vi.fn(),
176
+ sendUserMessage,
177
+ sendMessage: vi.fn(),
178
+ events: { emit: vi.fn(), on: vi.fn() },
179
+ } as any;
180
+ }
181
+
182
+ beforeEach(() => {
183
+ vi.clearAllMocks();
184
+ });
185
+
186
+ describe('when agent is idle', () => {
187
+ it('should kickstart with goal when running explicit /supervise <outcome>', async () => {
188
+ const pi = createMockExtensionAPI();
189
+ const ctx = createMockCommandContext({ isIdle: true });
190
+ const state = new SupervisorStateManager(pi);
191
+
192
+ const trimmed = 'Implement JWT authentication';
193
+ state.start(trimmed, 'anthropic', 'claude-haiku');
194
+
195
+ if (ctx.isIdle()) {
196
+ pi.sendUserMessage(`Please start working on this goal: ${trimmed}`, {
197
+ deliverAs: 'followUp',
198
+ });
199
+ }
200
+
201
+ expect(pi.sendUserMessage).toHaveBeenCalledTimes(1);
202
+ expect(pi.sendUserMessage).toHaveBeenCalledWith(
203
+ 'Please start working on this goal: Implement JWT authentication',
204
+ { deliverAs: 'followUp' }
205
+ );
206
+ });
207
+
208
+ it('should kickstart with inferred goal when agent is idle', async () => {
209
+ vi.mocked(inferOutcome).mockResolvedValue('Fix the memory leak in handler');
210
+
211
+ const pi = createMockExtensionAPI();
212
+ const ctx = createMockCommandContext({ isIdle: true });
213
+ const state = new SupervisorStateManager(pi);
214
+
215
+ const inferred = 'Fix the memory leak in handler';
216
+ state.start(inferred, 'anthropic', 'claude-haiku');
217
+
218
+ if (ctx.isIdle()) {
219
+ pi.sendUserMessage(`Please start working on this goal: ${inferred}`, {
220
+ deliverAs: 'followUp',
221
+ });
222
+ }
223
+
224
+ expect(pi.sendUserMessage).toHaveBeenCalledWith(
225
+ 'Please start working on this goal: Fix the memory leak in handler',
226
+ { deliverAs: 'followUp' }
227
+ );
228
+ });
229
+
230
+ it('should kickstart when tool initiates supervision', async () => {
231
+ const pi = createMockExtensionAPI();
232
+ const ctx = createMockCommandContext({ isIdle: true });
233
+ const state = new SupervisorStateManager(pi);
234
+
235
+ const outcome = 'Refactor database layer';
236
+ state.start(outcome, 'anthropic', 'claude-haiku');
237
+
238
+ if (ctx.isIdle()) {
239
+ pi.sendUserMessage(`Please start working on this goal: ${outcome}`, {
240
+ deliverAs: 'followUp',
241
+ });
242
+ }
243
+
244
+ expect(pi.sendUserMessage).toHaveBeenCalledWith(
245
+ 'Please start working on this goal: Refactor database layer',
246
+ { deliverAs: 'followUp' }
247
+ );
248
+ });
249
+ });
250
+
251
+ describe('when agent is busy', () => {
252
+ it('should NOT kickstart when running /supervise and agent is working', async () => {
253
+ const pi = createMockExtensionAPI();
254
+ const ctx = createMockCommandContext({ isIdle: false });
255
+ const state = new SupervisorStateManager(pi);
256
+
257
+ const trimmed = 'Implement feature X';
258
+ state.start(trimmed, 'anthropic', 'claude-haiku');
259
+
260
+ if (ctx.isIdle()) {
261
+ pi.sendUserMessage(`Please start working on this goal: ${trimmed}`, {
262
+ deliverAs: 'followUp',
263
+ });
264
+ }
265
+
266
+ expect(pi.sendUserMessage).not.toHaveBeenCalled();
267
+ });
268
+
269
+ it('should NOT kickstart when tool initiates supervision but agent is busy', async () => {
270
+ const pi = createMockExtensionAPI();
271
+ const ctx = createMockCommandContext({ isIdle: false });
272
+ const state = new SupervisorStateManager(pi);
273
+
274
+ const outcome = 'Add test coverage';
275
+ state.start(outcome, 'anthropic', 'claude-haiku');
276
+
277
+ if (ctx.isIdle()) {
278
+ pi.sendUserMessage(`Please start working on this goal: ${outcome}`, {
279
+ deliverAs: 'followUp',
280
+ });
281
+ }
282
+
283
+ expect(pi.sendUserMessage).not.toHaveBeenCalled();
284
+ });
285
+ });
286
+
287
+ describe('append behavior', () => {
288
+ it('should NOT kickstart when appending to existing supervision', async () => {
289
+ const pi = createMockExtensionAPI();
290
+ const ctx = createMockCommandContext({ isIdle: true });
291
+ const state = new SupervisorStateManager(pi);
292
+
293
+ state.start('Original goal', 'anthropic', 'claude-haiku');
294
+
295
+ pi.sendUserMessage.mockClear();
296
+
297
+ const trimmed = 'Additional requirement';
298
+ const existing = state.getState();
299
+ const appendedOutcome = `${existing!.outcome}. Additionally: ${trimmed}`;
300
+ state.updateOutcome(appendedOutcome);
301
+
302
+ expect(pi.sendUserMessage).not.toHaveBeenCalled();
303
+ expect(state.getState()!.outcome).toBe('Original goal. Additionally: Additional requirement');
304
+ });
305
+
306
+ it('should notify user when appending to goal', async () => {
307
+ const pi = createMockExtensionAPI();
308
+ const ctx = createMockCommandContext({ isIdle: true });
309
+ const state = new SupervisorStateManager(pi);
310
+
311
+ state.start('Original', 'anthropic', 'claude-haiku');
312
+
313
+ const trimmed = 'More work';
314
+ const existing = state.getState();
315
+ const appendedOutcome = `${existing!.outcome}. Additionally: ${trimmed}`;
316
+ state.updateOutcome(appendedOutcome);
317
+
318
+ expect(state.getState()!.outcome).toContain('Additionally: More work');
319
+ });
320
+ });
321
+
322
+ describe('edge cases', () => {
323
+ it('should handle kickstart with very long goals', async () => {
324
+ const pi = createMockExtensionAPI();
325
+ const ctx = createMockCommandContext({ isIdle: true });
326
+ const state = new SupervisorStateManager(pi);
327
+
328
+ const longGoal = 'A'.repeat(200);
329
+ state.start(longGoal, 'anthropic', 'claude-haiku');
330
+
331
+ if (ctx.isIdle()) {
332
+ pi.sendUserMessage(`Please start working on this goal: ${longGoal}`, {
333
+ deliverAs: 'followUp',
334
+ });
335
+ }
336
+
337
+ expect(pi.sendUserMessage).toHaveBeenCalledWith(
338
+ `Please start working on this goal: ${longGoal}`,
339
+ { deliverAs: 'followUp' }
340
+ );
341
+ });
342
+
343
+ it('should handle empty isIdle() result gracefully', async () => {
344
+ const pi = createMockExtensionAPI();
345
+ const ctx = {
346
+ ...createMockCommandContext(),
347
+ isIdle: vi.fn().mockReturnValue(undefined),
348
+ };
349
+ const state = new SupervisorStateManager(pi);
350
+
351
+ const trimmed = 'Test goal';
352
+ state.start(trimmed, 'anthropic', 'claude-haiku');
353
+
354
+ if (ctx.isIdle()) {
355
+ pi.sendUserMessage(`Please start working on this goal: ${trimmed}`, {
356
+ deliverAs: 'followUp',
357
+ });
358
+ }
359
+
360
+ expect(pi.sendUserMessage).not.toHaveBeenCalled();
361
+ });
362
+ });
363
+ });