@ai-devkit/agent-manager 0.12.0 → 0.13.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.
@@ -408,6 +408,194 @@ describe('ClaudeCodeAdapter', () => {
408
408
  fs.rmSync(tmpDir, { recursive: true, force: true });
409
409
  });
410
410
 
411
+ it('should prefer PID-file live status over JSONL-derived status and surface waitingFor in summary', async () => {
412
+ const startTime = new Date();
413
+ const processes: ProcessInfo[] = [
414
+ { pid: 55050, command: 'claude', cwd: '/project/wait', tty: 'ttys001', startTime },
415
+ ];
416
+ mockedListAgentProcesses.mockReturnValue(processes);
417
+ mockedEnrichProcesses.mockReturnValue(processes);
418
+
419
+ const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'claude-pid-wait-'));
420
+ const sessionsDir = path.join(tmpDir, 'sessions');
421
+ const projectsDir = path.join(tmpDir, 'projects');
422
+ const projDir = path.join(projectsDir, '-project-wait');
423
+ fs.mkdirSync(sessionsDir, { recursive: true });
424
+ fs.mkdirSync(projDir, { recursive: true });
425
+
426
+ const sessionId = 'wait-session';
427
+ const jsonlPath = path.join(projDir, `${sessionId}.jsonl`);
428
+ // JSONL trails with permission-mode → parser would resolve to UNKNOWN.
429
+ // PID file's live status must win.
430
+ fs.writeFileSync(jsonlPath, [
431
+ JSON.stringify({ type: 'user', timestamp: new Date().toISOString(), cwd: '/project/wait', message: { content: '/reddit-commenter' } }),
432
+ JSON.stringify({ type: 'permission-mode', timestamp: new Date().toISOString(), permissionMode: 'default' }),
433
+ ].join('\n'));
434
+
435
+ fs.writeFileSync(
436
+ path.join(sessionsDir, '55050.json'),
437
+ JSON.stringify({
438
+ pid: 55050, sessionId, cwd: '/project/wait',
439
+ startedAt: startTime.getTime(),
440
+ kind: 'interactive', entrypoint: 'cli',
441
+ status: 'waiting', waitingFor: 'approve Read',
442
+ }),
443
+ );
444
+
445
+ (adapter as any).sessionsDir = sessionsDir;
446
+ (adapter as any).projectsDir = projectsDir;
447
+
448
+ const agents = await adapter.detectAgents();
449
+
450
+ expect(agents).toHaveLength(1);
451
+ expect(agents[0].status).toBe(AgentStatus.WAITING);
452
+ expect(agents[0].summary).toContain('/reddit-commenter');
453
+ expect(agents[0].summary).toContain('waiting for approve Read');
454
+
455
+ fs.rmSync(tmpDir, { recursive: true, force: true });
456
+ });
457
+
458
+ it('should resolve PID-file status: "idle" to AgentStatus.IDLE even when JSONL would say UNKNOWN', async () => {
459
+ const startTime = new Date();
460
+ const processes: ProcessInfo[] = [
461
+ { pid: 55051, command: 'claude', cwd: '/project/idle', tty: 'ttys001', startTime },
462
+ ];
463
+ mockedListAgentProcesses.mockReturnValue(processes);
464
+ mockedEnrichProcesses.mockReturnValue(processes);
465
+
466
+ const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'claude-pid-idle-'));
467
+ const sessionsDir = path.join(tmpDir, 'sessions');
468
+ const projectsDir = path.join(tmpDir, 'projects');
469
+ const projDir = path.join(projectsDir, '-project-idle');
470
+ fs.mkdirSync(sessionsDir, { recursive: true });
471
+ fs.mkdirSync(projDir, { recursive: true });
472
+
473
+ const sessionId = 'idle-session';
474
+ const jsonlPath = path.join(projDir, `${sessionId}.jsonl`);
475
+ fs.writeFileSync(jsonlPath, [
476
+ JSON.stringify({ type: 'user', timestamp: new Date().toISOString(), cwd: '/project/idle', message: { content: 'hello' } }),
477
+ JSON.stringify({ type: 'permission-mode', timestamp: new Date().toISOString(), permissionMode: 'default' }),
478
+ ].join('\n'));
479
+
480
+ fs.writeFileSync(
481
+ path.join(sessionsDir, '55051.json'),
482
+ JSON.stringify({
483
+ pid: 55051, sessionId, cwd: '/project/idle',
484
+ startedAt: startTime.getTime(),
485
+ kind: 'interactive', entrypoint: 'cli',
486
+ status: 'idle',
487
+ }),
488
+ );
489
+
490
+ (adapter as any).sessionsDir = sessionsDir;
491
+ (adapter as any).projectsDir = projectsDir;
492
+
493
+ const agents = await adapter.detectAgents();
494
+
495
+ expect(agents).toHaveLength(1);
496
+ expect(agents[0].status).toBe(AgentStatus.IDLE);
497
+ // No waitingFor in PID file → summary is just the last user message
498
+ expect(agents[0].summary).toBe('hello');
499
+
500
+ fs.rmSync(tmpDir, { recursive: true, force: true });
501
+ });
502
+
503
+ it('should fall back to JSONL-derived status when PID-file status is missing or unrecognized', async () => {
504
+ const startTime = new Date();
505
+ const processes: ProcessInfo[] = [
506
+ { pid: 55052, command: 'claude', cwd: '/project/legacy', tty: 'ttys001', startTime },
507
+ ];
508
+ mockedListAgentProcesses.mockReturnValue(processes);
509
+ mockedEnrichProcesses.mockReturnValue(processes);
510
+
511
+ const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'claude-pid-nostat-'));
512
+ const sessionsDir = path.join(tmpDir, 'sessions');
513
+ const projectsDir = path.join(tmpDir, 'projects');
514
+ const projDir = path.join(projectsDir, '-project-legacy');
515
+ fs.mkdirSync(sessionsDir, { recursive: true });
516
+ fs.mkdirSync(projDir, { recursive: true });
517
+
518
+ const sessionId = 'legacy-session';
519
+ const jsonlPath = path.join(projDir, `${sessionId}.jsonl`);
520
+ // Last entry is assistant → JSONL parser yields WAITING.
521
+ fs.writeFileSync(jsonlPath, [
522
+ JSON.stringify({ type: 'user', timestamp: new Date().toISOString(), cwd: '/project/legacy', message: { content: 'do the thing' } }),
523
+ JSON.stringify({ type: 'assistant', timestamp: new Date().toISOString() }),
524
+ ].join('\n'));
525
+
526
+ // PID file with unrecognized status string — adapter must ignore it and use parser
527
+ fs.writeFileSync(
528
+ path.join(sessionsDir, '55052.json'),
529
+ JSON.stringify({
530
+ pid: 55052, sessionId, cwd: '/project/legacy',
531
+ startedAt: startTime.getTime(),
532
+ kind: 'interactive', entrypoint: 'cli',
533
+ status: 'fantastical-future-state',
534
+ }),
535
+ );
536
+
537
+ (adapter as any).sessionsDir = sessionsDir;
538
+ (adapter as any).projectsDir = projectsDir;
539
+
540
+ const agents = await adapter.detectAgents();
541
+
542
+ expect(agents).toHaveLength(1);
543
+ expect(agents[0].status).toBe(AgentStatus.WAITING);
544
+
545
+ fs.rmSync(tmpDir, { recursive: true, force: true });
546
+ });
547
+
548
+ it('should pick up live status from PID file even when matched via --resume', async () => {
549
+ const sessionId = 'aaaaaaaa-1111-2222-3333-444444444444';
550
+ const startTime = new Date();
551
+ const processes: ProcessInfo[] = [
552
+ {
553
+ pid: 55053,
554
+ command: `claude --resume ${sessionId}`,
555
+ cwd: '/project/resume-wait',
556
+ tty: 'ttys001',
557
+ startTime,
558
+ },
559
+ ];
560
+ mockedListAgentProcesses.mockReturnValue(processes);
561
+ mockedEnrichProcesses.mockReturnValue(processes);
562
+
563
+ const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'claude-resume-wait-'));
564
+ const sessionsDir = path.join(tmpDir, 'sessions');
565
+ const projectsDir = path.join(tmpDir, 'projects');
566
+ const projDir = path.join(projectsDir, '-project-resume-wait');
567
+ fs.mkdirSync(sessionsDir, { recursive: true });
568
+ fs.mkdirSync(projDir, { recursive: true });
569
+
570
+ const jsonlPath = path.join(projDir, `${sessionId}.jsonl`);
571
+ fs.writeFileSync(jsonlPath, [
572
+ JSON.stringify({ type: 'user', timestamp: new Date().toISOString(), cwd: '/project/resume-wait', message: { content: 'resumed work' } }),
573
+ JSON.stringify({ type: 'permission-mode', timestamp: new Date().toISOString(), permissionMode: 'default' }),
574
+ ].join('\n'));
575
+
576
+ fs.writeFileSync(
577
+ path.join(sessionsDir, '55053.json'),
578
+ JSON.stringify({
579
+ pid: 55053, sessionId, cwd: '/project/resume-wait',
580
+ startedAt: startTime.getTime(),
581
+ kind: 'interactive', entrypoint: 'cli',
582
+ status: 'waiting', waitingFor: 'approve Bash',
583
+ }),
584
+ );
585
+
586
+ (adapter as any).sessionsDir = sessionsDir;
587
+ (adapter as any).projectsDir = projectsDir;
588
+
589
+ const agents = await adapter.detectAgents();
590
+
591
+ expect(agents).toHaveLength(1);
592
+ expect(agents[0].status).toBe(AgentStatus.WAITING);
593
+ expect(agents[0].summary).toContain('resumed work');
594
+ expect(agents[0].summary).toContain('waiting for approve Bash');
595
+
596
+ fs.rmSync(tmpDir, { recursive: true, force: true });
597
+ });
598
+
411
599
  it('should fall back to process-only when direct-matched JSONL becomes unreadable', async () => {
412
600
  const startTime = new Date();
413
601
  const processes: ProcessInfo[] = [
@@ -914,7 +1102,7 @@ describe('ClaudeCodeAdapter', () => {
914
1102
  };
915
1103
 
916
1104
  const writeJsonl = (cwd: string, sessionId: string) => {
917
- const encoded = cwd.replace(/\//g, '-');
1105
+ const encoded = cwd.replace(/[^a-zA-Z0-9]/g, '-');
918
1106
  const projDir = path.join(projectsDir, encoded);
919
1107
  fs.mkdirSync(projDir, { recursive: true });
920
1108
  const filePath = path.join(projDir, `${sessionId}.jsonl`);
@@ -1049,6 +1237,44 @@ describe('ClaudeCodeAdapter', () => {
1049
1237
  });
1050
1238
  });
1051
1239
 
1240
+ describe('getProjectDir', () => {
1241
+ const encode = (cwd: string) => (adapter as any).getProjectDir(cwd) as string;
1242
+
1243
+ it('should replace path separators with hyphens', () => {
1244
+ const expected = path.join((adapter as any).projectsDir, '-Users-foo-bar');
1245
+ expect(encode('/Users/foo/bar')).toBe(expected);
1246
+ });
1247
+
1248
+ it('should encode underscores as hyphens (matches Claude Code CLI)', () => {
1249
+ const expected = path.join((adapter as any).projectsDir, '-Users-foo-my-project');
1250
+ expect(encode('/Users/foo/my_project')).toBe(expected);
1251
+ });
1252
+
1253
+ it('should encode dots as hyphens', () => {
1254
+ const expected = path.join((adapter as any).projectsDir, '-Users-foo--worktrees-x');
1255
+ expect(encode('/Users/foo/.worktrees/x')).toBe(expected);
1256
+ });
1257
+
1258
+ it('should collide paths that differ only in non-alphanumeric chars', () => {
1259
+ // The encoding is intentionally lossy — callers must
1260
+ // disambiguate via session JSONL contents, not dir name.
1261
+ expect(encode('/a/b_c')).toBe(encode('/a/b-c'));
1262
+ expect(encode('/a/b_c')).toBe(encode('/a/b.c'));
1263
+ });
1264
+
1265
+ it('should resolve to a real session dir when cwd contains underscores', () => {
1266
+ const cwd = '/Users/foo/my_project';
1267
+ const projectsDir = (adapter as any).projectsDir as string;
1268
+ const expectedDir = path.join(projectsDir, '-Users-foo-my-project');
1269
+ fs.mkdirSync(expectedDir, { recursive: true });
1270
+ const sessionFile = path.join(expectedDir, 'session-underscore.jsonl');
1271
+ fs.writeFileSync(sessionFile, '');
1272
+
1273
+ expect(encode(cwd)).toBe(expectedDir);
1274
+ expect(fs.existsSync(path.join(encode(cwd), 'session-underscore.jsonl'))).toBe(true);
1275
+ });
1276
+ });
1277
+
1052
1278
  describe('readSession', () => {
1053
1279
  it('should parse session file with timestamps, cwd, and entry type', () => {
1054
1280
  const readSession = (adapter as any).parser.readSession.bind((adapter as any).parser);