
    !i[W              
          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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" ee d      Z#dZ$dZ%de!e   fdZ&dedefdZ'ded eddfd!Z(d"ed#edefd$Z)d%edz  dede*eedz  edz  f   fd&Z+d'edefd(Z,deee-f   fd)Z.dede*ee!e   f   fd*Z/de-fd+Z0de!e   fd,Z1de-fd-Z2de-fd.Z3d3d/Z4d3d0Z5d3d1Z6e7d2k(  r e6        yy)4u;  .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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-2374-dev7/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   5   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   n   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 저장 (원자적 쓰기).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_activityrb   z   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   rT   r1   loads	read_textr=   	Exception)rc   rh   rj   task_id
timer_filerX   	task_datas          r#   extract_team_from_done_filers      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rg   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strftimerb   )rg   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*.donere   c              3   f   K   | ](  }t         j                  |z   z  j                          * y wN)
EVENTS_DIRrh   rT   ).0extrc   s     r#   	<genexpr>z"scan_done_files.<locals>.<genexpr>   s(     X#
inns23;;=Xs   .1)r   globsuffixanyappend)processed_exts
done_filesrc   s     @r#   scan_done_filesr      s`    
 DNJ__X. %	w&XXX)$% r}   z/memory/statei  r6   c                     g } t         j                  d      D ]e  }|j                  dk7  r|j                  }t         | dz  j	                         r:t         | dz  j	                         rU| j                  |       g | S )u   미관측(.merged 미생성) `.work-done` 파일을 스캔.

    - 동일 task_id에 대해 `.merged` 또는 `.merge-failed`가 이미 있으면 제외
    z*.work-donez
.work-done.merged.merge-failed)r   r   r   rl   rT   r   )work_done_fileswd_filerp   s      r#   scan_work_done_filesr      s    
 #%O??=1 (>>\),,G9G,,446G9M22::<w'( r}   rp   c                     t         |  dz  }|j                         si S 	 t        j                  |j	                  d            S # t        j
                  t        f$ r i cY S w xY w)u8   memory/state/{task_id}.json 로드 (없으면 빈 dict)..jsonr   r   )	STATE_DIRrT   r1   rm   rn   rV   r   )rp   
state_files     r#   _load_stater      se    y..J	zz*...@AA  '* 	s   $A A#"A#statec                 x   t         j                  dd       t         |  dz  }|j                  d      }	 t        |dd      5 }t	        j
                  ||dd	
       ddd       t        j                  ||       y# 1 sw Y    xY w# t        $ r' 	 t        j                  |        # t        $ r Y  w xY ww xY w)z)state.json atomic write (rename pattern).Tr   r   rZ   r[   r   r   Fr:   r]   r\   N)r   r   r^   r   r1   r_   r;   renamero   unlinkr   )rp   r   r   tmpr"   s        r#   _atomic_write_stater      s    OOD4O0y..J

 
 
(C	#sW- 	>IIeQU1=	>
		#z"	> 	>  	IIcN 	  		sG   B	 A=B	 =BB	 		B9B)(B9)	B52B94B55B9marker_pathrJ   c                 r   | j                         ry| j                  | j                  dz         }	 t        |dd      5 }t	        j
                  ||dd       ddd       t        j                  ||        y	# 1 sw Y    xY w# t        $ r) 	 t        j                  |       Y y# t        $ r Y Y yw xY ww xY w)
uA   마커 파일 atomic 생성 (idempotent: 이미 있으면 False).FrZ   r[   r   r   r:   r   NT)rT   r^   r   r   r1   r_   r;   r   ro   r   r   )r   rJ   r   r"   s       r#   _atomic_create_markerr     s    

!
!+"4"4v"=
>C
#sW- 	@IIgquQ?	@
		#{#	@ 	@  	IIcN   		s@   B A8B 8B=B 	B6B%%	B2.B61B22B6pr_urlc                    | syddd| ddg}	 t        j                  |ddd	      }|j                  d
k7  r-t        d| d|j                  j                         dd         yt        j                  |j                        }|j                  d      }|j                  d      xs i j                  d      }|rd||fS y# t         j                  t        j                  t        f$ r}t        d| d|        Y d}~yd}~ww xY w)ua   GitHub PR mergedAt 조회.

    Returns:
        (is_merged, mergedAt_iso, merge_commit_sha)
    )FNNghprviewz--jsonzmergedAt,mergeCommitTr   capture_outputr.   r2   r   u   [옵션D] gh pr view 실패 []: Nr3   mergedAtmergeCommitoidu   [옵션D] gh pr view 예외 [)
subprocessrun
returncoder$   stderrstripr1   rm   stdoutr=   TimeoutExpiredrV   r   )r   rp   cmdresultrX   	merged_atshaexcs           r#   _query_merged_atr     s     vvx1G
HC!DtRP!8	V]]EXEXEZ[_\_E`Dabc$zz&--(HHZ(	xx&,"11%8C'' %%t';';WE !4WISFG !s   AB> "AB> >(D&C<<Dwork_done_filec           
         | j                   }t        |      }|j                  dd      }|j                  dd      }|dk(  s|du rlt        | dz  }t	        j
                  t              j                  d      }t        |||d	d
d      r&d|d<   ||d<   t        ||       t        d| d       yy	 | j                         j                  }t        j                         |z
  }|j                  d      }	|	s7	 t        j                   | j#                  d            }
|
j                  d      }	t'        |	|      \  }}}|rTt        | dz  }t        |||||	dd      r5d|d<   ||d<   ||d<   t        ||       t        d| d|r|d	d nd d       yy|t(        kD  rt+        |j                  dd            }t        | dz  }t	        j
                  t              j                  d      }t        ||||d t+        |       d!|	d"      r+d#|d<   ||d$<   d%|d&<   t        ||       t        d| d'       y#yy(# t        $ r Y yw xY w# t        j$                  t        f$ r i }
Y 7w xY w))u   .work-done 파일 1건을 reconcile.

    Returns:
        결과 status: "merged" | "merge-failed" | "polling" | "system-done" | "skipped"
    kindcodemerge_requiredTmetaFr   z%Y-%m-%dT%H:%M:%S+09:00Nmeta_task_terminal)rp   r   merge_commit_shareasonsystem-donephasetimestamp_mergedu
   [옵션D] u3   : meta task → .merged 즉시 생성 (system-done)skippedr   r   r   mergedAt_observed)rp   r   r   r   r   mergedr   u1   : mergedAt 관측 → .merged atomic 생성 (sha=   ?r8   retry_countr   r   u   30분 polling timeout (elapsed=zs))rp   	failed_atr   
last_errorr   merge-failedtimestamp_merge_failedpolling_timeout_30mr   u)   : 30분 polling timeout → .merge-failedpolling)rl   r   r=   r   r   r   r   rz   r   r   r$   statst_mtimer   rC   r1   rm   rn   rV   r   MERGED_AT_TIMEOUT_SECONDSrB   )r   rp   r   r   r   merged_filer    wd_mtimeelapsedr   wd_data	is_mergedr   r   r   failed_files                   r#   reconcile_work_doner   1  s    !!G E 99VV$DYY/6Nv~50 gYg#66\\#''(AB  $*	/
  +E'N(*E$%/:gY.abc !&&(11 iikH$G YYx F	jj!9!97!9!KLG[[*F !1 AIy# gYg#66 " #)/
  &E'N(1E$%(+E$%/:gY._kn`cdfef`gtw_xxyz{ **%))M156 gYm#<<\\#''(AB &;CL>L/
  ,E'N.0E*+"7E,/:gY.WXY!i   $$g. 	G	s$   2H7 76I 7	III%$I%c                      dddddd} t               D ]&  }	 t        |      }| j                  |d      dz   | |<   ( | S # t        $ r%}t	        d|j
                   d|        Y d}~Td}~ww xY w)u   모든 미관측 .work-done 파일 reconcile.

    Returns:
        {"polling": N, "merged": N, "merge-failed": N, "system-done": N}
    r   )r   r   r   r   r   r+   u   [옵션D] reconcile 예외 [r   N)r   r   r=   ro   r$   rh   )statsr   r   r   s       r#   process_work_done_filesr     s     )*QZ[hijE') P	P(1F!IIfa014E&MP L  	P7~SNOO	Ps   #>	A,A''A,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 파싱 실패: Nrp   ru   u   필수 키 누락: qc_hashT)	sort_keysr]   u5   QC 해시 불일치 — 수동 생성 의심: stored=   z... computed=z...uG   qc_hash 필드 없음 — QC 게이트 미사용 또는 레거시 .doneu	   불일치u
   필수 키r   )r1   rm   rn   rV   r   r   itemsdumpshashlibsha256encode	hexdigestlen)rc   warningsrX   rN   required_keyskeystored_hashkvhash_payloadhash_strcomputed_hashr[   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 [r   zINTEGRITY_FAIL [u   ]: 무결성 검증 실패r+   )r   r   r$   rh   rs   r|   )r   	processedrc   r   r   r[   rg   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   rh   rT   r   )failed_filesr   s     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 파싱 실패: Nrp   fail_reasonzQC FAILz
[QC FAIL] u    — z[FAILED-WATCHER] u   : Telegram 알림 전송 시도r   u   : 알림 전송 완료 → r+   u5   : 알림 전송 실패 — 다음 주기에 재시도)r   r1   rm   rn   rV   r   r$   rh   r=   rl   rO   r   r   )	r   r   r   rX   rN   rp   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 ]H  }|j                  dk7  r|j                  }t        j                  d|      s7t        | dz  }|j                         rT	 |j                         j                  }||z
  t        k  r|t        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.                  j1                         dd         n9t        d	| d|j,                   d|j2                  j1                         dd         K | S # t        $ r Y Zw xY w# t(        j4                  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   re   z^task-\d+(?:\.\d+)?$z.anu-notifiedz[ANU-FOLLOWUP] u&   : finish-task.sh 누락 의심 (.done u   s 묵음) → 폴백 발사z/scripts/extract_followup.pysendT<   r   r+   u   : 폴백 발사 완료 — Nr3   u   : 폴백 실패 rc=z: u   : 폴백 예외: )r;   r<   r=   rC   r   r   r   rl   ri   rj   rT   r   r   r   ANU_FOLLOWUP_GRACE_SECONDSr$   rB   sys
executabler   r   r   r   r   r   r   r   )	firedr   rc   rp   notified_markermtimer   r   rN   s	            r#   fire_anu_followup_fallbackr    s    
zz~~,c2c9E
))+C__X. $J	w& ..xx/9$'-'@@!!#	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               }t	        d |j                         D              rt        d|        t               }|rt        d| d       yy)	u   1회 실행u!   [DONE-WATCHER] 1회 실행 시작rx   u   개 .done 파일 처리 완료u    개 .failed 파일 처리 완료c              3       K   | ]  }|  y wr    r   r   s     r#   r   zrun_once.<locals>.<genexpr><  s     
(1
(   u/   [DONE-WATCHER] [옵션D] .work-done reconcile:    건 아누 후속 폴백 발사N)r$   r   r   r   r   valuesr  )r   failed_processedwd_statsfallback_fireds       r#   run_oncer  3  s    45"$I?9+-KLM+-?#3"44TUV&(H

(hoo'
((FxjQR/1N~&66UVW r}   c                     t        d       t        dt         d       	 	 t               } | dkD  rt        d|  d       t	               }|dkD  rt        d| d       t               }t        d |j                         D              rt        d	|        t               }|dkD  rt        d| d
       t        j                  t               # t        $ r}t        d|        Y d}~6d}~ww xY w)u   데몬 모드u#   [DONE-WATCHER] 데몬 모드 시작u-   [DONE-WATCHER] 데몬 모드 시작 (간격: u   초)r   rx   u   개 .done 파일 처리u   개 .failed 파일 처리c              3       K   | ]  }|  y wr   r  r  s     r#   r   zrun_daemon.<locals>.<genexpr>R  s     010r  u%   [DONE-WATCHER] [옵션D] .work-done: r  u   ERROR: 예외 발생: N)r$   printDAEMON_INTERVALr   r   r   r   r  r  ro   rC   rD   )r   r  r  r  rN   s        r#   
run_daemonr  C  s    67	9/9J$
OP
	7*,I1}	{2IJK35!#(8'99RST.0H0hoo/00=hZHI79N!'77VWX 	

?#%   	71!566	7s   BC 	C.C))C.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)8__doc__r"  r   r1   r;   ri   r   r  rC   r   r   r   pathlibr   r?   r<   r=   r   r   rS   r   r  r  r   strr$   boolrO   dictrW   rb   rs   r|   listr   r   r   MERGED_AT_MAX_RETRIESr   r   r   r   tupler   r   rB   r   r   r   r   r   r  r  r  r)  __name__r  r}   r#   <module>r4     sk      	 	  
  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 , N#=12	#  d4j $   T d "t d t $!S4Z !# !%cDjRUX\R\@\:] !2S S Slc3h  $ 5tCy+A :C 24:  c B2C 2jX $4 zF r}   