
    yYi9                     x   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 d      Z ee d      Z ee d	      Zd
ZdZ e e
d            Z	 ej0                  j3                  de       ddlm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e+fd!Z/d&d"Z0d&d#Z1d&d$Z2e3d%k(  r e2        yy# e$ r dedefdZY w xY w)'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   i,  	   )hours)is_valid_task_id_with_legacysreturnc                 @    t        t        j                  d|             S )Nz^task-\d+(?:\.\d+)?$)boolrematch)r   s    E/home/jay/workspace/.worktrees/task-2507-dev5/scripts/done-watcher.py_is_valid_task_idr   ,   s    BHH4a899    messagec                 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       r   log_protocolr+   0   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  )r4   r5   u,   ERROR: Telegram plain text fallback 실패:  u   ERROR: Telegram 전송 실패: u	    (시도    u   ERROR: Telegram 요청 예외: N)osenvirongetr+   rangerequestspoststatus_codeintr8   timesleepr5   RequestException)r   	bot_tokenr4   urlmax_retriesattemptpayloadrespr<   plain_payloadresp2es               r   send_telegram_notificationrV   <   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%   r8   loadJSONDecodeErrorr'   r+   )r*   rU   s     r   load_bot_activityr^   u   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   rA   F)indentensure_asciiNTu(   ERROR: bot-activity.json 저장 실패: )rZ   with_suffixr%   r8   dumpreplacer'   r+   )r_   	temp_filer*   rU   s       r   save_bot_activityrh      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$r2   .doner.   z/memory/task-timers.jsonr   r   tasksteam_idN)namer   r   groupstemrf   r   r   r[   r8   loads	read_textrD   	Exception)ri   rn   r   task_id
timer_filer_   	task_datas          r   extract_team_from_done_filerw      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rm   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로 전환rX   zWARN: team_id 'u    '가 bot-activity.json에 없음FstatusidleTz%Y-%m-%dT%H:%M:%SZsince[DONE-WATCHER] u   : → idle (.done 감지))	r^   rD   r+   r   r   r   utcstrftimerh   )rm   r_   rX   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r   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*.donerk   c              3   f   K   | ](  }t         j                  |z   z  j                          * y w)N)
EVENTS_DIRrn   r[   ).0extri   s     r   	<genexpr>z"scan_done_files.<locals>.<genexpr>   s(     X#
inns23;;=Xs   .1)r   globsuffixanyappend)processed_exts
done_filesri   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 파싱 실패: Nrt   ry   u   필수 키 누락: qc_hashT)	sort_keysrc   u5   QC 해시 불일치 — 수동 생성 의심: stored=   z... computed=z...uG   qc_hash 필드 없음 — QC 게이트 미사용 또는 레거시 .doneu	   불일치u
   필수 키r   )r8   rq   rr   r]   r'   r   itemsdumpshashlibsha256encode	hexdigestlen)ri   warningsr_   rU   required_keyskeystored_hashkvhash_payloadhash_strcomputed_hashra   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   ]: 무결성 검증 실패r2   )r   r   r+   rn   rw   r   )r   	processedri   r   r   ra   rm   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   rn   r[   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 파싱 실패: Nrt   fail_reasonzQC FAILz
[QC FAIL] u    — z[FAILED-WATCHER] u   : Telegram 알림 전송 시도r   u   : 알림 전송 완료 → r2   u5   : 알림 전송 실패 — 다음 주기에 재시도)r   r8   rq   rr   r]   r'   r+   rn   rD   rp   rV   r   rename)	r   r   r   r_   rU   rt   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   rk   z.anu-notifiedz[ANU-FOLLOWUP] u&   : finish-task.sh 누락 의심 (.done u   s 묵음) → 폴백 발사z/scripts/extract_followup.pysendT<   )capture_outputr5   r9   r2   u   : 폴백 발사 완료 — Nr:   u   : 폴백 실패 rc=z: u   : 폴백 예외: )rB   rC   rD   rJ   r   r   r   rp   r   r[   statst_mtimer'   ANU_FOLLOWUP_GRACE_SECONDSr+   rI   sys
executabler   
subprocessrun
returncodestdoutstripstderrTimeoutExpired)	firedr   ri   rt   notified_markermtimecmdresultrU   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회 실행 시작r|   u   개 .done 파일 처리 완료u    개 .failed 파일 처리 완료   건 아누 후속 폴백 발사N)r+   r   r   r   )r   failed_processedfallback_fireds      r   run_oncer   r  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   r|   u   개 .done 파일 처리u   개 .failed 파일 처리r   u   ERROR: 예외 발생: N)	r+   printDAEMON_INTERVALr   r   r   rs   rJ   rK   )r   r   r   rU   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)4__doc__r   r   r8   rB   r   r   r   rJ   r   r   r   pathlibr   rF   rC   rD   r   r   rZ   r"   r   r   r    pathinsertutils.task_id_parserr   r   ImportErrorstrr   r+   rV   dictr^   rh   rw   r   listr   tupler   rI   r   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!":HHOOA~&V	# 	$ 	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 i  ::S :T ::s   "D) )D98D9