
    iH/                        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mZmZm	Z	 ddl
mZ ddlZej                  j                  dd      Z ee d      Z ee d      Z ee 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Z%d!dZ&d!dZ'd!dZ(e)d k(  r e(        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/workspacez/memory/eventsz /memory/events/bot-activity.jsonz/logs/done-protocol.log   	   )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       E/home/jay/workspace/.worktrees/task-2116-dev1/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intr0   timesleepr-   RequestException)r   	bot_tokenr,   urlmax_retriesattemptpayloadrespr4   plain_payloadresp2es               r"   send_telegram_notificationrN   1   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   r0   loadJSONDecodeErrorr   r#   )r!   rM   s     r"   load_bot_activityrV   j   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   r9   F)indentensure_asciiNTu(   ERROR: bot-activity.json 저장 실패: )rR   with_suffixr   r0   dumpreplacer   r#   )rW   	temp_filer!   rM   s       r"   save_bot_activityr`   v   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에서 조회 필요
    ztask-\d+\.\d+\.(\w+)\.done$r*   .doner&   z/memory/task-timers.jsonr   r   tasksteam_idN)namerematchgroupstemr^   r   r   rS   r0   loads	read_textr<   	Exception)ra   rf   rh   task_id
timer_filerW   	task_datas          r"   extract_team_from_done_filerq      s     >>DHH3T:E{{1~ nn$$Wb1G^,,DEF
::j22G2DED"-11'2>I==++    s   A9C 	CCre   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로 전환rP   zWARN: team_id 'u    '가 bot-activity.json에 없음FstatusidleTz%Y-%m-%dT%H:%M:%SZsince[DONE-WATCHER] u   : → idle (.done 감지))	rV   r<   r#   r   r   r   utcstrftimer`   )re   rW   rP   utc_nows       r"   set_bot_idlerz      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
.escalatedz*.donerc   c              3   f   K   | ](  }t         j                  |z   z  j                          * y w)N)
EVENTS_DIRrf   rS   ).0extra   s     r"   	<genexpr>z"scan_done_files.<locals>.<genexpr>   s(     X#
inns23;;=Xs   .1)r   globsuffixanyappend)processed_exts
done_filesra   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 파싱 실패: Nrn   rs   u   필수 키 누락: qc_hashT)	sort_keysr[   u5   QC 해시 불일치 — 수동 생성 의심: stored=   z... computed=z...uG   qc_hash 필드 없음 — QC 게이트 미사용 또는 레거시 .doneu	   불일치u
   필수 키r   )r0   rk   rl   rU   r   r   itemsdumpshashlibsha256encode	hexdigestlen)ra   warningsrW   rM   required_keyskeystored_hashkvhash_payloadhash_strcomputed_hashrY   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#   rf   rq   rz   )r   	processedra   r   r   rY   re   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   rf   rS   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 파싱 실패: Nrn   fail_reasonzQC FAILz
[QC FAIL] u    — z[FAILED-WATCHER] u   : Telegram 알림 전송 시도r}   u   : 알림 전송 완료 → r*   u5   : 알림 전송 실패 — 다음 주기에 재시도)r   r0   rk   rl   rU   r   r#   rf   r<   rj   rN   r   rename)	r   r   r   rW   rM   rn   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        d       t               } t        d|  d       t               }t        d| d       y)u   1회 실행u!   [DONE-WATCHER] 1회 실행 시작rv   u   개 .done 파일 처리 완료u    개 .failed 파일 처리 완료N)r#   r   r   )r   failed_processeds     r"   run_oncer   2  sB    45"$I?9+-KLM+-?#3"44TUVr{   c                  6   t        d       t        dt         d       	 	 t               } | dkD  rt        d|  d       t	               }|dkD  rt        d| d       t        j                  t               W# t
        $ r}t        d|        Y d	}~6d	}~ww xY w)
u   데몬 모드u#   [DONE-WATCHER] 데몬 모드 시작u-   [DONE-WATCHER] 데몬 모드 시작 (간격: u   초)r   rv   u   개 .done 파일 처리u   개 .failed 파일 처리u   ERROR: 예외 발생: N)r#   printDAEMON_INTERVALr   r   rm   rB   rC   )r   r   rM   s      r"   
run_daemonr   ;  s    67	9/9J$
OP
	7*,I1}	{2IJK35!#(8'99RST 	

?#   	71!566	7s   <A7 7	B BB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   N  sH    $$1yzF

<>[\D{{
r{   __main__)r   N)*__doc__r   r   r0   r:   rg   rB   r   r   r   pathlibr   r>   r;   r<   r   r   rR   r   r   r   strr#   boolrN   dictrV   r`   rq   rz   listr   tupler   rA   r   r   r   r   r   r   __name__ r{   r"   <module>r      s{      	 	  2 2   02GH^$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W$& zF r{   