
    _i9                        d Z ddlZddlZddlZddlZddlZddlZddlZddlZddl	m	Z	m
Z
mZ ddlmZ ddlZej                  j!                  dd      Z e ee      j)                         j*                  j*                        Zeej.                  vrej.                  j1                  de       ddlmZ  ee d      Z ee d	      Z ee d
      ZdZdZ e e
d            Z deddfdZ!dede"fdZ#de$fdZ%de$de"fdZ&dededz  fdZ'dede"fdZ(de)e   fdZ*dede+e"e)e   f   fdZ,de-fdZ.de)e   fdZ/de-fdZ0de-fdZ1d$d Z2d$d!Z3d$d"Z4e5d#k(  r e4        yy)%u;  .done 파일 + .failed 파일 감시 → bot-activity idle 전환 + FAIL 알림

done-watcher는 memory/events/*.done 파일을 감시하여
해당 팀의 bot-activity.json 상태를 idle로 전환합니다.

또한 memory/events/*.failed 파일을 감시하여
아누(개발실장)에게 QC FAIL 알림을 Telegram으로 전송합니다.

이는 finish-task.sh가 호출되지 않은 경우를 대비한 방어 코드입니다.

Usage:
    python3 scripts/done-watcher.py          # 1회 실행
    python3 scripts/done-watcher.py --daemon # 데몬 모드 (30초마다)
    N)datetime	timedeltatimezone)PathWORKSPACE_ROOTz/home/jay/workspace)is_valid_task_idz/memory/eventsz /memory/events/bot-activity.jsonz/logs/done-protocol.log   i,  	   )hoursmessagereturnc                 6   t        j                  t              j                         }d| d|  d}	 t        j
                  j                  dd       t        t        dd      5 }|j                  |       d	d	d	       y	# 1 sw Y   y	xY w# t        $ r Y y	w xY w)
u   done-protocol.log에 기록[z] [done-watcher] 
T)parentsexist_okautf-8encodingN)
r   nowKST	isoformatDONE_PROTOCOL_LOGparentmkdiropenwriteOSError)r   tslinefs       G/home/jay/workspace/.worktrees/task-2487-B-dev2/scripts/done-watcher.pylog_protocolr$   .   s    	c		$	$	&Brd#G9B/D  &&td&C#S7; 	qGGDM	 	 	 s/   3B %B 7B  B	B 	B 	BBc                    t         j                  j                  dd      }t         j                  j                  dd      }|st        d       yd| d}d	}t	        d
|d
z         D ]P  }|| dd}	 t        j                  ||d      }|j                  dk(  r y|j                  dk(  rdt        |j                         j                  di       j                  dd            }t        d| d| d| d       t        j                  |       |j                  dk(  rU|| d}	t        j                  ||	d      }
|
j                  dk(  r yt        d|
j                   d|
j                           yt        d|j                   d|j                   d| d| d	       ||k  rt        j                  d       S y# t
        j                  $ r:}t        d | d| d| d       ||k  rt        j                  d       Y d!}~d!}~ww xY w)"u   아누(개발실장)에게 Telegram 알림 전송

    notify-completion.py의 동일 패턴 차용.
    3회 재시도 + 429 rate limit 대응.
    Markdown parse_mode 사용 + 400 에러 시 plain text fallback.
    ANU_BOT_TOKEN COKACDIR_CHAT_ID
6937032012uE   WARN: ANU_BOT_TOKEN 환경변수 미설정 — Telegram 알림 생략Fzhttps://api.telegram.org/botz/sendMessage      Markdown)chat_idtext
parse_mode
   )jsontimeout   Ti  
parametersretry_after   u"   WARN: Telegram 429 rate limit — u   초 대기 (시도 /)i  )r-   r.   u,   ERROR: Telegram plain text fallback 실패:  u   ERROR: Telegram 전송 실패: u	    (시도    u   ERROR: Telegram 요청 예외: N)osenvirongetr$   rangerequestspoststatus_codeintr1   timesleepr.   RequestException)r   	bot_tokenr-   urlmax_retriesattemptpayloadrespr5   plain_payloadresp2es               r#   send_telegram_notificationrO   :   s    

3Ijjnn/>G\](<
@CKK!O, # $

	==7B?D3&!!S(!$))+//,"C"G"GWX"YZA+Nabiajjklwkxxyz{

;'!!S(  '#! !crJ$$+KEL]L]K^^_`e`j`j_klm>t?O?O>PPQRVR[R[Q\\efmennop{o||}~[(JJqM?#J  (( 	:1#YwiqQ\P]]^_`$

1	s2   1'F;A2F;;F;
%F;1AF;;H/HHc                     	 t         j                         sdi iS t        t         dd      5 } t        j                  |       cddd       S # 1 sw Y   yxY w# t        j
                  t        f$ r}t        d|        di icY d}~S d}~ww xY w)u   bot-activity.json 로드botsrr   r   Nu(   ERROR: bot-activity.json 로드 실패: )BOT_ACTIVITY_FILEexistsr   r1   loadJSONDecodeErrorr   r$   )r"   rN   s     r#   load_bot_activityrW   s   s     '')B<#S7; 	 q99Q<	  	  	   '* ?sCD|s?   A A A	A AA A B0BBBdatac                    	 t         j                  d      }t        |dd      5 }t        j                  | |dd       ddd       |j                  t                y	# 1 sw Y   xY w# t        $ r}t        d
|        Y d}~yd}~ww xY w)u+   bot-activity.json 저장 (원자적 쓰기)z.tmpwr   r   r:   F)indentensure_asciiNTu(   ERROR: bot-activity.json 저장 실패: )rS   with_suffixr   r1   dumpreplacer   r$   )rX   	temp_filer"   rN   s       r#   save_bot_activityra      s    %11&9	)S73 	=qIIdAae<	=+,	= 	=  ?sCDs-   #A) AA) A&"A) )	B
2BB
	done_filec                    | j                   }t        j                  d|      }|r|j                  d      S | j                  j                  dd      }	 t        t         d      }|j                         rXt        j                  |j                  d            }|j                  di       j                  |i       }|j                  d	      S 	 y
# t        $ r Y y
w xY w)u   .done 파일명에서 팀 ID 추출

    패턴:
    - task-648.1.dev1.done → dev1
    - task-648.1.dev2.done → dev2
    - task-648.1.dev3.done → dev3
    - task-648.1.done → task-timers.json에서 조회 필요
    zCtask-\d+(?:\.\d+)?(?:_\d+\.\d+)?(?:_[a-z])?(?:\+\d+)?\.(\w+)\.done$r+   .doner'   z/memory/task-timers.jsonr   r   tasksteam_idN)namerematchgroupstemr_   r   r   rT   r1   loads	read_textr=   	Exception)rb   rg   ri   task_id
timer_filerX   	task_datas          r#   extract_team_from_done_filerr      s     >>DHH[]abE{{1~ nn$$Wb1G^,,DEF
::j22G2DED"-11'2>I==++    s   A9C 	CCrf   c                 Z   t               }|j                  di       }| |vrt        d|  d       y||    j                  d      dk(  ryt        j                  t
        j                        j                  d      }d|d   |    d<   ||d   |    d	<   t        |      rt        d
|  d       yy)u   봇 상태를 idle로 전환rQ   zWARN: team_id 'u    '가 bot-activity.json에 없음FstatusidleTz%Y-%m-%dT%H:%M:%SZsince[DONE-WATCHER] u   : → idle (.done 감지))	rW   r=   r$   r   r   r   utcstrftimera   )rf   rX   rQ   utc_nows       r#   set_bot_idler{      s    D88FBDdwi/OPQ G}"f, ll8<<(112FGG&,DL(#%,DL'"wi/HIJ    c                      g d} g }t         j                  d      D ]8  j                  dk7  rt        fd| D              r(|j	                         : |S )u   처리되지 않은 .done 파일 스캔

    .done 파일 중 .done.acked, .done.clear 등으로 처리된 파일은 제외
    ).ackedz.clearz.mergingz
.escalated*.donerd   c              3   f   K   | ](  }t         j                  |z   z  j                          * y w)N)
EVENTS_DIRrg   rT   ).0extrb   s     r#   	<genexpr>z"scan_done_files.<locals>.<genexpr>   s(     X#
inns23;;=Xs   .1)r   globsuffixanyappend)processed_exts
done_filesrb   s     @r#   scan_done_filesr      s`    
 DNJ__X. %	w&XXX)$% r|   c                    g }	 t        j                  | j                  d            }ddg}|D ]  }||vs|j                  d|         d	|v r|d	   }|j                         D ci c]  \  }}|d	k7  s|| }	}}t        j                  |	d
d      }
t        j                  |
j                  d            j                         }||k7  r0|j                  d|dd  d|dd  d       n|j                  d       t        |D cg c]  }d|v sd|v s| c}      dk(  }||fS # t         j                  t        f$ r}dd| gfcY d}~S d}~ww xY wc c}}w c c}w )u2   V-3/V-7: .done 파일 무결성 + 스키마 검증r   r   Fu   JSON 파싱 실패: Nro   rt   u   필수 키 누락: qc_hashT)	sort_keysr\   u5   QC 해시 불일치 — 수동 생성 의심: stored=   z... computed=z...uG   qc_hash 필드 없음 — QC 게이트 미사용 또는 레거시 .doneu	   불일치u
   필수 키r   )r1   rl   rm   rV   r   r   itemsdumpshashlibsha256encode	hexdigestlen)rb   warningsrX   rN   required_keyskeystored_hashkvhash_payloadhash_strcomputed_hashrZ   is_valids                 r#   validate_done_filer      s   H3zz)--w-?@
 )M 9d?OO1#789
 D9o)-HAi1HH::ldOxw'?@JJL-'OOST_`cacTdSeer  tA  BE  CE  tF  sG  GJ  K  LabxQ!;!+;|q?PAQRVWWHX+   '* 3-aS12223 I Rs5   %D' )E7EEE'E EEEc                      t               } d}| D ]l  }t        |      \  }}|D ]  }t        d|j                   d|         |st        d|j                   d       t	        |      }|s\t        |      sh|dz  }n |S )uW   .done 파일 처리 → bot idle 전환

    Returns:
        처리된 파일 수
    r   zWARN [z]: zINTEGRITY_FAIL [u   ]: 무결성 검증 실패r+   )r   r   r$   rg   rr   r{   )r   	processedrb   r   r   rZ   rf   s          r#   process_done_filesr      s     !"JI 	/	:( 	:A6)..!1QC89	:+INN+;;UVW-i8G$Q	 r|   c                      g } t         j                  d      D ]H  }|j                  dk7  rt         |j                  dz   z  j	                         r8| j                  |       J | S )u   처리되지 않은 .failed 파일 스캔

    .failed.acked 마커 파일이 이미 존재하면 제외 (중복 알림 방지)
    z*.failedz.failedr~   )r   r   r   rg   rT   r   )failed_filesfailed_files     r#   scan_failed_filesr     sg    
 L!z2 )*+**X56>>@K() r|   c                     t               } d}| D ]  }	 t        j                  |j                  d            }|j                  d|j                        }|j                  dd	      }d
| d| }t        d|j                   d       t        |      rRt        |j                  dz   z  }|j                  |       t        d|j                   d|j                          |dz  }t        d|j                   d        |S # t        j                  t
        f$ r&}t        d|j                   d|        Y d}~,d}~ww xY w)ul   .failed 파일 처리 → 아누에게 QC FAIL 알림 전송

    Returns:
        처리된 파일 수
    r   r   r   zERROR [u   ]: JSON 파싱 실패: Nro   fail_reasonzQC FAILz
[QC FAIL] u    — z[FAILED-WATCHER] u   : Telegram 알림 전송 시도r~   u   : 알림 전송 완료 → r+   u5   : 알림 전송 실패 — 다음 주기에 재시도)r   r1   rl   rm   rV   r   r$   rg   r=   rk   rO   r   rename)	r   r   r   rX   rN   ro   r   r   
acked_paths	            r#   process_failed_filesr     sP    %&LI# v	::k33W3EFD
 ((9k&6&67hh}i8wiu[M:()9)9(::YZ[%g.#{'7'7('BCJz*,[-=-=,>>YZdZiZiYjklNI,[-=-=,>>stu'v* % $$g. 	7;#3#3"44KA3OP	s   %DE D;;E c                     t         j                  j                  dd      dk(  ryd} t        j                         }t        j                  d      D ]=  }|j                  dk7  r|j                  }t        |      s,t        | dz  }|j                         rI	 |j                         j                  }||z
  t        k  rqt        d| d	t        ||z
         d
       t         j"                  t$         dd|g}	 t'        j(                  |ddd      }|j*                  dk(  r2| dz  } t        d| d|j,                  j/                         dd         n9t        d| d|j*                   d|j0                  j/                         dd         @ | S # t        $ r Y Ow xY w# t&        j2                  t        f$ r}t        d| d|        Y d}~d}~ww xY w)u   finish-task.sh 누락 폴백: .done이 5분 이상 묵었는데
    .anu-notified 마커가 없으면 extract_followup.py send 실행.

    Returns:
        폴백 발사된 task 수
    DISABLE_ANU_FOLLOWUP01r   r   rd   z.anu-notifiedz[ANU-FOLLOWUP] u&   : finish-task.sh 누락 의심 (.done u   s 묵음) → 폴백 발사z/scripts/extract_followup.pysendT<   )capture_outputr.   r2   r+   u   : 폴백 발사 완료 — Nr3   u   : 폴백 실패 rc=z: u   : 폴백 예외: )r;   r<   r=   rC   r   r   r   rk   r   rT   statst_mtimer   ANU_FOLLOWUP_GRACE_SECONDSr$   rB   sys
executabler   
subprocessrun
returncodestdoutstripstderrTimeoutExpired)	firedr   rb   ro   notified_markermtimecmdresultrN   s	            r#   fire_anu_followup_fallbackr   <  s    
zz~~,c2c9E
))+C__X. $J	w& ..($'-'@@!!#	NN$--E %K55 	wi/UVYZ]^cZcVdUe  fA  B  	CNN:;	
	J^^C4QSTF  A%
wi7RSYS`S`SfSfShimjmSnRopqwi7J6K\K\J]]_`f`m`m`s`s`uvzwz`{_|}~E$JL L/  		( ))73 	J?7)3DQCHII	Js+   F;BF#	F F #G<GGc                      t        d       t               } t        d|  d       t               }t        d| d       t               }|rt        d| d       yy)u   1회 실행u!   [DONE-WATCHER] 1회 실행 시작rw   u   개 .done 파일 처리 완료u    개 .failed 파일 처리 완료   건 아누 후속 폴백 발사N)r$   r   r   r   )r   failed_processedfallback_fireds      r#   run_oncer   q  se    45"$I?9+-KLM+-?#3"44TUV/1N~&66UVW r|   c                  r   t        d       t        dt         d       	 	 t               } | dkD  rt        d|  d       t	               }|dkD  rt        d| d       t               }|dkD  rt        d| d       t        j                  t               u# t        $ r}t        d	|        Y d
}~6d
}~ww xY w)u   데몬 모드u#   [DONE-WATCHER] 데몬 모드 시작u-   [DONE-WATCHER] 데몬 모드 시작 (간격: u   초)r   rw   u   개 .done 파일 처리u   개 .failed 파일 처리r   u   ERROR: 예외 발생: N)	r$   printDAEMON_INTERVALr   r   r   rn   rC   rD   )r   r   r   rN   s       r#   
run_daemonr   }  s    67	9/9J$
OP
	7*,I1}	{2IJK35!#(8'99RST79N!'77VWX 	

?#   	71!566	7s   AB 	B6B11B6c                      t        j                  d      } | j                  ddd       | j                         }|j                  rt                y t                y )NuF   .done 파일 + .failed 파일 감시 → bot idle 전환 + FAIL 알림)descriptionz--daemon
store_trueu   데몬 모드 (30초마다))actionhelp)argparseArgumentParseradd_argument
parse_argsdaemonr   r   )parserargss     r#   mainr     sH    $$1yzF

<>[\D{{
r|   __main__)r   N)6__doc__r   r   r1   r;   rh   r   r   rC   r   r   r   pathlibr   r?   r<   r=   r   str__file__resolver   
_UTILS_DIRpathinsertutils.task_id_parserr   r   rS   r   r   r   r   r$   boolrO   dictrW   ra   rr   r{   listr   tupler   rB   r   r   r   r   r   r   r   __name__ r|   r#   <module>r      s      	 	  
  2 2   02GHh'')00778
SXXHHOOAz" 1^$N34
N++KLM N++BCD   yq!"	# 	$ 	6 6 6r	4 	
D 
T 
4 C$J <# $ 2d $$ 5tCy+A :C 24:  c B2C 2j	X$, zF r|   