principles-disciple 1.17.0 → 1.18.0

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.
@@ -71,225 +71,6 @@ describe('EvolutionWorkerService nocturnal hardening', () => {
71
71
  EvolutionWorkerService.api = null;
72
72
  });
73
73
 
74
- it('does not start a nocturnal workflow when only an empty fallback snapshot is available', async () => {
75
- const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-nocturnal-empty-'));
76
- const stateDir = path.join(workspaceDir, '.state');
77
- fs.mkdirSync(path.join(stateDir, 'sessions'), { recursive: true });
78
- fs.mkdirSync(path.join(stateDir, 'logs'), { recursive: true });
79
-
80
- mockGetNocturnalSessionSnapshot.mockReturnValue(null);
81
-
82
- fs.writeFileSync(
83
- path.join(stateDir, 'evolution_queue.json'),
84
- JSON.stringify([
85
- {
86
- id: 'sleep-empty',
87
- taskKind: 'sleep_reflection',
88
- priority: 'medium',
89
- score: 50,
90
- source: 'nocturnal',
91
- reason: 'Sleep reflection',
92
- timestamp: '2026-04-10T00:00:00.000Z',
93
- enqueued_at: '2026-04-10T00:00:00.000Z',
94
- status: 'pending',
95
- retryCount: 0,
96
- maxRetries: 1,
97
- recentPainContext: {
98
- mostRecent: null,
99
- recentPainCount: 0,
100
- recentMaxPainScore: 0,
101
- },
102
- },
103
- ], null, 2),
104
- 'utf8'
105
- );
106
-
107
- try {
108
- EvolutionWorkerService.start({
109
- workspaceDir,
110
- stateDir,
111
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
112
- config: {},
113
- } as any);
114
-
115
- await vi.advanceTimersByTimeAsync(6000);
116
-
117
- const queue = readQueue(stateDir);
118
- expect(queue[0].status).toBe('failed');
119
- expect(queue[0].lastError).toContain('invalid_snapshot_ingress');
120
- expect(queue[0].lastError).toContain('fallback snapshot must contain at least one pain signal');
121
- expect(queue[0].resultRef).toBeFalsy();
122
- expect(mockStartWorkflow).not.toHaveBeenCalled();
123
- } finally {
124
- EvolutionWorkerService.stop({ workspaceDir, stateDir, logger: console } as any);
125
- safeRmDir(workspaceDir);
126
- }
127
- });
128
-
129
- it('uses stub_fallback for expected gateway-only background unavailability', async () => {
130
- const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-nocturnal-gateway-'));
131
- const stateDir = path.join(workspaceDir, '.state');
132
- fs.mkdirSync(path.join(stateDir, 'sessions'), { recursive: true });
133
- fs.mkdirSync(path.join(stateDir, 'logs'), { recursive: true });
134
-
135
- mockGetNocturnalSessionSnapshot.mockReturnValue({
136
- sessionId: 'sleep-gateway',
137
- startedAt: '2026-04-10T00:00:00.000Z',
138
- updatedAt: '2026-04-10T00:01:00.000Z',
139
- assistantTurns: [],
140
- userTurns: [],
141
- toolCalls: [],
142
- painEvents: [],
143
- gateBlocks: [],
144
- stats: {
145
- totalAssistantTurns: 1,
146
- totalToolCalls: 1,
147
- totalPainEvents: 0,
148
- totalGateBlocks: 0,
149
- failureCount: 0,
150
- },
151
- });
152
- mockStartWorkflow.mockResolvedValue({ workflowId: 'wf-1', childSessionKey: 'child-1', state: 'active' });
153
- mockGetWorkflowDebugSummary.mockResolvedValue({
154
- state: 'terminal_error',
155
- metadata: {},
156
- recentEvents: [
157
- {
158
- reason: 'Error: Plugin runtime subagent methods are only available during a gateway request.',
159
- payload: {},
160
- },
161
- ],
162
- });
163
-
164
- EvolutionWorkerService.api = {
165
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
166
- runtime: {},
167
- } as any;
168
-
169
- fs.writeFileSync(
170
- path.join(stateDir, 'evolution_queue.json'),
171
- JSON.stringify([
172
- {
173
- id: 'sleep-gateway',
174
- taskKind: 'sleep_reflection',
175
- priority: 'medium',
176
- score: 50,
177
- source: 'nocturnal',
178
- reason: 'Sleep reflection',
179
- timestamp: '2026-04-10T00:00:00.000Z',
180
- enqueued_at: '2026-04-10T00:00:00.000Z',
181
- status: 'pending',
182
- retryCount: 0,
183
- maxRetries: 1,
184
- recentPainContext: {
185
- mostRecent: { score: 0.5, source: 'pain', reason: 'x', timestamp: '2026-04-10T00:00:00.000Z' },
186
- recentPainCount: 1,
187
- recentMaxPainScore: 0.5,
188
- },
189
- },
190
- ], null, 2),
191
- 'utf8'
192
- );
193
-
194
- try {
195
- EvolutionWorkerService.start({
196
- workspaceDir,
197
- stateDir,
198
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
199
- config: {},
200
- } as any);
201
-
202
- await vi.advanceTimersByTimeAsync(6000);
203
-
204
- const queue = readQueue(stateDir);
205
- // #237: Expected gateway unavailability → stub_fallback (completed), not failed
206
- // This is an environment limitation (daemon mode, cron job, etc.), not a real failure
207
- expect(queue[0].status).toBe('completed');
208
- expect(queue[0].resolution).toBe('stub_fallback');
209
- expect(queue[0].lastError).toContain('gateway request');
210
- } finally {
211
- EvolutionWorkerService.stop({ workspaceDir, stateDir, logger: console } as any);
212
- safeRmDir(workspaceDir);
213
- }
214
- });
215
-
216
- it('uses stub_fallback for expected subagent runtime unavailability', async () => {
217
- const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-nocturnal-runtime-unavailable-'));
218
- const stateDir = path.join(workspaceDir, '.state');
219
- fs.mkdirSync(path.join(stateDir, 'sessions'), { recursive: true });
220
- fs.mkdirSync(path.join(stateDir, 'logs'), { recursive: true });
221
-
222
- mockGetNocturnalSessionSnapshot.mockReturnValue({
223
- sessionId: 'sleep-runtime',
224
- startedAt: '2026-04-10T00:00:00.000Z',
225
- updatedAt: '2026-04-10T00:01:00.000Z',
226
- assistantTurns: [],
227
- userTurns: [],
228
- toolCalls: [],
229
- painEvents: [],
230
- gateBlocks: [],
231
- stats: {
232
- totalAssistantTurns: 1,
233
- totalToolCalls: 1,
234
- totalPainEvents: 0,
235
- totalGateBlocks: 0,
236
- failureCount: 0,
237
- },
238
- });
239
- mockStartWorkflow.mockRejectedValue(new Error('NocturnalWorkflowManager: subagent runtime unavailable'));
240
-
241
- EvolutionWorkerService.api = {
242
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
243
- runtime: {},
244
- } as any;
245
-
246
- fs.writeFileSync(
247
- path.join(stateDir, 'evolution_queue.json'),
248
- JSON.stringify([
249
- {
250
- id: 'sleep-runtime',
251
- taskKind: 'sleep_reflection',
252
- priority: 'medium',
253
- score: 50,
254
- source: 'nocturnal',
255
- reason: 'Sleep reflection',
256
- timestamp: '2026-04-10T00:00:00.000Z',
257
- enqueued_at: '2026-04-10T00:00:00.000Z',
258
- status: 'pending',
259
- retryCount: 0,
260
- maxRetries: 1,
261
- recentPainContext: {
262
- mostRecent: { score: 0.5, source: 'pain', reason: 'x', timestamp: '2026-04-10T00:00:00.000Z', sessionId: 'sleep-runtime' },
263
- recentPainCount: 1,
264
- recentMaxPainScore: 0.5,
265
- },
266
- },
267
- ], null, 2),
268
- 'utf8'
269
- );
270
-
271
- try {
272
- EvolutionWorkerService.start({
273
- workspaceDir,
274
- stateDir,
275
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
276
- config: {},
277
- } as any);
278
-
279
- await vi.advanceTimersByTimeAsync(6000);
280
-
281
- const queue = readQueue(stateDir);
282
- // #237: Expected subagent unavailability → stub_fallback (completed), not failed
283
- // This is an environment limitation (daemon mode, process isolation, etc.), not a real failure
284
- expect(queue[0].status).toBe('completed');
285
- expect(queue[0].resolution).toBe('stub_fallback');
286
- expect(queue[0].lastError).toContain('subagent runtime unavailable');
287
- } finally {
288
- EvolutionWorkerService.stop({ workspaceDir, stateDir, logger: console } as any);
289
- safeRmDir(workspaceDir);
290
- }
291
- });
292
-
293
74
  it('extracts session_id from .pain_flag file correctly', async () => {
294
75
  const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-pain-session-'));
295
76
  const stateDir = path.join(workspaceDir, '.state');
@@ -346,213 +127,6 @@ score: 80`,
346
127
  }
347
128
  });
348
129
 
349
- it('prioritizes pain signal session ID for snapshot extraction', async () => {
350
- const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-pain-priority-'));
351
- const stateDir = path.join(workspaceDir, '.state');
352
- fs.mkdirSync(stateDir, { recursive: true });
353
- fs.mkdirSync(path.join(stateDir, 'sessions'), { recursive: true });
354
- fs.mkdirSync(path.join(stateDir, 'logs'), { recursive: true });
355
-
356
- // Mock extractor to succeed ONLY for the pain session ID
357
- mockGetNocturnalSessionSnapshot.mockImplementation((sessionId: string) => {
358
- if (sessionId === 'pain-session-id') {
359
- return {
360
- sessionId: 'pain-session-id',
361
- startedAt: '2026-04-10T00:00:00.000Z',
362
- updatedAt: '2026-04-10T00:01:00.000Z',
363
- assistantTurns: [],
364
- userTurns: [],
365
- toolCalls: [],
366
- painEvents: [],
367
- gateBlocks: [],
368
- stats: { totalToolCalls: 10, totalAssistantTurns: 5, failureCount: 2 },
369
- stats: {
370
- totalAssistantTurns: 5,
371
- totalToolCalls: 10,
372
- totalPainEvents: 0,
373
- totalGateBlocks: 0,
374
- failureCount: 2,
375
- },
376
- };
377
- }
378
- return null;
379
- });
380
-
381
- mockStartWorkflow.mockResolvedValue({ workflowId: 'wf-1', childSessionKey: 'child-1', state: 'active' });
382
- mockGetWorkflowDebugSummary.mockResolvedValue({ state: 'active', metadata: {} });
383
-
384
- EvolutionWorkerService.api = {
385
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
386
- runtime: {},
387
- } as any;
388
-
389
- // Create a queue with a task that HAS a pain session ID
390
- const taskWithPainSession = {
391
- id: 'task-with-pain',
392
- taskKind: 'sleep_reflection',
393
- priority: 'medium',
394
- score: 50,
395
- source: 'nocturnal',
396
- reason: 'Sleep reflection',
397
- timestamp: '2026-04-10T00:00:00.000Z',
398
- enqueued_at: '2026-04-10T00:00:00.000Z',
399
- status: 'pending',
400
- retryCount: 0,
401
- maxRetries: 1,
402
- recentPainContext: {
403
- mostRecent: { sessionId: 'pain-session-id', score: 80, source: 'test', reason: 'r', timestamp: 't' },
404
- recentPainCount: 1,
405
- recentMaxPainScore: 80,
406
- },
407
- };
408
-
409
- fs.writeFileSync(
410
- path.join(stateDir, 'evolution_queue.json'),
411
- JSON.stringify([taskWithPainSession]),
412
- 'utf8'
413
- );
414
-
415
- try {
416
- EvolutionWorkerService.start({
417
- workspaceDir,
418
- stateDir,
419
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
420
- config: { get: () => 15000 },
421
- } as any);
422
-
423
- // Advance time to process the pending task
424
- await vi.advanceTimersByTimeAsync(6000);
425
-
426
- // Verify the extractor was called with the pain session ID first
427
- expect(mockGetNocturnalSessionSnapshot).toHaveBeenCalledWith('pain-session-id');
428
-
429
- // Verify workflow started (meaning snapshot was found via pain session ID)
430
- expect(mockStartWorkflow).toHaveBeenCalled();
431
- const workflowStartInput = mockStartWorkflow.mock.calls[0][1];
432
- expect(workflowStartInput.metadata.snapshot.startedAt).toBe('2026-04-10T00:00:00.000Z');
433
- expect(Array.isArray(workflowStartInput.metadata.snapshot.assistantTurns)).toBe(true);
434
- expect(Array.isArray(workflowStartInput.metadata.snapshot.toolCalls)).toBe(true);
435
-
436
- // Verify task status updated
437
- const queue = readQueue(stateDir);
438
- expect(queue[0].status).toBe('in_progress');
439
- expect(queue[0].resultRef).toBe('wf-1');
440
-
441
- } finally {
442
- EvolutionWorkerService.stop({ workspaceDir, stateDir, logger: console } as any);
443
- safeRmDir(workspaceDir);
444
- }
445
- });
446
-
447
- it('does not select fallback sessions newer than the triggering task timestamp', async () => {
448
- const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-nocturnal-bounded-'));
449
- const stateDir = path.join(workspaceDir, '.state');
450
- fs.mkdirSync(stateDir, { recursive: true });
451
- fs.mkdirSync(path.join(stateDir, 'sessions'), { recursive: true });
452
- fs.mkdirSync(path.join(stateDir, 'logs'), { recursive: true });
453
-
454
- mockGetNocturnalSessionSnapshot.mockImplementation((sessionId: string) => {
455
- if (sessionId === 'older-session') {
456
- return {
457
- sessionId: 'older-session',
458
- startedAt: '2026-04-09T23:00:00.000Z',
459
- updatedAt: '2026-04-09T23:10:00.000Z',
460
- assistantTurns: [],
461
- userTurns: [],
462
- toolCalls: [],
463
- painEvents: [],
464
- gateBlocks: [],
465
- stats: {
466
- totalAssistantTurns: 1,
467
- totalToolCalls: 1,
468
- totalPainEvents: 0,
469
- totalGateBlocks: 0,
470
- failureCount: 1,
471
- },
472
- };
473
- }
474
- return null;
475
- });
476
- mockListRecentNocturnalCandidateSessions.mockReturnValue([
477
- {
478
- sessionId: 'newer-session',
479
- startedAt: '2026-04-10T01:00:00.000Z',
480
- updatedAt: '2026-04-10T01:10:00.000Z',
481
- assistantTurnCount: 1,
482
- toolCallCount: 2,
483
- painEventCount: 1,
484
- gateBlockCount: 0,
485
- failureCount: 1,
486
- },
487
- {
488
- sessionId: 'older-session',
489
- startedAt: '2026-04-09T23:00:00.000Z',
490
- updatedAt: '2026-04-09T23:10:00.000Z',
491
- assistantTurnCount: 1,
492
- toolCallCount: 2,
493
- painEventCount: 1,
494
- gateBlockCount: 0,
495
- failureCount: 1,
496
- },
497
- ]);
498
- mockStartWorkflow.mockResolvedValue({ workflowId: 'wf-bounded', childSessionKey: 'child-bounded', state: 'active' });
499
- mockGetWorkflowDebugSummary.mockResolvedValue({ state: 'active', metadata: {} });
500
-
501
- EvolutionWorkerService.api = {
502
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
503
- runtime: {},
504
- } as any;
505
-
506
- fs.writeFileSync(
507
- path.join(stateDir, 'evolution_queue.json'),
508
- JSON.stringify([
509
- {
510
- id: 'sleep-bounded',
511
- taskKind: 'sleep_reflection',
512
- priority: 'medium',
513
- score: 50,
514
- source: 'nocturnal',
515
- reason: 'Sleep reflection',
516
- timestamp: '2026-04-10T00:00:00.000Z',
517
- enqueued_at: '2026-04-10T00:00:00.000Z',
518
- status: 'pending',
519
- retryCount: 0,
520
- maxRetries: 1,
521
- recentPainContext: {
522
- mostRecent: null,
523
- recentPainCount: 0,
524
- recentMaxPainScore: 0,
525
- },
526
- },
527
- ], null, 2),
528
- 'utf8'
529
- );
530
-
531
- try {
532
- EvolutionWorkerService.start({
533
- workspaceDir,
534
- stateDir,
535
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
536
- config: { get: () => 15000 },
537
- } as any);
538
-
539
- await vi.advanceTimersByTimeAsync(6000);
540
-
541
- expect(mockListRecentNocturnalCandidateSessions).toHaveBeenCalledWith(
542
- expect.objectContaining({
543
- limit: 20,
544
- minToolCalls: 1,
545
- dateTo: '2026-04-10T00:00:00.000Z',
546
- })
547
- );
548
- expect(mockGetNocturnalSessionSnapshot).not.toHaveBeenCalledWith('newer-session');
549
- expect(mockGetNocturnalSessionSnapshot).toHaveBeenCalledWith('older-session');
550
- } finally {
551
- EvolutionWorkerService.stop({ workspaceDir, stateDir, logger: console } as any);
552
- safeRmDir(workspaceDir);
553
- }
554
- });
555
-
556
130
  // === End-to-End Contract Tests ===
557
131
 
558
132
  it('e2e: pain flag → worker enqueue → session_id is correctly attached to queued task', async () => {
@@ -640,125 +214,4 @@ session_id: pain-session-abc
640
214
  safeRmDir(workspaceDir);
641
215
  }
642
216
  });
643
-
644
- it('e2e: bounded session selection — never picks a session newer than the triggering task', async () => {
645
- const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-e2e-bounded-'));
646
- const stateDir = path.join(workspaceDir, '.state');
647
- fs.mkdirSync(stateDir, { recursive: true });
648
- fs.mkdirSync(path.join(stateDir, 'sessions'), { recursive: true });
649
- fs.mkdirSync(path.join(stateDir, 'logs'), { recursive: true });
650
-
651
- // Mock: only the older session snapshot is available
652
- mockGetNocturnalSessionSnapshot.mockImplementation((sessionId: string) => {
653
- if (sessionId === 'older-session-bounded') {
654
- return {
655
- sessionId: 'older-session-bounded',
656
- startedAt: '2026-04-09T23:00:00.000Z',
657
- updatedAt: '2026-04-09T23:10:00.000Z',
658
- assistantTurns: [],
659
- userTurns: [],
660
- toolCalls: [],
661
- painEvents: [{ source: 'tool_failure', score: 60 }],
662
- gateBlocks: [],
663
- stats: {
664
- totalAssistantTurns: 3,
665
- totalToolCalls: 5,
666
- totalPainEvents: 1,
667
- totalGateBlocks: 0,
668
- failureCount: 1,
669
- },
670
- };
671
- }
672
- return null;
673
- });
674
-
675
- // Candidate sessions — one newer, one older than the task trigger time
676
- mockListRecentNocturnalCandidateSessions.mockReturnValue([
677
- {
678
- sessionId: 'newer-unrelated',
679
- startedAt: '2026-04-10T01:00:00.000Z',
680
- updatedAt: '2026-04-10T01:10:00.000Z',
681
- assistantTurnCount: 2,
682
- toolCallCount: 5,
683
- painEventCount: 2,
684
- gateBlockCount: 0,
685
- failureCount: 1,
686
- },
687
- {
688
- sessionId: 'older-session-bounded',
689
- startedAt: '2026-04-09T23:00:00.000Z',
690
- updatedAt: '2026-04-09T23:10:00.000Z',
691
- assistantTurnCount: 3,
692
- toolCallCount: 5,
693
- painEventCount: 1,
694
- gateBlockCount: 0,
695
- failureCount: 1,
696
- },
697
- ]);
698
-
699
- mockStartWorkflow.mockResolvedValue({ workflowId: 'wf-bounded', childSessionKey: 'child-bounded', state: 'active' });
700
- mockGetWorkflowDebugSummary.mockResolvedValue({ state: 'active', metadata: {} });
701
-
702
- EvolutionWorkerService.api = {
703
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
704
- runtime: {},
705
- } as any;
706
-
707
- // Task triggered at 2026-04-10T00:00:00.000Z — no pain session ID, so it falls back to candidate selection
708
- fs.writeFileSync(
709
- path.join(stateDir, 'evolution_queue.json'),
710
- JSON.stringify([
711
- {
712
- id: 'sleep-bounded',
713
- taskKind: 'sleep_reflection',
714
- priority: 'medium',
715
- score: 50,
716
- source: 'nocturnal',
717
- reason: 'Sleep reflection',
718
- timestamp: '2026-04-10T00:00:00.000Z',
719
- enqueued_at: '2026-04-10T00:00:00.000Z',
720
- status: 'pending',
721
- retryCount: 0,
722
- maxRetries: 1,
723
- recentPainContext: {
724
- mostRecent: null,
725
- recentPainCount: 0,
726
- recentMaxPainScore: 0,
727
- },
728
- },
729
- ], null, 2),
730
- 'utf8'
731
- );
732
-
733
- try {
734
- EvolutionWorkerService.start({
735
- workspaceDir,
736
- stateDir,
737
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
738
- config: { get: () => 15000 },
739
- } as any);
740
-
741
- await vi.advanceTimersByTimeAsync(6000);
742
-
743
- // Verify dateTo boundary was passed
744
- expect(mockListRecentNocturnalCandidateSessions).toHaveBeenCalledWith(
745
- expect.objectContaining({
746
- limit: 20,
747
- minToolCalls: 1,
748
- dateTo: '2026-04-10T00:00:00.000Z',
749
- })
750
- );
751
-
752
- // Should NOT query the newer session
753
- expect(mockGetNocturnalSessionSnapshot).not.toHaveBeenCalledWith('newer-unrelated');
754
- // Should use the older session that passes the time boundary
755
- expect(mockGetNocturnalSessionSnapshot).toHaveBeenCalledWith('older-session-bounded');
756
-
757
- // Workflow should have started
758
- expect(mockStartWorkflow).toHaveBeenCalled();
759
- } finally {
760
- EvolutionWorkerService.stop({ workspaceDir, stateDir, logger: console } as any);
761
- safeRmDir(workspaceDir);
762
- }
763
- });
764
217
  });