
    AjQ                       U d Z ddlmZ ddlZddlZddlZddlmZ ddlmZm	Z	m
Z
 ddlmZ ddlmZ dZd	ed
<    e
 e	d            Z ej$                  dej&                        ZdZ ej$                  ddj-                  d eD              z   dz         ZddZe G d d             Ze G d d             Ze G d d             Z G d d      Zy)u  anu_v2.worktree_cleanup — 6대 안전조건 기반 worktree cleanup helper (task-2550).

회장 §명시 (2026-05-11 C방안 승인):
  - 81개 누적 .worktrees 정리 — dry-run 우선, --apply는 별도 승인
  - 6대 안전조건 AND 게이트 — 어느 하나라도 FAIL 시 skip + log
  - main worktree 절대 보호 (workspace_root path 차단)
  - one-way isolation: anu_v2 외부 import 금지

6대 안전조건:
  1. task .done.acked 마커 존재
  2. PR state = MERGED (gh API)
  3. task .merge-done 마커 존재
  4. branch가 main에 ancestor (git merge-base --is-ancestor)
  5. worktree 사용 중 X (pgrep + git worktree list lock)
  6. dry-run default; --apply 명시 시에만 실제 삭제

dirty worktree skip + log (memory/events/worktree-cleanup-skipped-<ts>.json)
    )annotationsN)	dataclass)datetime	timedeltatimezone)Path)Callablel   L5: intDEFAULT_CHAT_ID	   )hoursz((?:ghp_|ghs_|github_pat_)[A-Za-z0-9_\-]+)github_tokenbot_github_tokengh_token	owner_patz	x-api-keyauthorizationsecretpasswordz(?i)(|c              #  F   K   | ]  }t        j                  |        y wN)reescape).0hs     H/home/jay/workspace/.worktrees/task-2550-dev5/anu_v2/worktree_cleanup.py	<genexpr>r   ,   s     >		!>s   !z)([=:\s]+[^\s,;\"']{1,200})c                    t        | t              r| n
t        |       }t        j                  d|      }t        j                  d |      }|S )u   raw token / API key 마스킹.***MASKED***c                T    | j                  d      | j                  d      d   z   dz   S )N      r   r   )group)ms    r   <lambda>z _sanitize_text.<locals>.<lambda>5   s$    AGGAJA$>$O     )
isinstancestr_TOKEN_PREFIX_REsub_KEY_VALUE_RE)textss     r   _sanitize_textr.   1   sA    4%3t9A^Q/AOQRSAHr&   c                  :    e Zd ZU dZded<   ded<   ded<   ded<   y)	WorktreeCandidateu   worktree 후보 1개.r(   pathbranch
str | Nonetask_idhead_shaN__name__
__module____qualname____doc____annotations__ r&   r   r0   r0   9   s    
IKMr&   r0   c                  :    e Zd ZU dZded<   ded<   ded<   ded<   y	)
SafetyConditionResultu   6대 안전조건 결과.r
   	conditionr(   nameboolpasseddetailNr6   r<   r&   r   r>   r>   B   s    #N
ILKr&   r>   c                  v    e Zd ZU dZded<   ded<   ded<   ded	<   ded
<   ded<   ded<   ded<   ded<   ded<   y)CleanupResultu   worktree 1개 cleanup 결과.r(   worktree_pathr3   r4   zlist[SafetyConditionResult]safety_resultsrA   all_safedirtyis_mainappliedskippedskip_reasontsNr6   r<   r&   r   rE   rE   K   s;    '//NKMMMGr&   rE   c                      e Zd ZdZdddd	 	 	 	 	 	 	 ddZddZdddZddZddZdd	Z	dd
Z
ddZddZddZedd       ZdddZdddZddZy) WorktreeCleanupuq   6대 안전조건 기반 worktree cleanup helper.

    외부 부수효과는 모두 Callable 주입 가능.
    N)subprocess_runnerclockworkspace_rootc                   ||nt         j                  | _        ||nd | _        ||| _        y t	        d      | _        y )Nc                 6    t        j                  t              S )N)tz)r   now_KSTr<   r&   r   r%   z*WorktreeCleanup.__init__.<locals>.<lambda>h   s    x||t?T r&   z/home/jay/workspace)
subprocessrun_subprocess_runner_clockr   _workspace_root)selfrQ   rR   rS   s       r   __init__zWorktreeCleanup.__init__`   sF     8I7T"3ZdZhZh$0e7T1?1K~QUVkQlr&   c                    | j                   dz  dz  | dz  }|j                         }t        dd||rd      S d|       S )	u:   안전조건 1: memory/events/<task_id>.done.acked 존재.memoryeventsz.done.ackedr!   
done_ackedok	missing: r?   r@   rB   rC   r]   existsr>   r^   r4   markerrB   s       r   check_safety_1_done_ackedz)WorktreeCleanup.check_safety_1_done_ackedm   [    %%08;	>UU$l6"D
 	
*3F8(<
 	
r&   c                   d}|r!|j                  d      r|t        d      d n|}	 g d}|r|j                  d|g       n|j                  d|g       | j                  |dddt	        | j
                        d	      }|j                  d
k7  r(t        ddddt        |j                        dd        S t        j                  |j                  xs d      }|st        dddd      S |D cg c]"  }|t	        |j                  dd            v s!|$ }}|st        dddd| d      S |D ]:  }|j                  d      dk(  st        dddd|j                  d       d      c S  t        dddd|D 	cg c]  }	|	j                  d       c}	       S c c}w c c}	w # t        j                  t        j                   t"        f$ r1}
t        ddddt        t	        |
            dd        cY d}
~
S d}
~
ww xY w)u  안전조건 2: gh API로 task의 PR state == MERGED 확인.

        gh API 호출 전략:
          1. branch가 주어지면 `--head <branch suffix>` 정확 매칭 시도
          2. branch가 None이거나 1번 실패 시 head 없이 전체 검색 후 headRefName에서 task_id 매칭
        gh API 실패 시 FAIL (보수적). 어느 dev 팀(dev1~dev7)이든 매칭 가능.
        Nzrefs/heads/)ghprlistz--stateallz--jsonznumber,state,headRefNamez--headz--searchTF   capture_outputr,   checkcwdtimeoutr   r"   	pr_mergedzgh api failed:    rf   z[]zno PR foundheadRefName zno PR with task_id=z' in headRefName (unrelated PR rejected)stateMERGEDzPR #numberz MERGEDz
PR state: error: d   )
startswithlenextendr[   r(   r]   
returncoder>   r.   stderrjsonloadsstdoutgetrY   TimeoutExpiredJSONDecodeErrorOSError)r^   r4   r2   head_refcmdprocprsro   
candidatespes              r   check_safety_2_pr_mergedz(WorktreeCleanup.check_safety_2_pr_mergedv   st     $6<6G6G6Vvc-012\bH	GiC

Hh/0

J01**3t$V[adeieyeyaz  EG*  HD!#,k%,^DKK-H#-N,OP  **T[[0D1C,q{SXanoo (+XgRVVMSU=V9W.W"XJX,k%0	9`a  ! F66'?h.01;W[fjkmkqkqrzk{j|  }D  eE  F  FF )1;u_i  DN  kO~klkpkpqxky  kO  jP  ^Q  R  R Y kO))4+?+?I 	G(1;u_fguvyz{v|g}  C  @C  hD  gE  ^F  G  G	Gs`   BF# 53F# )F# -"FFF# +F# #F# )F# 7F	F# 
F# #(G<&G71G<7G<c                    | j                   dz  dz  | dz  }|j                         }t        dd||rd      S d|       S )	u:   안전조건 3: memory/events/<task_id>.merge-done 존재.ra   rb   z.merge-done   
merge_donerd   re   rf   rg   ri   s       r   check_safety_3_merge_donez)WorktreeCleanup.check_safety_3_merge_done   rl   r&   c           
     X   	 ddd|dg}| j                  |dddt        | j                        d      }|j                  d	k(  }t	        d
d||rd      S d|j                   d      S # t
        j                  t        f$ r(}t	        d
dddt        |      dd        cY d}~S d}~ww xY w)u   안전조건 4: git merge-base --is-ancestor <branch> origin/main → 0 (PASS).

        branch가 main에 머지되었으면 ancestor.
        gitz
merge-basez--is-ancestorzorigin/mainTF   rs   r      branch_in_mainzancestor of mainzNOT ancestor (rc=)rf   r   Nr   )r[   r(   r]   r   r>   rY   r   r   )r^   r2   r   r   rB   r   s         r   check_safety_4_branch_in_mainz-WorktreeCleanup.check_safety_4_branch_in_main   s    
		|,OC**3t$V[adeieyeyaz  EG*  HDoo*F("26.4* <MdooM^^_:`  ))73 	|(1;KTYdklopqlrswtwlxkybz{{	|s$   AA( A( (B)B$B)$B)c           
     B   	 g d}| j                  |dddt        | j                        d      }|j                  dk7  rt	        dddd	|j                   
      S d}d}|j
                  j                         D ]c  }|j                  d      r |t        d      d j                         |k(  }4|r|j                         dv rd} n|sO|j                  d      sad} n |rt	        dddd
      S 	 	 dd|g}| j                  |dddd      }	|	j                  dk(  rt	        dddd
      S |	j                  dk(  rCt        |	j
                  j                         j                               }
t	        ddd|
 d
      S t	        dddd|	j                   
      S # t        j                  t        f$ r(}t	        ddddt        |      dd  
      cY d}~S d}~ww xY w# t        j                  t        f$ r(}t	        ddddt        |      dd  
      cY d}~S d}~ww xY w)u{  안전조건 5: worktree 사용 중 X.

        이중 안전장치:
          (a) git worktree list --porcelain: 해당 path가 locked / prunable이 아닌지 확인
          (b) pgrep -f <worktree_path>: 다른 봇 process가 path를 사용 중이 아닌지 확인
        둘 다 PASS여야 안전 (AND 게이트). 한쪽이라도 FAIL이면 사용 중으로 판정.
        r   worktreerp   --porcelainTFr   rs   r      
not_in_usezgit worktree list failed: rc=rf   	worktree N)lockedprunable)zlocked z	prunable z4worktree is locked or prunable per git worktree listzgit worktree list error: r   pgrepz-frt   r,   ru   rw   r!   z#git list OK + no process using pathz process(es) using pathz	pgrep rc=zpgrep error: )r[   r(   r]   r   r>   r   
splitlinesr   r   striprY   r   r   )r^   rF   cmd_aproc_alocked_or_prunablein_target_blockliner   cmd_bproc_b	pid_counts              r   check_safety_5_not_in_usez)WorktreeCleanup.check_safety_5_not_in_use   s   	J>E,,dU,,-r - F   A%,l5:6;L;L:MN 
 "'#O002 ??;/&*3{+;+<&=&C&C&E&VO$9O)O)-&$9Q)R)-& ",l5Q  "	~dM2E,,U4dZ_ik,lF   A%,q|TX  bG  H  H""a' 3 3 5 @ @ BC	,q|TYendo  pG  cH  I  I,q|TYdmntnn  nA  cB  C  C ))73 	J(1<PU`yz}~  {A  BF  CF  {G  zH  _I  J  J	J  ))73 	~(1<PU`mnqrsntuyvynzm{^|}}	~s\   AF A,F F F 28G +AG =G G2GGGH6HHHc                2    t        dd||rd      S d      S )u?   안전조건 6: dry-run default; --apply 명시 시에만 PASS.   apply_explicitz--apply specifiedzdry-run (no actual delete)rf   )r>   )r^   
apply_flags     r   check_safety_6_apply_explicitz-WorktreeCleanup.check_safety_6_apply_explicit   s,     %.z+5'
 	
;W
 	
r&   c                    	 dd|ddg}| j                  |dddd      }t        |j                  j                               S # t        j
                  t        f$ r Y yw xY w)	uu   uncommitted changes 존재 여부.

        `git status --porcelain` 결과가 비어있지 않으면 dirty.
        r   z-Cstatusr   TFr   r   )r[   rA   r   r   rY   r   r   )r^   rF   r   r   s       r   is_dirty_worktreez!WorktreeCleanup.is_dirty_worktree   si    
	$xGC**3t$V[eg*hD))+,,))73 		s   ?A AAc                    	 t        |      j                         }| j                  j                         }||k(  S # t        $ r Y yw xY w)uI   ★ workspace_root와 동일 경로면 main worktree → 절대 삭제 X.T)r   resolver]   r   )r^   rF   wpwrs       r   is_main_worktreez WorktreeCleanup.is_main_worktree	  sI    	m$,,.B%%--/B8O 		s   7: 	AAc           	        	 g d}| j                  |dddt        | j                        d      }|j                  dk7  rg S g }|j                  j                         j                  d      }|D ]  }|j                         j                         }|s$d}d}d}	|D ]  }
|
j                  d	      r|
t        d	      d
 j                         }1|
j                  d      r|
t        d      d
 j                         }_|
j                  d      sq|
t        d      d
 j                         }	 |s| j                  |	xs |      }|j                  t        ||	||              |S # t        j                  t        f$ r g cY S w xY w)u   git worktree list로 모든 worktree 열거.

        main worktree는 결과에 포함되지만, cleanup_worktree에서 차단.
        r   TFr   rs   r   z

r{   r   NzHEAD zbranch )r1   r2   r4   r5   )r[   r(   r]   r   r   r   splitr   r   r   _extract_task_idappendr0   rY   r   r   )r^   r   r   r   blocksblklinesr1   headr2   r   r4   s               r   enumerate_worktreesz#WorktreeCleanup.enumerate_worktrees  s   
	<C**3t$V[adeieyeyaz  EG*  HD!#	24J[[&&(..v6F s		..0! ?D{3#C$4$56<<>1#CLM288:3!%c)no!6!<!<!>? "33FNdCG%%&7T&Zalp&qr!s" ))73 	I	s$   >E# C	E#  E# ,6E# #F Fc                ^    t        j                  d|       }|rd|j                  d       S dS )uL   branch / path에서 task_id 추출 (예: task/task-2474-dev2 → task-2474).ztask-(\d+(?:\.\d+)?)ztask-r!   N)r   searchr#   )r-   r$   s     r   r   z WorktreeCleanup._extract_task_id5  s1     II-q1'(qwwqzl#2d2r&   c                   | j                         j                         }| j                  |j                        r<| j	                  |d|       t        |j                  |j                  g dddddd|
      S | j                  |j                        }|r<| j	                  |d|       t        |j                  |j                  g dddddd|
      S |j                  st        |j                  dg dddddd	|
      S | j                  |j                        | j                  |j                  |j                  
      | j                  |j                        | j                  |j                        | j                  |j                        | j                  |      g}t        d |D              }d}d}d}	|s`d}|D 
cg c]  }
|
j                   r|
j"                   }}
d| }	t%        |      }|dhz
  }|r| j	                  |dt'        |       |       nq|sd}d}	nj	 ddd|j                  g}| j)                  |dddt+        | j,                        d      }|j.                  dk(  rd}nd}dt1        |j2                        dd  }	t        |j                  |j                  ||dd|||	|
      S c c}
w # t4        j6                  t8        f$ r&}d}dt1        t+        |            dd  }	Y d}~hd}~ww xY w)uH   단일 worktree cleanup 시도. 6대 안전조건 AND 검증 후 실행.main_worktree_protectedFTu0   main worktree (workspace_root) — never deleted)
rF   r4   rG   rH   rI   rJ   rK   rL   rM   rN   rI   z$dirty worktree (uncommitted changes)Nztask_id cannot be extracted)r2   c              3  4   K   | ]  }|j                     y wr   )rB   )r   rs     r   r   z3WorktreeCleanup.cleanup_worktree.<locals>.<genexpr>h  s     1Aqxx1s   zsafety failed: r   zsafety_failed:zdry-run mode (apply=False)r   r   removerr   rs   r   zgit worktree remove failed: ry   r   r   )r\   	isoformatr   r1   _log_skippedrE   r4   r   rk   r   r2   r   r   r   r   rq   rB   r@   setsortedr[   r(   r]   r   r.   r   rY   r   r   )r^   	candidateapplyrN   rI   resultsrH   rK   rL   rM   r   failed
failed_setnon_apply_failuresr   r   r   s                    r   cleanup_worktreez WorktreeCleanup.cleanup_worktree=  s   [[]$$&   0i)BBG 'nni6G6G!Et9k	  &&y~~6i"5 'nni6G6G!Eut9_	     'nnd!Et9V	  **9+<+<=)))*;*;IDTDT)U**9+<+<=..y/?/?@**9>>:..u5
 111"&G&->QXXaff>F>+F84KVJ!+/?.@!@!!!)~fEW>X=Y-Z\^_G6K
Gj(INNC..s4dZ_ehimi}i}e~  IK.  L??a'"G"G$@PTP[P[A\]a^aAb@c"dK
 #..)2C2C"XUEW+	
 	
3 ?* --w7 G 's1v(>t(D'EFGs%   :J.J.A)J3 3K2K--K2c                x    g }| j                         D ]$  }|j                  | j                  ||             & |S )u/   모든 worktree 후보 검사. dry-run default.)r   )r   r   r   )r^   r   r   cands       r   cleanup_all_dry_runz#WorktreeCleanup.cleanup_all_dry_run  sB    '),,. 	EDNN400U0CD	Er&   c                <   	 |j                  dd      j                  dd      }t        t        |j                              dz  }| j                  dz  dz  d| d| dz  }|j
                  j                  d	d	
       ||j                  rt        |j                        ndt        |j                        t        |j                        |t        d}t        |dd      5 }t        j                  ||dd       ddd       y# 1 sw Y   yxY w# t        $ r Y yw xY w)u  skip log 박제 → memory/events/worktree-cleanup-skipped-<ts_compact>-<hash>.json.

        ts + path hash로 파일명 유일성 확보 (동시 실행 시 덮어쓰기 방지).
        branch / worktree_path 필드는 _sanitize_text로 token-like 노출 방어.
        :-.i ra   rb   zworktree-cleanup-skipped-z.jsonT)parentsexist_okN)rN   r4   rF   r2   reasonchat_idwzutf-8)encodingFr"   )ensure_asciiindent)replaceabshashr1   r]   parentmkdirr4   r.   r2   r   openr   dumpr   )	r^   r   r   rN   
ts_compact	path_hashlog_pathpayloadfs	            r   r   zWorktreeCleanup._log_skipped  s   	C-55c3?JD01U;I++h6AF_`j_kklmvlww|D}}HOO!!$!>@I@Q@Q>)*;*;<W[!/	!?()9)9: *G hg6 D!		'15CD D D 		s0   CD  D:D DD D 	DD)rQ   z1Callable[..., subprocess.CompletedProcess] | NonerR   zCallable[[], datetime] | NonerS   zPath | NonereturnNone)r4   r(   r   r>   r   )r4   r(   r2   r3   r   r>   )r2   r(   r   r>   )rF   r(   r   r>   )r   rA   r   r>   )rF   r(   r   rA   )r   zlist[WorktreeCandidate])r-   r(   r   r3   )F)r   r0   r   rA   r   rE   )r   rA   r   zlist[CleanupResult])r   r0   r   r(   rN   r(   r   r   )r7   r8   r9   r:   r_   rk   r   r   r   r   r   r   r   r   staticmethodr   r   r   r   r<   r&   r   rP   rP   Z   s     PT/3&*	m M	m -		m
 $	m 
	m
(GT
| 6~p
B 3 3Q
fr&   rP   )r,   objectr   r(   )r:   
__future__r   r   r   rY   dataclassesr   r   r   r   pathlibr   typingr	   r   r;   rX   compile
IGNORECASEr)   TOKEN_KEY_HINTSjoinr+   r.   r0   r>   rE   rP   r<   r&   r   <module>r     s   & #  	  ! 2 2   " !	"# 2::/MM  

sxx>o>>> B" "         V Vr&   