
    kZ"jXK                     2   d Z ddlZddlZddlmZ ddlmZ ddlmZm	Z	m
Z
mZ  e ee      j                         j                  j                        Zeej"                  vrej"                  j%                  de       ddlmZmZmZmZmZmZmZmZ ddlmZmZmZ dZ d	Z!d
Z"e G d d             Z#de#de$de#fdZ%de#de#fdZ&de#de#fdZ'de#de#fdZ(de#de#fdZ)de#de#fdZ*de#de#fdZ+e dde,de,de,de$fdZ-ddddddede,ded ede#d!e
e	eef      d"e
e   d#e
e,   d$e
e   de	eef   fd%Z.d&ed'ed(e,de#deeef   f
d)Z/ed*d+e
e0   d,e0d-e,de$fd.Z1de#d,e0de#fd/Z2d0ed1de#d,e0d2ed-e,de	eef   f
d3Z3dddd4dede,d5ed6ed7ed8e$de#d9e
e   d:e
e   d;e
e   de	eef   fd<Z4ddddd=de,d>ed?ed$e
e   d@e$dAe
e   dBe
e   dCe
e,   dDe
e,   de	eef   fdEZ5ddFdedGe$dHe$dIe$dJe$dKe
e   de	eef   fdLZ6dPdMe
e7   de,fdNZ8e9dOk(  r ejt                   e8              yy)Qu  scripts/ci_watch_handoff_runner.py — CI_WATCH_HANDOFF progress watcher runner.

task-2729 Phase 1 — progress watcher dispatch gate (watcher 골격).

회장 목표:
  dispatch 직후 등록되는 progress watcher 의 러너 골격.
  6 상태(head change · non-force push · CI · finish-task .done · normal callback ·
  fallback prune) 추적 + review-settle quiet-window 골격 + terminal 도달 시
  ANU normal callback 발사를 담당한다.

  ★ ACTIVE=false (Phase 1):
    본 러너는 --dry-run/--once 모드에서 네트워크 호출(gh 폴링) 없이 import-safe 하게
    동작하는 골격이다. 실제 live gh polling 루프는 골격만 두고 미실행한다
    (watcher 위임 골격). production ACTIVE 전환은 별도 회장 승인 대상.

  ★ fallback 은 progress trigger 아님:
    fallback_prune 추적 상태는 dead-man safety-net 의 관측 항목일 뿐이며
    progress trigger 가 아니다. progress 판단은 watcher 의 6 상태/terminal classify 가
    담당한다.

기존 classifier(utils.pr_watcher_terminal_state_classifier) 를 재사용하며
raw ANU 키 문자열을 절대 작성하지 않는다 (anu_key=None → registrar 가 env→default 해결).
    N)	dataclass)Path)AnyDictOptionalTuple)
PRSnapshotclassifyTERMINAL_STATESbuild_callback_enveloperegister_terminal_callbackclassify_lifetime_gapCI_WATCHER_SESSION_LIFETIME_GAP%WATCHER_HEARTBEAT_STALE_THRESHOLD_SEC)WATCHER_TRACKED_STATES watcher_terminal_callback_statusall_states_trackedx      i  c                       e Zd ZU dZdZeed<   dZeed<   dZeed<   dZ	eed<   dZ
eed<   dZeed<   d	Zeed
<   d	Zeed<   dZee   ed<   dZee   ed<   deeef   fdZdefdZy)WatcherStateu.   watcher 6 상태 추적 + poll/elapsed 메타.Fhead_changenon_force_pushcifinish_donenormal_callbackfallback_pruner   pollselapsed_secNlast_poll_tsheartbeat_tsreturnc                     | j                   | j                  | j                  | j                  | j                  | j
                  dS )u+   WATCHER_TRACKED_STATES 키 → bool 매핑.r   r   r   r   r   r   r$   selfs    "scripts/ci_watch_handoff_runner.pyas_tracked_dictzWatcherState.as_tracked_dictO   sB      ++"11''++#33"11
 	
    c                 4    t        | j                               S )u;   6 상태가 모두 True 인지 (all_states_tracked 사용).)r   r(   r%   s    r'   all_trackedzWatcherState.all_trackedZ   s    !$"6"6"899r)   )__name__
__module____qualname____doc__r   bool__annotations__r   r   r   r   r   r   intr   r    r   floatr!   r   strr(   r+    r)   r'   r   r   ?   s    8K ND BK!OT! ND E3NK$(L(5/($(L(5/(	
c4i 	
:T :r)   r   stateflagsr"   c                 t    |j                         D ]$  \  }}|t        v st        | |t        |             & | S )u7   6 상태 갱신 (WATCHER_TRACKED_STATES 키만 허용).)itemsr   setattrr0   )r6   r7   keyvalues       r'   update_stater=   _   s;    kkm -
U((E3U,- Lr)   c                     d| _         | S NT)r   r6   s    r'   mark_head_changerA   g       ELr)   c                     d| _         | S r?   )r   r@   s    r'   mark_non_force_pushrD   l       ELr)   c                     d| _         | S r?   )r   r@   s    r'   mark_cirG   q   s    EHLr)   c                     d| _         | S r?   )r   r@   s    r'   mark_finish_donerI   v   rB   r)   c                     d| _         | S r?   )r   r@   s    r'   mark_normal_callbackrK   {   s     ELr)   c                     d| _         | S r?   )r   r@   s    r'   mark_fallback_prunerM      rE   r)   )quiet_window_seclast_event_secnow_secrN   c                     || z
  |k\  S )u   review-settle quiet-window 판정 골격.

    마지막 이벤트 이후 quiet_window_sec 이상 무이벤트면 settled.
    r5   )rO   rP   rN   s      r'   is_quiet_window_settledrR      s     n$)999r)   )last_snapshotanu_keychat_idrunnertask_id	pr_numberterminal_statereasonrS   rT   rU   rV   c        	   	         t        | ||||j                  |j                  |      }	t        |	|||      }
|
j                  rd|_        |
j                  |t        d|
j                        d|
j                   |
j                  dS )u  terminal 도달 시 ANU normal callback 발사 (classifier 재사용).

    - build_callback_envelope 로 envelope 생성 → register_terminal_callback 호출.
    - ★ anu_key=None 이면 registrar 가 env→ANU_KEY_DEFAULT 로 해결 (self-key 차단은
      그쪽 책임). raw 키 문자열 절대 작성 금지.
    - result.fired=False 면 callback_status == WATCHER_TERMINAL_CALLBACK_NOT_WIRED.
    - ★ P0-A: terminal 도달 → callback MANDATORY. 미발사 시 not_wired=True + NOT_WIRED.
    )rW   rX   rY   rZ   polls_completedr   rS   )enveloperT   rU   rV   T)terminal_reachedcallback_fired)firedrY   callback_statuscallback_mandatory	not_wiredskipped_reason)r   r   r   r   r`   r   r   rd   )rW   rX   rY   rZ   r6   rS   rT   rU   rV   r]   results              r'   fire_terminal_callbackrf      s    ( '%%%#H (	F || $(;!!<<
 # ,,& // r)   snapexpected_headelapsed_watcher_secc                    |xj                   dz  c_         ||_        t        | ||      \  }}|r|t        v r||fS d|fS )u   단일 classify 평가 → (terminal_state_or_empty, reason).

    terminal 이면 반환 state 는 TERMINAL_STATES 에 포함된다.
       )ri   rh    )r   r   r
   r   )rg   rh   ri   r6   rY   rZ   s         r'   run_oncerm      sS     
KK1K+E%/#NF
 .O;v%%v:r)   )stale_threshold_secr    now_tsrn   c                     | y|| z
  |k\  S )u6  watcher heartbeat/last_poll staleness 판정 (순수 함수).

    - last_poll_ts is None → watcher 가 한 번도 heartbeat 를 남기지 않음 → stale=True.
    - now_ts - last_poll_ts >= stale_threshold_sec → stale=True.
    ★ OS 레벨 감지/재기동은 P0-B. 여기선 판정 contract 만.
    Tr5   r    ro   rn   s      r'   is_watcher_stalerr      s     \!&999r)   c                 "    || _         || _        | S )u   poll 시각을 last_poll_ts/heartbeat_ts 에 기록 (lifecycle 보강, record-only).

    기존 run_once 는 변경하지 않는다. 호출부가 필요 시 별도로 heartbeat 를 갱신한다.
    )r    r!   )r6   ro   s     r'   record_poll_heartbeatrt      s    
  EELr)   rl   )pr_terminal_statern   ru   c                 p    t        | j                  ||      }t        ||      \  }}|||t        k(  |ddS )u/  watcher staleness + PR pending → CI_WATCHER_SESSION_LIFETIME_GAP 진단 (record-only 호출부).

    is_watcher_stale() 로 staleness 판정 후 classifier.classify_lifetime_gap() 호출.
    ★ ACTIVE=false: 분류 결과만 반환(record-only). OS 감지/재기동/auto-register 없음(P0-B).
    rq   )watcher_staleru   F)rw   lifetime_gap_statelifetime_gap_detectedrZ   active)rr   r    r   r   )r6   ro   ru   rn   stale	gap_staterZ   s          r'   classify_watcher_lifetime_gapr}      sU     ''/E
 .+Iv
 '!*.M!M r)   )triggeralready_fired_heads
audit_pathownerrepocurrent_head_shahead_changedr~   r   r   c        
             |sdddS 	 ddl m}
 |
j                  ||||| d|	|dd|      }|S # t        $ r0}ddt	        |      j
                   d	| d
||ddddd	}Y d}~|S d}~ww xY w)u  결함6 결선: head 변경 시 OWNER /gemini review 자동 발사.

    ★ ACTIVE=false 골격: 실 gh 네트워크 호출 없이 구조·결선만 제공.
    ★ review-settle: 발사 후 state는 변경하지 않음 (progress trigger 아님).
      /gemini review 발사는 리뷰 요청일 뿐이며 watcher progress 판단에 개입하지 않는다.
    ★ head_changed가 False이면 발사하지 않고 NO_NEW_HEAD 반환.
    FNO_NEW_HEAD)r`   statusr   NT)rX   r   r   r   rW   observed_commentr   routerrequest_onlytoken_providerr   zFIRE_ERROR: : z/gemini review)	r`   r   bodyrX   head_shadeduper   token_hash_prefixrz   )utils.owner_gemini_triggerowner_gemini_triggerfire_owner_gemini_review	Exceptiontyper,   )rW   rX   r   r   r   r   r6   r~   r   r   _ogtre   excs                r'   #maybe_fire_owner_gemini_on_new_headr   !  s    ( -88
1..-!! 3 / 
6 M  
$T#Y%7%7$83%@$"( !%

 M
s   "- 	A&%A!!A&)sync_fnfetch_state_fnmax_diff_linesobserved_diff_linesmerge_state_status
pr_workdirmerge_approvedr   r   r   r   c        	         X   |sdddS |dv rdddS |dk7  rdddd	S |	 dd
l m}	 |	||fd}	  || |      }
|
j                  d      rdddd	S ||
||kD  rdddS |	 dd
l m}	 |	j                  }	  || |      }|j                  dd      }|dk(  rdddS dddd	S # t        $ r dddcY S w xY w# t        $ r(}ddt	        |      j
                   d| dcY d
}~S d
}~ww xY w# t        $ r dddcY S w xY w# t        $ r(}ddt	        |      j
                   d| dcY d
}~S d
}~ww xY w)u  결함5: merge 전 base sync 파이프라인.

    - merge_approved False → NOT_APPROVED.
    - merge_state_status가 admin·force 필요 신호(DIRTY/BLOCKED) → ABORT_ADMIN_OR_FORCE_REQUIRED.
      절대 admin override/force 호출 금지.
    - UP_TO_DATE면 sync 불필요 → UP_TO_DATE 반환.
    - BEHIND면 sync_fn(기본=merge_queue_executor.sync_pr_base) 호출.
      conflict → ABORT_CONFLICT.
      diff 초과 → ABORT_DIFF_OVER_LIMIT.
      성공 시 fetch_state_fn으로 재검증 → BEHIND면 ABORT_STILL_BEHIND, 아니면 BASE_SYNCED_REVALIDATED.
    - sync_fn/fetch_state_fn 미주입 시 merge_queue_executor 지연 import(테스트 mock 우선).
    FNOT_APPROVED)merge_okr   )DIRTYBLOCKEDABORT_ADMIN_OR_FORCE_REQUIREDBEHINDT
UP_TO_DATE)r   r   base_syncedNr   ABORT_SYNC_FN_UNAVAILABLEc                 *    |j                  d |||      S N)sync_pr_base)_pn_rn_mqe_wd_mss        r'   r   z'base_sync_before_merge.<locals>.sync_fn  s    $$T3S99r)   zABORT_SYNC_ERROR: r   conflictABORT_CONFLICTABORT_DIFF_OVER_LIMITABORT_FETCH_FN_UNAVAILABLEzABORT_REVALIDATE_ERROR: mergeStateStatusUNKNOWNABORT_STILL_BEHINDBASE_SYNCED_REVALIDATED)utils.merge_queue_executormerge_queue_executorImportErrorr   r   r,   getfetch_merge_state)rX   r   r   rV   r   r   r   r   r   r   sync_resultr   revalidatedrevalidated_statuss                 r'   base_sync_before_merger   Y  s   2 !^<< 11!-LMM X% LOO 	N5
 $(Z=O 	:_i0 z"!-=dSS 	"+.0!-DEE 	O5!33Ne$Y7 %);YGX%!-ABB(ARVWWQ  	N %1LMM	N  _!/A$s)BTBTAUUWX[W\-]^^_&  	O %1MNN	O
  e!/GS	HZHZG[[]^a]b-cddes^   B 	B0 C$ 1	C8 B-,B-0	C!9CC!C!$C54C58	D)D$D)$D))marker_writerexternal_dirtymerge_base_clean	ci_passedlocal_fix_verifiedr   c                     |sddddS |r|r|rddddddd| d}|	  ||       |S |S ddddS # t         $ r(}t        |      j                   d	| |d
<   Y d}~|S d}~ww xY w)u  결함4: external dirty block 존재 시 scope evidence 3개로 authoritative completion 인정.

    ★ .done 파일 직접 생성 코드 절대 작성 금지.
      marker_writer 주입 시 marker_writer(marker dict)를 호출하는 것뿐이며,
      본 함수는 .done 파일을 직접 생성하지 않는다.
    ★ manual_done_forgery=False 항상 유지 (.done 위조 0).

    - external_dirty False → NO_EXTERNAL_DIRTY_BLOCK (authoritative 인정 전제 없음).
    - external_dirty True + scope evidence 3개(merge_base_clean AND ci_passed AND local_fix_verified) 모두 True
      → AUTHORITATIVE_COMPLETION_BY_SCOPE_EVIDENCE, marker_writer 주입 시 호출.
    - evidence 하나라도 빠지면 → EXTERNAL_DIRTY_BLOCK_UNRESOLVED.
    FNO_EXTERNAL_DIRTY_BLOCK)authoritative_completionr   manual_done_forgeryT*AUTHORITATIVE_COMPLETION_BY_SCOPE_EVIDENCE)r   r   r   )r   r   evidencer   rW   Nr   marker_writer_errorEXTERNAL_DIRTY_BLOCK_UNRESOLVED)r   r   r,   )rW   r   r   r   r   r   markerr   s           r'   authoritative_completion_markerr     s    * (-/#(
 	
 I*<(,B$(&*
 $)
"
 $Of% v %*3$ 	  O3793E3E2Fb0N,-Os   0 	A!AA!argvc                 l   t        j                  dd      }|j                  ddd       |j                  dt        d	d
       |j                  ddd       |j                  ddd       |j                  ddd       |j	                  |       }t               }|j                  s|j                  ry	y	)u   argparse 엔트리.

    --dry-run/--once 모드는 실제 gh 폴링 없이 import-safe 하게 동작(네트워크 호출 없이
    0 반환). 실제 live 폴링 루프는 골격만 둔다.
    ci_watch_handoff_runnerzItask-2729 Phase 1 CI_WATCH_HANDOFF progress watcher runner (ACTIVE=false))progdescriptionz	--task-idrl   ztask id (e.g. 2729))defaulthelpz--prr   z	PR number)r   r   r   z--expected-headzexpected head oidz--once
store_trueu(   단일 평가 후 종료 (폴링 없음))actionr   z	--dry-runu.   네트워크 호출 없이 import-safe dry-run)argparseArgumentParseradd_argumentr2   
parse_argsr   dry_runonce)r   parserargsr6   s       r'   mainr     s     $$&_F R6KL
S!+F
)2<OP
<fg
L?opT"DNE||tyy  r)   __main__r   );r/   r   sysdataclassesr   pathlibr   typingr   r   r   r   r4   __file__resolveparent
_REPO_ROOTpathinsert*utils.pr_watcher_terminal_state_classifierr	   r
   r   r   r   r   r   r   dispatch.progress_watcher_gater   r   r   REVIEW_SETTLE_QUIET_WINDOW_SECDEFAULT_POLL_INTERVAL_SECDEFAULT_MAX_WATCH_SECr   r0   r=   rA   rD   rG   rI   rK   rM   r2   rR   rf   rm   r3   rr   rt   r}   r   r   r   listr   r,   exitr5   r)   r'   <module>r      s  .  
 !  - -
 h'')00778
SXXHHOOAz"	 	 	  "%    : : :> t  L \ 
|  
< L 
L \ 
  
|   ;	
:
: 
: 	
:
 

:( /3!! 11 1 	1
 1 1 DcN+1 c]1 c]1 SM1 
#s(^1h
  	
  38_B  E	:5/: : 	:
 
:" u    D  	
  
#s(^X ")- $55 5 	5
 5 5 5 5 c]5 "#5 5 
#s(^5~ "$($()-PXPX PX 	PX
 SMPX PX c]PX SMPX SMPX "#PX 
#s(^PXt $(44 4 	4
 4 4 C=4 
#s(^4nx~  @ zCHHTV r)   