@@ -408,6 +408,194 @@ describe('ClaudeCodeAdapter', () => {
408408 fs . rmSync ( tmpDir , { recursive : true , force : true } ) ;
409409 } ) ;
410410
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+
411599 it ( 'should fall back to process-only when direct-matched JSONL becomes unreadable' , async ( ) => {
412600 const startTime = new Date ( ) ;
413601 const processes : ProcessInfo [ ] = [
0 commit comments