
    Kjy                       U d Z ddlmZ ddlmZ ddlmZ ddlmZm	Z	m
Z
 ddlmZmZmZmZmZmZmZ ddlmZmZmZmZmZmZmZ dZd	ed
<   dZd	ed<   dZd	ed<    eeeeh      Z ded<   dZ!d	ed<   dZ"d	ed<   dZ#d	ed<   dZ$d	ed<    ee!e"e#e$h      Z%ded<   dZ&d	ed<   dZ'd	ed<   dZ(d	ed<   d Z)d	ed!<   d"Z*d	ed#<   d$Z+d	ed%<   d&Z,d	ed'<   d(Z-d	ed)<   d*Z.d+ed,<   d-Z/d+ed.<   d/Z0d	ed0<   d1Z1d	ed2<    G d3 d4e2      Z3	 	 	 	 	 	 dDd5Z4 ed67       G d8 d9             Z5 ed67       G d: d;             Z6 ed67       G d< d=             Z7 ed67       G d> d?             Z8 G d@ dA      Z9g dBZ:yC)Eu
  utils.ci_watch_handoff_runner — task-2642 CI_WATCH_HANDOFF state machine.

회장 verbatim (2026-05-23 19:38 KST) 1:1 박제:
  'ANU 는 CI/Gemini 를 직접 기다리지 않는다. PR open 이후 대기/감시/자동수렴은
  반드시 bot 또는 watcher task 에 위임한다.'

본 runner 책임 (spec §4 state machine):
  PR_OPEN (handoff received)
      ↓
  poll loop · interval=poll_interval_seconds · timeout=max_watch_minutes
      ├─ CI 미완료 (PENDING)               → polling continue
      ├─ CI FAIL 자동수렴 가능              → AUTO_REMEDIATE → re-push → re-poll
      ├─ CI FAIL 자동수렴 불가              → CI_FAILED_NON_REMEDIABLE
      ├─ forbidden_path 수정 감지          → CHAIR_REQUIRED (Critical7)
      └─ CI PASS → router 호출 (Gemini freshness)
            ├─ FRESH                       → MERGE_READY
            ├─ NUDGE_POSTED                → polling continue (fresh review 대기)
            ├─ NUDGE_DEDUPED               → polling continue (직전 nudge active)
            ├─ GEMINI_EXTERNAL_TRIGGER_STALE → terminal 동명 enum
            ├─ NUDGE_PERMISSION_DENIED /
            │  CHAIR_UI_FALLBACK_REQUIRED /
            │  NUDGE_FAILED /
            │  NOT_GEMINI_TRIGGER         → CHAIR_REQUIRED (NUDGE_403 / permission)
      ↓
  TERMINAL_REACHED → CALLBACK_FIRE (envelope UTF-8 ≤3900 bytes)
      ↓
  WATCHER_EXIT

PR #144 OWNER_GEMINI_TRIGGER_ROUTER stack 재사용 (gemini_router_call_fn 으로 inject).
forbidden 15종 + owner_trigger 4종 + owner_gemini_trigger_router 3종 무수정.

one-way isolation: utils/ 외부 import 0 (PR #144 router 는 caller 가 inject).
live cokacdir / gh CLI / merge / push 호출 0 (regression mock 가능).

frozen anchor:
  ANCHOR-1: state machine 1 차 — handoff 정규화 후 poll loop 진입
  ANCHOR-2: router final_state 8종 → terminal_states 5종 매핑 (CHAIR_UI_FALLBACK /
            NUDGE_PERMISSION_DENIED / NUDGE_FAILED / NOT_GEMINI_TRIGGER →
            CHAIR_REQUIRED)
  ANCHOR-3: auto_remediation_fn 반환 enum 4종 (APPLIED / NON_REMEDIABLE /
            FORBIDDEN_HIT / LOOP_BOUNDARY) → terminal classification
  ANCHOR-4: callback envelope UTF-8 ≤3900 bytes hard limit (task-2612+3 박제) —
            초과 시 RunnerContractError fail-closed
  ANCHOR-5: handoff.terminal_states subset 외부의 분류는 CHAIR_REQUIRED 로 escalate
  ANCHOR-6: real auto-merge 0 / push 0 / PR open 0 / live cokacdir 0 — caller 가
            inject 한 callable 만 호출
    )annotations)	dataclass)Path)AnyCallableFinal)CiWatchHandoffAuditEVENT_AUTO_REMEDIATEEVENT_CALLBACK_FIREDEVENT_HANDOFF_RECEIVEDEVENT_OWNER_NUDGEEVENT_POLL_TICKEVENT_TERMINAL_REACHED)ALL_TERMINAL_STATESTERMINAL_CHAIR_REQUIRED!TERMINAL_CI_FAILED_NON_REMEDIABLE&TERMINAL_GEMINI_EXTERNAL_TRIGGER_STALETERMINAL_LOOP_BOUNDARYTERMINAL_MERGE_READYvalidate_handoffPENDINGz
Final[str]CI_STATUS_PENDINGPASSCI_STATUS_PASSFAILCI_STATUS_FAILzFinal[frozenset[str]]ALL_CI_STATUSESAPPLIEDREMEDIATE_APPLIEDNON_REMEDIABLEREMEDIATE_NON_REMEDIABLEFORBIDDEN_HITREMEDIATE_FORBIDDEN_HITLOOP_BOUNDARYREMEDIATE_LOOP_BOUNDARYALL_REMEDIATE_OUTCOMESFRESHROUTER_FRESHNUDGE_POSTEDROUTER_NUDGE_POSTEDNUDGE_DEDUPEDROUTER_NUDGE_DEDUPEDGEMINI_EXTERNAL_TRIGGER_STALEROUTER_STALECHAIR_UI_FALLBACK_REQUIREDROUTER_CHAIR_UI_FALLBACKNUDGE_PERMISSION_DENIEDROUTER_NUDGE_PERMISSION_DENIEDNUDGE_FAILEDROUTER_NUDGE_FAILEDNOT_GEMINI_TRIGGERROUTER_NOT_GEMINI_TRIGGER   z
Final[int]DEFAULT_LOOP_BOUNDARY_ATTEMPTSi<  CALLBACK_ENVELOPE_BYTE_LIMITc119085addb0f8b7ANU_COLLECTOR_KEYz/home/jay/workspaceCANONICAL_ROOTc                      e Zd ZdZy)RunnerContractErroru2   runner 입력 / inject callable / envelope 위반.N)__name__
__module____qualname____doc__     4/home/jay/workspace/utils/ci_watch_handoff_runner.pyr>   r>   w   s    <rD   r>   c                    t         S )u=  fallback auto_remediation_fn — 항상 NON_REMEDIABLE (회장 verbatim §8 보호).

    caller 가 explicit 하게 auto_remediation_fn 을 inject 하지 않으면 watcher 는
    어떤 자동수렴도 하지 않고 CI_FAILED_NON_REMEDIABLE 로 분류 (회장 verbatim
    §8 watcher 책임 명시화 보호).
    )r!   )_handoff_ci_snaps     rE   _default_no_op_remediationrI   {   s
     $#rD   T)frozenc                  8    e Zd ZU dZded<   dZded<   dZded<   y	)
RouterCallResultu?   gemini_router_call_fn 반환 (PR #144 RouterResult 의 subset).strfinal_stateFboolpermission_diagnostics_present reasonN)r?   r@   rA   rB   __annotations__rP   rR   rC   rD   rE   rL   rL      s     I+0"D0FCrD   rL   c                  T    e Zd ZU dZded<   dZded<   dZded<   d	Zd
ed<   d	Zd
ed<   y)CIStatusSnapshotuI   ci_status_fn 반환 — caller 가 한 poll tick 의 CI 상태를 집약.rM   statusrC   ztuple[str, ...]failing_checksrQ   severityFrO   forbidden_path_touchedsame_function_high_repeatedN)	r?   r@   rA   rB   rS   rW   rX   rY   rZ   rC   rD   rE   rU   rU      s4    SK&(NO(Hc#(D((--rD   rU   c                  ^    e Zd ZU dZded<   ded<   dZded<   dZded<   d	Zd
ed<   d	Zd
ed<   y)TerminalDecisionu3   terminal state 분류 결과 + 진단 컨텍스트.rM   terminal_staterR   N
str | Nonerouter_final_state	ci_statusr   intauto_remediation_attemptsloop_iterations)	r?   r@   rA   rB   rS   r_   r`   rb   rc   rC   rD   rE   r\   r\      s:    =K%)
) Iz %&s&OSrD   r\   c                  0    e Zd ZU dZded<   ded<   ded<   y)		RunResultuG   runner.run() 의 최종 반환 — terminal decision + callback 결과.r\   decisionrO   callback_firedra   callback_prompt_bytesN)r?   r@   rA   rB   rS   rC   rD   rE   re   re      s    QrD   re   c                      e Zd ZdZdddddded	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZddd	 	 	 	 	 	 	 ddZedd	       Zedd
       Z		 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZ
e	 	 	 	 	 	 	 	 	 	 dd       Zy)CiWatchHandoffRunneru  CI_WATCH_HANDOFF state machine 단일 진입점 (spec §4).

    side-effect 추상화 (test injection 가능):
      - ``ci_status_fn``: ``Callable[[dict], CIStatusSnapshot]`` — 한 poll tick
        에서 caller 가 CI 상태를 회수해서 반환. regression 은 sequence 형태로
        호출되어 PENDING → PASS / FAIL 등을 차례대로 반환.
      - ``gemini_router_call_fn``: ``Callable[[dict], RouterCallResult]`` —
        PR #144 OWNER_GEMINI_TRIGGER_ROUTER stack 호출 wrapper (재사용만).
        본 runner 는 router 자체를 import 하지 않고 wrapper 시그니처로 inject.
      - ``auto_remediation_fn``: ``Callable[[dict, CIStatusSnapshot], str]`` —
        expected_files 내부 medium/style/quality/non-critical HIGH 자동수렴.
        반환은 REMEDIATE_* enum. dev bot 위임 (ANU 직접 코드 0).
      - ``callback_send_fn``: ``Callable[[str], int]`` — envelope text 를 받아
        실제 cokacdir --cron 호출 (또는 mock). 반환 무관 (audit 는 byte 수만 기록).

    runner 자체는 sleep 0 / live shell 0. cadence 는 caller 가 흉내내고 본
    클래스는 state classification 만 책임. 회귀는 ci_status_fn 을 generator
    스타일로 시퀀스 반환하도록 만들어 검증.
    N<   )ci_status_fngemini_router_call_fnauto_remediation_fncallback_send_fnaudit	max_pollsloop_boundary_attemptsc                  |t        d      |t        d      t        |t              r|dk  rt        d      t        |t              r|dk  rt        d      t        |      j	                         | _        || _        || _        |xs t        | _	        || _
        ||nt        | j
                        | _        || _        || _        y )Nz&ci_status_fn callable must be injectedz]gemini_router_call_fn callable must be injected (PR #144 OWNER_GEMINI_TRIGGER_ROUTER wrapper)r   zmax_polls must be positive intz+loop_boundary_attempts must be positive int)r>   
isinstancera   r   resolve_workspace_root_ci_status_fn_gemini_router_call_fnrI   _auto_remediation_fn_callback_send_fnr	   _audit
_max_polls_loop_boundary_attempts)	selfworkspace_rootrl   rm   rn   ro   rp   rq   rr   s	            rE   __init__zCiWatchHandoffRunner.__init__   s     %&NOO (%@  )S)Y!^%&FGG137%*%&STT#N3;;=)&;#$7$U;U!!1&E,?@T@T,U 	 $'=$rD   rQ   )task_idwatcher_schedule_idc               
   t        |      }|d   }|d   }|d   }t        |d         }t        |d         }	| j                  j	                  |||||xs dt
        d| d| d	|d
    d       d}
d}t        | j                        D ]  }|dz   }| j                  |      }t        |t              s!t        dt        |      j                         |j                  t        vr(t        d|j                  dt!        t                     | j                  j	                  |||||xs dt"        |j                  |d| d|j                   d|j$                  xs d d|j&                   d	       |j&                  r5| j)                  t+        t,        d|j                  |
|      |||||	      c S |j                  t.        k(  r@|j                  t0        k(  r|j2                  rU|
| j4                  k\  rF| j)                  t+        t6        d|
 d| j4                   d|j                  |
|      |||||	      c S | j9                  ||      }|t:        vrt        d|dt!        t:                     |
dz  }
| j                  j	                  |||||xs dt<        |j                  |
|d| d|j$                  xs d d d!
       |t>        k(  rJ|t@        k(  r5| j)                  t+        t,        d"|j                  |
|      |||||	      c S |tB        k(  r5| j)                  t+        t6        d#|j                  |
|      |||||	      c S | j)                  t+        tD        d$|j$                  d%tG        |jH                         d&|j                  |
|      |||||	      c S | jK                  |      }t        |tL              s!t        d't        |      j                         | j                  j	                  |||||xs dtN        |jP                  |j                  |d(|jP                   d)|jR                   d*|jT                   d+
       | jW                  |jP                        }|U|jP                  tX        k(  r@| j)                  t+        tZ        d,|jP                  |j                  |
|-      |||||	      c S <| j)                  t+        || j]                  |jP                        |jP                  |j                  |
|-      |||||	      c S  | j)                  t+        t,        d.| j                   d/|
|0      |||||	      S )1uI   state machine 한 번 실행. terminal decision + callback 결과 반환.	pr_numberhead_shawatcher_ownerterminal_statescallback_on_terminal_staterQ   zPR #z! handoff received; watcher_owner=z	; branch=branch)r   r   r   r   r   eventrR   r      z/ci_status_fn must return CIStatusSnapshot, got z%ci_status_fn returned invalid status z
; allowed=z
poll tick z: CI=z; severity=n/az; forbidden_touched=)	r   r   r   r   r   r   r`   rc   rR   uP   forbidden_paths 수정 감지 — Critical7 (spec §3.2 / 회장 verbatim §3.2))r]   rR   r`   rb   rc   )rf   handoffr   r   terminal_subsetcallback_on_terminalzauto_remediation attempts=z >= u;    + same-function HIGH 반복 — LOOP_BOUNDARY (spec §3.5)z-auto_remediation_fn returned invalid outcome zauto_remediation outcome=z (severity=))
r   r   r   r   r   r   r`   rb   rc   rR   uT   auto_remediation refused — forbidden_paths hit / Critical7 escalation (spec §3.2)uR   auto_remediation declared LOOP_BOUNDARY (same-function HIGH 반복) — spec §3.5u'   CI failure non-remediable — severity=z failing_checks=u    (spec §3.4)z8gemini_router_call_fn must return RouterCallResult, got u   router → z; perm_diag=z; )
r   r   r   r   r   r   r_   r`   rc   rR   uJ   CI PASS + Gemini fresh + 0 unresolved + CLEAN — MERGE_READY (spec §3.1)r]   rR   r_   r`   rb   rc   zmax_polls (uG   ) 도달 — terminal 미분류 (max_watch_minutes hard timeout 정합))r]   rR   rb   rc   )/r   setrO   r{   appendr   ranger|   rw   rt   rU   r>   typer?   rV   r   sortedr   rX   rY   	_finalizer\   r   r   r   rZ   r}   r   ry   r&   r
   r   r#   r%   r   listrW   rx   rL   r   rN   rP   rR   _map_router_stater(   r   _terminal_reason_for_router)r~   r   r   r   
normalizedprheadr   r   r   rb   rc   tickci_snapoutcomerouter_resultmappeds                    rE   runzCiWatchHandoffRunner.run   s~    &g.
$*%"?3j):;<#J/K$LM" !.':'@b/2$? O(235	
 %&!$//* m	D"QhO((4Gg'78)EG}--.0  ~~_4);GNN;M N%o679 
 KK&!# $%2+>+D",!('6$_$5U7>>:J K$$+$4$4$=#> ?--4-K-K,LN& --~~-'>C #*..2K(7	 '#(;$3)= &  $ ~~!22~~/ 771T5Q5QQ>>!1+A"<#<"=T#'#?#?"@ AM!M
 '.nn6O,;" !+ ',?(7-A# *  ( 33JH"88-G"+Z7M0N/OQ  *Q.)""#*%'$()6/B/Hb!5%,^^5N+:7y A))0)9)9)BU(C1F$ //55>>!1+B!F '.nn6O,;	" !+ ',?(7-A *  $ 55>>!1+A!M '.nn6O,;	" !+ ',?(7-A *  & ~~-'H((/(8(8'; <..273I3I.J-K L++
 #*..2K(7 '#(;$3)=# &  * !77
CMm-=>)NM*3346 
 KK&!# $%2+>+D".*7*C*C!('6%m&?&?%@ A%%2%Q%Q$RRT(//02& ++M,E,EFF~ !,,<>>!1+?!? 0=/H/H&-nn6O,;
" !+ ',?(7-A! *  & >>)#);;%11 (5'@'@%nn.G$3	 #$7 /%9 "  {m	` ~~%6!$//!2 3> > +D /  3+!5  
 	
rD   c                    | t         t        t        fv ry| t        k(  rt        S | t
        t        t        t        fv rt        S t        S )u   router final_state → CI_WATCH_HANDOFF terminal_state 매핑.

        반환 None 이면 polling continue 또는 MERGE_READY (caller 분기).
        N)
r(   r*   r,   r.   r   r0   r2   r4   r6   r   states    rE   r   z&CiWatchHandoffRunner._map_router_state  sM     \#68LMML 99$*%	
 
 +*&&rD   c                |    | t         k(  r	 y| t        k(  r	 y| t        k(  r	 y| t        k(  r	 y| t        k(  r	 yd| dS )Nue   OWNER nudge 1회 hard limit 후 fresh review 미도착 — GEMINI_EXTERNAL_TRIGGER_STALE (spec §3.3)u   router NUDGE_PERMISSION_DENIED (403 X-Accepted-* headers 기록) — CHAIR_REQUIRED (회장 verbatim §8 / NUDGE_403=permission)ug   router CHAIR_UI_FALLBACK_REQUIRED — CHAIR_REQUIRED (회장 UI 최후수단 회피, 본 정책 §2.10)u_   router NUDGE_FAILED (transient/unknown) — CHAIR_REQUIRED (자동 retry 회피, 회장 보고)u[   router NOT_GEMINI_TRIGGER (PR Review 오인 차단 — task-2640 박제) — CHAIR_REQUIREDzunknown router state u!    — CHAIR_REQUIRED (fail-closed))r.   r2   r0   r4   r6   r   s    rE   r   z0CiWatchHandoffRunner._terminal_reason_for_router$  s|    L = 22R ,,E ''7 --% 'ui/PQQrD   c                  |j                   t        vrt        d|j                   d      |j                   |vrWt        t        d|j                    dt        |       d|j                  |j                  |j                  |j                        }| j                  j                  ||d   |d   |d	   |xs d
t        |j                   |j                  |j                  |j                  |j                  |j                  d       d}d}|r| j                  ||||      }	t        |	j!                  d            }
|
t"        kD  rt        dt"         d|
 d      |
}| j$                  | j%                  |	       d}| j                  j                  ||d   |d   |d	   |xs d
t&        |j                   |d| dt"         dd	       t)        |||      S )u:   terminal 도달 audit + (옵션) callback envelope 발사.zterminal_state z not in enumzcomputed terminal=u$    이 handoff terminal_states subset u.    외부 — CHAIR_REQUIRED escalate (ANCHOR-5)r   r   r   r   rQ   )r   r   r   r   r   r   r]   r_   r`   rb   rc   rR   Fr   )r   rf   r   r   zutf-8zcallback envelope exceeds z bytes (u#   ) — task-2612+3 박제 hard limitTzANU normal callback fired (z bytes UTF-8 <= r   )	r   r   r   r   r   r   r]   rh   rR   )rf   rg   rh   )r]   r   r>   r\   r   r   r_   r`   rb   rc   r{   r   r   rR   _build_callback_envelopelenencoder9   rz   r   re   )r~   rf   r   r   r   r   r   rg   callback_bytesenvelope
byte_counts              rE   r   zCiWatchHandoffRunner._finalizeA  s-    ""*==%!("9"9!<LI 
 ""/9'6()@)@(A B..4_.E-F G99 $,#>#>",,*2*L*L ( 8 8H 	"$[1#J/!(!9':'@b/"*"9"9&.&A&A%//-5-O-O#+#;#;"//	
" 44!$7	 5 H X__W56J88)01M0N O(\)LN  (N%%1&&x0!NKK&!(!5 '
 3%,_%=+>+D"1&.&=&=-;5n5E F:;1>" )"0
 	
rD   c                `   | d   dd }d|xs d d| d    d| d	| d
    d| d    d|xs d d|j                    d|j                  xs d d|j                  xs d d|j                   d|j                   dt
         dt         d|j                   g}dj                  |      S )u#  ANU normal callback envelope (spec §4.3).

        envelope 5축 + canonical_root + terminal_state + watcher_owner + schedule_id
        명시. UTF-8 ≤3900 bytes hard limit (caller wc -c 검증). 상세 보고는
        result.json / report.md 에 위임 (envelope 만 포함).
        r   N   z![CI_WATCH_HANDOFF_TERMINAL] task=r   zpr=#r   z head=zbranch=r   zwatcher_owner=r   zwatcher_schedule_id=rQ   zterminal_state=zrouter_final_state=z
ci_status=zauto_remediation_attempts=zloop_iterations=zcanonical_root=zcollector_role=ANU owner_key=zreason=
)	r]   r_   r`   rb   rc   r<   r;   rR   join)r   rf   r   r   head_prefixliness         rE   r   z-CiWatchHandoffRunner._build_callback_envelope  s	    j)#2./0@5/AB7;'({m<gh'()W_567"#6#<""=>h5567!("="="F!GH++4u56()K)K(LMx7789n-.+,=+>?hoo&'
 yyrD   )r   z
str | Pathrl   z)Callable[[dict], CIStatusSnapshot] | Nonerm   z)Callable[[dict], RouterCallResult] | Nonern   z.Callable[[dict, CIStatusSnapshot], str] | Nonero   zCallable[[str], Any] | Nonerp   zCiWatchHandoffAudit | Nonerq   ra   rr   ra   returnNone)r   dictr   rM   r   r^   r   re   )r   rM   r   r^   )r   rM   r   rM   )rf   r\   r   r   r   rM   r   r^   r   r   r   rO   r   re   )
r   r   rf   r\   r   rM   r   r^   r   rM   )r?   r@   rA   rB   r8   r   r   staticmethodr   r   r   r   rC   rD   rE   rj   rj      s~   0 CGKONR8<,0&D$> #$> @	$>
  I$> L$> 6$> *$> $> !$$> 
$>X *.`
`
 	`

 (`
 
`
H	 ' '$ R R8Z
 #Z
 	Z

 Z
 (Z
 Z
 #Z
 
Z
x    #  	 
 (  
   rD   rj   )r   r   r   r   r   r!   r#   r%   r&   r(   r*   r,   r.   r0   r2   r4   r6   r8   r9   r;   r<   r>   rL   rU   r\   re   rj   N)rG   r   rH   z'CIStatusSnapshot'r   rM   );rB   
__future__r   dataclassesr   pathlibr   typingr   r   r   utils.ci_watch_handoff_auditr	   r
   r   r   r   r   r   utils.ci_watch_handoff_schemar   r   r   r   r   r   r   r   rS   r   r   	frozensetr   r   r!   r#   r%   r&   r(   r*   r,   r.   r0   r2   r4   r6   r8   r9   r;   r<   RuntimeErrorr>   rI   rL   rU   r\   re   rj   __all__rC   rD   rE   <module>r      s  .^ # !  ' '     !* : )#
 ##
 #)27*& 
 !* : )'7 * 7&5  5&5  509 	1 -  #j ""0 Z 0#2 j 2:j :'C * C-F 
 F"0 Z 0(< : <-. 
 . ,0 j / !3 : 22
 2=, =	$	$0	$	$ $   $. . . $   $  K  K \rD   