@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.
- package/CHANGELOG.md +120 -0
- package/LICENSE +21 -0
- package/README.md +341 -0
- package/media/demo.mp4 +0 -0
- package/media/screenshot.png +0 -0
- package/package.json +87 -0
- package/src/compaction/brief.ts +841 -0
- package/src/compaction/build-sections.ts +340 -0
- package/src/compaction/causal-keys.ts +138 -0
- package/src/compaction/content.ts +68 -0
- package/src/compaction/extract/commits.ts +78 -0
- package/src/compaction/extract/goals.ts +79 -0
- package/src/compaction/extract/preferences.ts +52 -0
- package/src/compaction/extract/shared-symbols.ts +376 -0
- package/src/compaction/filter-noise.ts +47 -0
- package/src/compaction/format.ts +89 -0
- package/src/compaction/index.ts +38 -0
- package/src/compaction/normalize.ts +73 -0
- package/src/compaction/sanitize.ts +5 -0
- package/src/compaction/sections.ts +19 -0
- package/src/compaction/skill-collapse.ts +35 -0
- package/src/compaction/tool-args.ts +14 -0
- package/src/compaction/types.ts +26 -0
- package/src/core/analyzer.ts +58 -0
- package/src/core/index.ts +8 -0
- package/src/core/inference.ts +77 -0
- package/src/core/prompt-builder.ts +137 -0
- package/src/core/prompt-loader.ts +125 -0
- package/src/core/reframe.ts +27 -0
- package/src/fabric-provider.ts +115 -0
- package/src/global-config.ts +65 -0
- package/src/index.ts +514 -0
- package/src/session/client.ts +46 -0
- package/src/session/response-parser.ts +37 -0
- package/src/session/supervisor-session.ts +102 -0
- package/src/state/manager.ts +133 -0
- package/src/state/mid-run-signals.ts +103 -0
- package/src/state/patterns.ts +82 -0
- package/src/state/reframe.ts +27 -0
- package/src/subagent-detector.ts +94 -0
- package/src/types.ts +42 -0
- package/src/ui/animations.ts +95 -0
- package/src/ui/model-picker.ts +72 -0
- package/src/ui/model-settings-selector.ts +440 -0
- package/src/ui/model-sort.ts +101 -0
- package/src/ui/renderer.ts +314 -0
- package/src/ui/types.ts +48 -0
- package/tests/compaction.test.ts +507 -0
- package/tests/engine.test.ts +622 -0
- package/tests/ephemeral-supervision.test.ts +347 -0
- package/tests/fabric-provider.test.ts +55 -0
- package/tests/full-fidelity-snapshot.test.ts +250 -0
- package/tests/global-config.test.ts +74 -0
- package/tests/model-sort.test.ts +157 -0
- package/tests/parsing.test.ts +303 -0
- package/tests/state.test.ts +474 -0
- package/tests/status-widget.test.ts +539 -0
- package/tests/subagent-detector.test.ts +191 -0
- package/tests/supervise-command.test.ts +363 -0
- package/tests/supervise-model-command.test.ts +184 -0
- package/tsconfig.json +14 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import type { SupervisorState } from '../src/types.js';
|
|
3
|
+
|
|
4
|
+
// Mock fs for loadSystemPrompt tests
|
|
5
|
+
vi.mock('node:fs', async () => {
|
|
6
|
+
const actual = await vi.importActual<typeof import('node:fs')>('node:fs');
|
|
7
|
+
return {
|
|
8
|
+
...actual,
|
|
9
|
+
existsSync: vi.fn(),
|
|
10
|
+
readFileSync: vi.fn(),
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import {
|
|
16
|
+
loadSystemPrompt,
|
|
17
|
+
buildUserPrompt,
|
|
18
|
+
getReframeGuidance,
|
|
19
|
+
inferOutcome,
|
|
20
|
+
} from '../src/core/index.js';
|
|
21
|
+
import { SupervisorSession } from '../src/session/supervisor-session.js';
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
vi.stubEnv('PI_CODING_AGENT_DIR', '/home/test/.pi/agent');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
vi.unstubAllEnvs();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('loadSystemPrompt', () => {
|
|
33
|
+
it('returns built-in prompt when no files exist', () => {
|
|
34
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
35
|
+
|
|
36
|
+
const result = loadSystemPrompt('/test/cwd');
|
|
37
|
+
|
|
38
|
+
expect(result.source).toBe('built-in');
|
|
39
|
+
expect(result.prompt).toContain('You are a supervisor');
|
|
40
|
+
expect(result.prompt).toContain('Response schema');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('loads project SUPERVISOR.md when it exists', () => {
|
|
44
|
+
vi.mocked(existsSync).mockImplementation((path) => {
|
|
45
|
+
return String(path).includes('/test/cwd/.pi/SUPERVISOR.md');
|
|
46
|
+
});
|
|
47
|
+
vi.mocked(readFileSync).mockReturnValue('Custom project prompt');
|
|
48
|
+
|
|
49
|
+
const result = loadSystemPrompt('/test/cwd');
|
|
50
|
+
|
|
51
|
+
expect(result.source).toBe('/test/cwd/.pi/SUPERVISOR.md');
|
|
52
|
+
expect(result.prompt).toBe('Custom project prompt');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("falls back to global when project doesn't exist", () => {
|
|
56
|
+
vi.mocked(existsSync).mockImplementation((path) => {
|
|
57
|
+
return String(path).includes('/home/test/.pi/agent/SUPERVISOR.md');
|
|
58
|
+
});
|
|
59
|
+
vi.mocked(readFileSync).mockReturnValue('Global prompt');
|
|
60
|
+
|
|
61
|
+
const result = loadSystemPrompt('/test/cwd');
|
|
62
|
+
|
|
63
|
+
expect(result.source).toBe('/home/test/.pi/agent/SUPERVISOR.md');
|
|
64
|
+
expect(result.prompt).toBe('Global prompt');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('prefers project over global', () => {
|
|
68
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
69
|
+
vi.mocked(readFileSync).mockReturnValue('Project wins');
|
|
70
|
+
|
|
71
|
+
const result = loadSystemPrompt('/test/cwd');
|
|
72
|
+
|
|
73
|
+
expect(result.source).toBe('/test/cwd/.pi/SUPERVISOR.md');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('built-in prompt includes cheating prevention section', () => {
|
|
77
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
78
|
+
|
|
79
|
+
const result = loadSystemPrompt('/test/cwd');
|
|
80
|
+
|
|
81
|
+
expect(result.source).toBe('built-in');
|
|
82
|
+
expect(result.prompt).toContain('CHEATING PREVENTION');
|
|
83
|
+
expect(result.prompt).toContain('Unverified Claims');
|
|
84
|
+
expect(result.prompt).toContain('Test Manipulation');
|
|
85
|
+
expect(result.prompt).toContain('Metric Gaming');
|
|
86
|
+
expect(result.prompt).toContain('Short-Circuiting');
|
|
87
|
+
expect(result.prompt).toContain('Contradictions');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('built-in prompt includes ASI loop section', () => {
|
|
91
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
92
|
+
|
|
93
|
+
const result = loadSystemPrompt('/test/cwd');
|
|
94
|
+
|
|
95
|
+
expect(result.source).toBe('built-in');
|
|
96
|
+
expect(result.prompt).toContain('CLOSING THE ASI LOOP');
|
|
97
|
+
expect(result.prompt).toContain(
|
|
98
|
+
'ASI (Actionable Side Information) is your memory across turns'
|
|
99
|
+
);
|
|
100
|
+
expect(result.prompt).toContain('READ your past ASI entries');
|
|
101
|
+
expect(result.prompt).toContain('REQUIRED when steering');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('getReframeGuidance', () => {
|
|
106
|
+
it('returns empty string for tier 0 without ineffective pattern', () => {
|
|
107
|
+
const result = getReframeGuidance(0);
|
|
108
|
+
expect(result).toBe('');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('returns pattern warning even for tier 0 when ineffective pattern detected', () => {
|
|
112
|
+
const result = getReframeGuidance(0, {
|
|
113
|
+
detected: true,
|
|
114
|
+
similarCount: 2,
|
|
115
|
+
secondsSinceLastSteer: 90,
|
|
116
|
+
});
|
|
117
|
+
expect(result).toContain('INEFFECTIVE PATTERN DETECTED');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('returns tier 1 guidance', () => {
|
|
121
|
+
const result = getReframeGuidance(1);
|
|
122
|
+
expect(result).toContain('REFRAME TIER 1');
|
|
123
|
+
expect(result).toContain('DIRECTIVE');
|
|
124
|
+
expect(result).toContain('extremely specific');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('returns tier 2 guidance', () => {
|
|
128
|
+
const result = getReframeGuidance(2);
|
|
129
|
+
expect(result).toContain('REFRAME TIER 2');
|
|
130
|
+
expect(result).toContain('SUBGOAL');
|
|
131
|
+
expect(result).toContain('smaller, verifiable milestone');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('returns tier 3 guidance', () => {
|
|
135
|
+
const result = getReframeGuidance(3);
|
|
136
|
+
expect(result).toContain('REFRAME TIER 3');
|
|
137
|
+
expect(result).toContain('PIVOT');
|
|
138
|
+
expect(result).toContain('completely different strategy');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('returns tier 4 guidance', () => {
|
|
142
|
+
const result = getReframeGuidance(4);
|
|
143
|
+
expect(result).toContain('REFRAME TIER 4');
|
|
144
|
+
expect(result).toContain('MINIMAL SLICE');
|
|
145
|
+
expect(result).toContain('smallest working version');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('includes ineffective pattern warning when detected', () => {
|
|
149
|
+
const result = getReframeGuidance(2, {
|
|
150
|
+
detected: true,
|
|
151
|
+
similarCount: 2,
|
|
152
|
+
secondsSinceLastSteer: 90,
|
|
153
|
+
});
|
|
154
|
+
expect(result).toContain('INEFFECTIVE PATTERN DETECTED');
|
|
155
|
+
expect(result).toContain('Last 2 steering messages');
|
|
156
|
+
expect(result).toContain('90s since last steer');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('does not include pattern warning when not detected', () => {
|
|
160
|
+
const result = getReframeGuidance(2, {
|
|
161
|
+
detected: false,
|
|
162
|
+
similarCount: 1,
|
|
163
|
+
secondsSinceLastSteer: 10,
|
|
164
|
+
});
|
|
165
|
+
expect(result).not.toContain('INEFFECTIVE PATTERN DETECTED');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('buildUserPrompt', () => {
|
|
170
|
+
it('includes reframe guidance when tier > 0', () => {
|
|
171
|
+
const state: SupervisorState = {
|
|
172
|
+
active: true,
|
|
173
|
+
outcome: 'Implement auth',
|
|
174
|
+
provider: 'anthropic',
|
|
175
|
+
modelId: 'claude',
|
|
176
|
+
interventions: [],
|
|
177
|
+
startedAt: Date.now(),
|
|
178
|
+
reframeTier: 2,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const result = buildUserPrompt(state, '[Session Goal]\n- Do stuff', true);
|
|
182
|
+
expect(result).toContain('REFRAME TIER 2');
|
|
183
|
+
expect(result).toContain('SUBGOAL');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('does not include reframe guidance when tier is 0', () => {
|
|
187
|
+
const state: SupervisorState = {
|
|
188
|
+
active: true,
|
|
189
|
+
outcome: 'Implement auth',
|
|
190
|
+
provider: 'anthropic',
|
|
191
|
+
modelId: 'claude',
|
|
192
|
+
interventions: [],
|
|
193
|
+
startedAt: Date.now(),
|
|
194
|
+
reframeTier: 0,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const result = buildUserPrompt(state, '[Session Goal]\n- Do stuff', true);
|
|
198
|
+
expect(result).not.toContain('REFRAME TIER');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('includes ineffective pattern warning in prompt', () => {
|
|
202
|
+
const state: SupervisorState = {
|
|
203
|
+
active: true,
|
|
204
|
+
outcome: 'Implement auth',
|
|
205
|
+
provider: 'anthropic',
|
|
206
|
+
modelId: 'claude',
|
|
207
|
+
interventions: [{ message: 'Focus on auth', reasoning: 'Test', timestamp: Date.now() }],
|
|
208
|
+
startedAt: Date.now(),
|
|
209
|
+
reframeTier: 1,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const ineffectivePattern = { detected: true, similarCount: 1, secondsSinceLastSteer: 90 };
|
|
213
|
+
const result = buildUserPrompt(state, '[Session Goal]\n- Do stuff', true, ineffectivePattern);
|
|
214
|
+
|
|
215
|
+
expect(result).toContain('INEFFECTIVE PATTERN DETECTED');
|
|
216
|
+
expect(result).toContain('90s since last steer');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('includes outcome and agent status', () => {
|
|
220
|
+
const state: SupervisorState = {
|
|
221
|
+
active: true,
|
|
222
|
+
outcome: 'Build API',
|
|
223
|
+
provider: 'anthropic',
|
|
224
|
+
modelId: 'claude',
|
|
225
|
+
interventions: [],
|
|
226
|
+
startedAt: Date.now(),
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const result = buildUserPrompt(state, '[Session Goal]\n- Do stuff', true);
|
|
230
|
+
expect(result).toContain('DESIRED OUTCOME:');
|
|
231
|
+
expect(result).toContain('Build API');
|
|
232
|
+
expect(result).toContain('AGENT STATUS: IDLE');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('shows WORKING status when agent is not idle', () => {
|
|
236
|
+
const state: SupervisorState = {
|
|
237
|
+
active: true,
|
|
238
|
+
outcome: 'Build API',
|
|
239
|
+
provider: 'anthropic',
|
|
240
|
+
modelId: 'claude',
|
|
241
|
+
interventions: [],
|
|
242
|
+
startedAt: Date.now(),
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const result = buildUserPrompt(state, '[Session Goal]\n- Do stuff', false);
|
|
246
|
+
expect(result).toContain('AGENT STATUS: WORKING');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('includes structured conversation context', () => {
|
|
250
|
+
const state: SupervisorState = {
|
|
251
|
+
active: true,
|
|
252
|
+
outcome: 'Build API',
|
|
253
|
+
provider: 'anthropic',
|
|
254
|
+
modelId: 'claude',
|
|
255
|
+
interventions: [],
|
|
256
|
+
startedAt: Date.now(),
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const contextText =
|
|
260
|
+
'[Session Goal]\n- Build auth\n[Outstanding Context]\n- [ERROR] build failed';
|
|
261
|
+
const result = buildUserPrompt(state, contextText, true);
|
|
262
|
+
expect(result).toContain('STRUCTURED CONVERSATION CONTEXT:');
|
|
263
|
+
expect(result).toContain('[Session Goal]');
|
|
264
|
+
expect(result).toContain('Build auth');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('includes intervention history', () => {
|
|
268
|
+
const state: SupervisorState = {
|
|
269
|
+
active: true,
|
|
270
|
+
outcome: 'Build API',
|
|
271
|
+
provider: 'anthropic',
|
|
272
|
+
modelId: 'claude',
|
|
273
|
+
interventions: [{ message: 'Focus on X', reasoning: 'Drift', timestamp: 123456 }],
|
|
274
|
+
startedAt: Date.now(),
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const result = buildUserPrompt(state, '', true);
|
|
278
|
+
expect(result).toContain('YOUR INTERVENTION HISTORY (with ASI observations):');
|
|
279
|
+
expect(result).toContain('[1]');
|
|
280
|
+
expect(result).toContain('Focus on X');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('includes ASI from previous interventions', () => {
|
|
284
|
+
const state: SupervisorState = {
|
|
285
|
+
active: true,
|
|
286
|
+
outcome: 'Build API',
|
|
287
|
+
provider: 'anthropic',
|
|
288
|
+
modelId: 'claude',
|
|
289
|
+
interventions: [
|
|
290
|
+
{
|
|
291
|
+
message: 'Focus on tests',
|
|
292
|
+
reasoning: 'Drift',
|
|
293
|
+
timestamp: 123456,
|
|
294
|
+
asi: {
|
|
295
|
+
why_stuck: 'agent refactoring without tests',
|
|
296
|
+
strategy_used: 'directive',
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
startedAt: Date.now(),
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const result = buildUserPrompt(state, '', true);
|
|
304
|
+
expect(result).toContain('ASI {');
|
|
305
|
+
expect(result).toContain('why_stuck: "agent refactoring without tests"');
|
|
306
|
+
expect(result).toContain('strategy_used: "directive"');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('surfaces recurring ASI patterns in summary', () => {
|
|
310
|
+
const state: SupervisorState = {
|
|
311
|
+
active: true,
|
|
312
|
+
outcome: 'Build API',
|
|
313
|
+
provider: 'anthropic',
|
|
314
|
+
modelId: 'claude',
|
|
315
|
+
interventions: [
|
|
316
|
+
{
|
|
317
|
+
message: 'Focus on tests',
|
|
318
|
+
reasoning: 'Drift',
|
|
319
|
+
timestamp: 123456,
|
|
320
|
+
asi: { suspicious_claim: true, pattern: 'unverified' },
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
message: 'Verify the output',
|
|
324
|
+
reasoning: 'Contradiction',
|
|
325
|
+
timestamp: 123457,
|
|
326
|
+
asi: { suspicious_claim: true, pattern: 'contradicted' },
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
message: 'Show proof',
|
|
330
|
+
reasoning: 'Unverified',
|
|
331
|
+
timestamp: 123458,
|
|
332
|
+
asi: { requires_proof: true },
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
startedAt: Date.now(),
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const result = buildUserPrompt(state, '', true);
|
|
339
|
+
expect(result).toContain('ASI PATTERN SUMMARY');
|
|
340
|
+
expect(result).toContain('Pattern seen 2x: "suspicious_claim"');
|
|
341
|
+
expect(result).toContain('⚠️ Previous interventions flagged suspicious claims');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('warns about verification failures in ASI summary', () => {
|
|
345
|
+
const state: SupervisorState = {
|
|
346
|
+
active: true,
|
|
347
|
+
outcome: 'Build API',
|
|
348
|
+
provider: 'anthropic',
|
|
349
|
+
modelId: 'claude',
|
|
350
|
+
interventions: [
|
|
351
|
+
{
|
|
352
|
+
message: 'Focus on tests',
|
|
353
|
+
reasoning: 'Drift',
|
|
354
|
+
timestamp: 123456,
|
|
355
|
+
asi: { claim_status: 'contradicted_by_tool_output' },
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
message: 'Verify the output',
|
|
359
|
+
reasoning: 'Contradiction',
|
|
360
|
+
timestamp: 123457,
|
|
361
|
+
asi: { claim_status: 'unverified' },
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
message: 'Show proof',
|
|
365
|
+
reasoning: 'Unverified',
|
|
366
|
+
timestamp: 123458,
|
|
367
|
+
asi: { claim_status: 'contradicted_by_tool_output' },
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
startedAt: Date.now(),
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const result = buildUserPrompt(state, '', true);
|
|
374
|
+
expect(result).toContain('ASI PATTERN SUMMARY');
|
|
375
|
+
expect(result).toContain('⚠️ 3 interventions involved unverified/contradicted claims');
|
|
376
|
+
expect(result).toContain('agent has pattern of unreliable reporting');
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('detects suspicious keywords in ASI values', () => {
|
|
380
|
+
const state: SupervisorState = {
|
|
381
|
+
active: true,
|
|
382
|
+
outcome: 'Build API',
|
|
383
|
+
provider: 'anthropic',
|
|
384
|
+
modelId: 'claude',
|
|
385
|
+
interventions: [
|
|
386
|
+
{
|
|
387
|
+
message: 'Check for cheating',
|
|
388
|
+
reasoning: 'Suspicious',
|
|
389
|
+
timestamp: 123456,
|
|
390
|
+
asi: { observation: 'agent_attempted_to_fake_test_results' },
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
startedAt: Date.now(),
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const result = buildUserPrompt(state, '', true);
|
|
397
|
+
expect(result).toContain('ASI PATTERN SUMMARY');
|
|
398
|
+
expect(result).toContain('⚠️ Previous interventions flagged suspicious claims');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('handles empty ASI gracefully', () => {
|
|
402
|
+
const state: SupervisorState = {
|
|
403
|
+
active: true,
|
|
404
|
+
outcome: 'Build API',
|
|
405
|
+
provider: 'anthropic',
|
|
406
|
+
modelId: 'claude',
|
|
407
|
+
interventions: [
|
|
408
|
+
{
|
|
409
|
+
message: 'Focus on X',
|
|
410
|
+
reasoning: 'Drift',
|
|
411
|
+
timestamp: 123456,
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
startedAt: Date.now(),
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const result = buildUserPrompt(state, '', true);
|
|
418
|
+
expect(result).toContain('[1] "Focus on X"');
|
|
419
|
+
expect(result).not.toContain('ASI {}');
|
|
420
|
+
expect(result).not.toContain('ASI PATTERN SUMMARY');
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('shows fallback when no context available', () => {
|
|
424
|
+
const state: SupervisorState = {
|
|
425
|
+
active: true,
|
|
426
|
+
outcome: 'Build API',
|
|
427
|
+
provider: 'anthropic',
|
|
428
|
+
modelId: 'claude',
|
|
429
|
+
interventions: [],
|
|
430
|
+
startedAt: Date.now(),
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const result = buildUserPrompt(state, '', true);
|
|
434
|
+
expect(result).toContain('No conversation context available');
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
describe('inferOutcome', () => {
|
|
439
|
+
let ensureStartedSpy: ReturnType<typeof vi.spyOn>;
|
|
440
|
+
let promptSpy: ReturnType<typeof vi.spyOn>;
|
|
441
|
+
let disposeSpy: ReturnType<typeof vi.spyOn>;
|
|
442
|
+
|
|
443
|
+
beforeEach(() => {
|
|
444
|
+
ensureStartedSpy = vi.spyOn(SupervisorSession.prototype, 'ensureStarted');
|
|
445
|
+
promptSpy = vi.spyOn(SupervisorSession.prototype, 'prompt');
|
|
446
|
+
disposeSpy = vi.spyOn(SupervisorSession.prototype, 'dispose');
|
|
447
|
+
|
|
448
|
+
ensureStartedSpy.mockResolvedValue(true);
|
|
449
|
+
promptSpy.mockResolvedValue('Build auth system');
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
afterEach(() => {
|
|
453
|
+
ensureStartedSpy.mockRestore();
|
|
454
|
+
promptSpy.mockRestore();
|
|
455
|
+
disposeSpy.mockRestore();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('returns null when sessionManager has no branch entries', async () => {
|
|
459
|
+
const mockCtx = {
|
|
460
|
+
sessionManager: {
|
|
461
|
+
getBranch: () => [],
|
|
462
|
+
},
|
|
463
|
+
modelRegistry: {
|
|
464
|
+
find: () => ({ name: 'test-model' }),
|
|
465
|
+
},
|
|
466
|
+
} as any;
|
|
467
|
+
|
|
468
|
+
const result = await inferOutcome(mockCtx, 'anthropic', 'claude');
|
|
469
|
+
expect(result).toBeNull();
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('returns null when model not found in registry', async () => {
|
|
473
|
+
const mockCtx = {
|
|
474
|
+
sessionManager: {
|
|
475
|
+
getBranch: () => [{ type: 'message', message: { role: 'user', content: 'Hello' } }],
|
|
476
|
+
},
|
|
477
|
+
modelRegistry: {
|
|
478
|
+
find: () => null,
|
|
479
|
+
},
|
|
480
|
+
} as any;
|
|
481
|
+
|
|
482
|
+
ensureStartedSpy.mockResolvedValue(false);
|
|
483
|
+
|
|
484
|
+
const result = await inferOutcome(mockCtx, 'anthropic', 'claude');
|
|
485
|
+
expect(result).toBeNull();
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('returns null when session fails to start', async () => {
|
|
489
|
+
const mockCtx = {
|
|
490
|
+
sessionManager: {
|
|
491
|
+
getBranch: () => [
|
|
492
|
+
{
|
|
493
|
+
type: 'message',
|
|
494
|
+
message: { role: 'user', content: [{ type: 'text', text: 'Build auth' }] },
|
|
495
|
+
},
|
|
496
|
+
],
|
|
497
|
+
},
|
|
498
|
+
modelRegistry: {
|
|
499
|
+
find: () => ({ name: 'test-model' }),
|
|
500
|
+
},
|
|
501
|
+
} as any;
|
|
502
|
+
|
|
503
|
+
ensureStartedSpy.mockResolvedValue(false);
|
|
504
|
+
|
|
505
|
+
const result = await inferOutcome(mockCtx, 'anthropic', 'claude');
|
|
506
|
+
expect(result).toBeNull();
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('extracts outcome successfully', async () => {
|
|
510
|
+
const mockCtx = {
|
|
511
|
+
sessionManager: {
|
|
512
|
+
getBranch: () => [
|
|
513
|
+
{
|
|
514
|
+
type: 'message',
|
|
515
|
+
message: { role: 'user', content: [{ type: 'text', text: 'Build auth' }] },
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
},
|
|
519
|
+
modelRegistry: {
|
|
520
|
+
find: () => ({ name: 'test-model' }),
|
|
521
|
+
},
|
|
522
|
+
} as any;
|
|
523
|
+
|
|
524
|
+
promptSpy.mockResolvedValue('Add JWT authentication with refresh tokens');
|
|
525
|
+
|
|
526
|
+
const result = await inferOutcome(mockCtx, 'anthropic', 'claude');
|
|
527
|
+
|
|
528
|
+
expect(result).toBe('Add JWT authentication with refresh tokens');
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('cleans up result: removes quotes, newlines, and limits length', async () => {
|
|
532
|
+
const mockCtx = {
|
|
533
|
+
sessionManager: {
|
|
534
|
+
getBranch: () => [
|
|
535
|
+
{
|
|
536
|
+
type: 'message',
|
|
537
|
+
message: { role: 'user', content: [{ type: 'text', text: 'Build auth' }] },
|
|
538
|
+
},
|
|
539
|
+
],
|
|
540
|
+
},
|
|
541
|
+
modelRegistry: {
|
|
542
|
+
find: () => ({ name: 'test-model' }),
|
|
543
|
+
},
|
|
544
|
+
} as any;
|
|
545
|
+
|
|
546
|
+
promptSpy.mockResolvedValue('"Fix the\nbug in the handler"');
|
|
547
|
+
|
|
548
|
+
const result = await inferOutcome(mockCtx, 'anthropic', 'claude');
|
|
549
|
+
|
|
550
|
+
expect(result).toBe('Fix the bug in the handler');
|
|
551
|
+
expect(result.length).toBeLessThanOrEqual(200);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it('returns null when prompt returns null', async () => {
|
|
555
|
+
const mockCtx = {
|
|
556
|
+
sessionManager: {
|
|
557
|
+
getBranch: () => [
|
|
558
|
+
{
|
|
559
|
+
type: 'message',
|
|
560
|
+
message: { role: 'user', content: [{ type: 'text', text: 'Build auth' }] },
|
|
561
|
+
},
|
|
562
|
+
],
|
|
563
|
+
},
|
|
564
|
+
modelRegistry: {
|
|
565
|
+
find: () => ({ name: 'test-model' }),
|
|
566
|
+
},
|
|
567
|
+
} as any;
|
|
568
|
+
|
|
569
|
+
promptSpy.mockResolvedValue(null);
|
|
570
|
+
|
|
571
|
+
const result = await inferOutcome(mockCtx, 'anthropic', 'claude');
|
|
572
|
+
|
|
573
|
+
expect(result).toBeNull();
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('returns null when exception thrown', async () => {
|
|
577
|
+
const mockCtx = {
|
|
578
|
+
sessionManager: {
|
|
579
|
+
getBranch: () => [
|
|
580
|
+
{
|
|
581
|
+
type: 'message',
|
|
582
|
+
message: { role: 'user', content: [{ type: 'text', text: 'Build auth' }] },
|
|
583
|
+
},
|
|
584
|
+
],
|
|
585
|
+
},
|
|
586
|
+
modelRegistry: {
|
|
587
|
+
find: () => ({ name: 'test-model' }),
|
|
588
|
+
},
|
|
589
|
+
} as any;
|
|
590
|
+
|
|
591
|
+
ensureStartedSpy.mockRejectedValue(new Error('Network error'));
|
|
592
|
+
|
|
593
|
+
const result = await inferOutcome(mockCtx, 'anthropic', 'claude');
|
|
594
|
+
|
|
595
|
+
expect(result).toBeNull();
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it('uses goal extraction system prompt', async () => {
|
|
599
|
+
const mockCtx = {
|
|
600
|
+
sessionManager: {
|
|
601
|
+
getBranch: () => [
|
|
602
|
+
{
|
|
603
|
+
type: 'message',
|
|
604
|
+
message: { role: 'user', content: [{ type: 'text', text: 'Build auth' }] },
|
|
605
|
+
},
|
|
606
|
+
],
|
|
607
|
+
},
|
|
608
|
+
modelRegistry: {
|
|
609
|
+
find: () => ({ name: 'test-model' }),
|
|
610
|
+
},
|
|
611
|
+
} as any;
|
|
612
|
+
|
|
613
|
+
await inferOutcome(mockCtx, 'anthropic', 'claude');
|
|
614
|
+
|
|
615
|
+
expect(ensureStartedSpy).toHaveBeenCalledWith(
|
|
616
|
+
mockCtx,
|
|
617
|
+
'anthropic',
|
|
618
|
+
'claude',
|
|
619
|
+
expect.stringContaining('goal extraction')
|
|
620
|
+
);
|
|
621
|
+
});
|
|
622
|
+
});
|