
    #j'P                       d Z ddlm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 ddlmZ e
j                  j!                  dej                  j#                  ej                  j%                  e      d             	 ddlmZmZmZmZ  ej6                  d      Zd	Zd
ZdZdZ dZ!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f	 	 	 	 	 	 	 d$dZ*	 	 	 	 	 	 	 	 	 	 	 	 d%dZ+	 	 	 	 	 	 	 	 	 	 d&dZ,ddddef	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d'dZ-d(d)dZ.e/dk(  r e0 e.             y# e$ r ddlmZmZmZmZ Y w xY w)*u'  
gemini_cli_gate_check.py — G4 Pre-PR Gemini CLI Gate (task-2562)

회장 §명시 2026-05-12 Track C 박제:
- Pre-PR Gemini CLI 단발 gate (OAuth-personal). 공식 merge gate 아님.
- GitHub Gemini App은 PR open 후 자동 호출되는 공식 merge gate로 별도 유지.
- 본 모듈은 PR open 전 single shot 호출로 code-changing issue를 사전 감지한다.

핵심 결정 박제:
- fix_loop_count max = 2 (회장 §명시)
- Lv 기반 mixed gate
    Lv.1~2  : soft (warning + PR open 허용)
    Lv.2    : risk-trigger hard (보안 민감 파일 / affected_files > 5 / danger 키워드)
    Lv.3+   : hard (.done 차단 + PR open 금지)
- scope_violation = True → 즉시 ESCALATED (fix_loop 진입 0)
- API key 사용 금지 (GEMINI_API_KEY env var 감지 시 즉시 abort)
- codex_gate_check.py 60% 재사용 (helper utilities)

OAuth-personal 사용 범위:
- gemini CLI 0.31.0 binary 호출 (~/.config/gemini/oauth_token.json auto-refresh)
- 단발 호출 (per-task 1회 입력 → JSON 결과 1회 출력)
- long polling 0 (max wait < 60s)
    )annotationsN)datetimetimezone)Anyz..)_extract_json_from_output_normalize_affected_item_read_file_safe_detect_workspace_rootgemini_cli_gate_checkgemini<      )u   인증u   결제u   보안u   권한PIIu   개인정보u   API키u   토큰authpaymentsecrettoken
credential)zauth/zpayment/z	dispatch/owner_triggerz.envg3_independent_verifierexecutor_schedulerc                 z    t         j                  j                  dd      j                         } | rt	        d      y)u6   API key 사용을 즉시 차단 (회장 §명시 1:1).GEMINI_API_KEY ux   FORBIDDEN_CAPABILITY_USE: GEMINI_API_KEY env var detected — G4 gate는 OAuth-personal 강제 (회장 §명시 박제).N)osenvirongetstripRuntimeError)api_keys    Q/home/jay/workspace/.worktrees/task-2729+10-dev6/scripts/gemini_cli_gate_check.py_assert_oauth_personal_onlyr"   W   s;    jjnn-r288:GI
 	
     c                R   t         j                  j                  | | d      }t         j                  j                  |      sy	 t	        |dd      5 }t        |j                         j                         xs d      cd d d        S # 1 sw Y   y xY w# t        t        f$ r Y yw xY w)N.g4-fix-loop-countr   rutf-8encoding0)
r   pathjoinisfileopenintreadr   OSError
ValueError)
events_dirtask_idr+   fs       r!   _read_fix_loop_countr6   a   s    77<<
wi/A$BCD77>>$$g. 	0!qvvx~~'.3/	0 	0 	0Z  s0   B +B>	B BB B B&%B&c                (   t        j                  | d       t         j                  j                  | | d      }|dz   }t	        |dd      5 }|j                  t        |             d d d        t        j                  ||       y # 1 sw Y    xY w)NTexist_okr%   .tmpwr'   r(   )r   makedirsr+   r,   r.   writestrreplace)r3   r4   countr+   tmpr5   s         r!   _write_fix_loop_countrB   l   sv    KK
T*77<<
wi/A$BCD
-C	c3	) Q	E
JJsD s   BBc                    | syt        j                  d|       }|sy|j                  d      j                         }t        j                  d|      ryd|v sd|v sd|v ryy)	u:   task md에서 레벨 추출. 'lv1'/'lv2'/'lv3plus' 반환.lv1u   ##\s*레벨\s*\n([^\n]+)   z+lv\.?\s*[34]|level\s*[34]|critical|securitylv3pluszlv.2lv2zlevel 2)researchgrouplower)task_contentmraws      r!   _detect_level_from_task_filerO   u   sa    
		-|<A
''!*


C	yy?E}	S(8r#   c                r    | D ]2  }t        |      \  }}|j                         }t        D ]	  }||v s  y 4 y)NTF)r   rK   SECURITY_SENSITIVE_FRAGMENTS)affected_filesitemr+   _lowfrags         r!   _has_security_sensitive_filesrW      sH     *40ajjl0 	Ds{	 r#   c                T    | sg S g }t         D ]  }|| v s|j                  |        |S N)DANGER_KEYWORDSappend)rL   foundkws      r!   _has_danger_keywordsr^      s9    	E LL Lr#   c                \   | dk(  rdd| dS g }| dk(  r{t        |      r|j                  d       t        |      dkD  r|j                  dt        |       d	       t        |      }|r&|j                  d
dj	                  |dd               |rdddj	                  |      z   | dS dd| dS )u|   
    Lv 기반 mixed gate 분기 결정.

    Returns dict with keys: mode ("soft"|"hard"), trigger (str), level (str).
    rF   hardzlv3+_required)modetriggerlevelrG   security_sensitive_file   zaffected_files>5 (=)zdanger_keywords=,N   zlv2_risk_trigger:+softlv1_or_lv2_no_risk)rW   r[   lenr^   r,   )rc   rR   rL   triggerskwss        r!   _classify_gate_modero      s     	?UKKH~(8OO56~"OO1#n2E1FaHI"<0OO.sxxBQ/@.ABC+>(AS+S^cdd';eLLr#   c                    |sdg fS |D ch c]#  }|j                         s|j                         % }}g }| D ]&  }t        |      \  }}||vs|j                  |       ( t        |      |fS c c}w )uL   expected_files 외 파일이 affected_files에 포함되면 scope_violation.F)r   r   r[   bool)rR   expected_filespexpected_setextrasrS   r+   rT   s           r!   _detect_scope_violationrv      s    
 by'5C!AGGICLCF  *40a|#MM$  < Ds
   A/A/c           	        t         j                  j                         }|j                  dd       |j                  dd       	 t	        j
                  |g| ddt        ||      }|j                  dk7  r!dd|j                   d|j                  dd	  fS t        |j                        }|y
|dfS # t        $ r Y yt        j                  $ r ddt         dfcY S t        $ r'}ddt        |      j                   d| fcY d}~S d}~ww xY w)u~   Gemini CLI 0.31.0 binary 단발 호출 (OAuth-personal, stdin).

    Returns (parsed_json or None, error_reason or None).
    r   NGOOGLE_API_KEYT)inputcapture_outputtexttimeoutcwdenvr   zgemini_cli_nonzero_exit rc=z stderr=   )Ngemini_cli_json_parse_fail)Ngemini_cli_binary_not_foundzgemini_cli_timeout (>zs)zgemini_cli_exception::)r   r   copypop
subprocessrunGEMINI_TIMEOUT_S
returncodestderrr   stdoutFileNotFoundErrorTimeoutExpired	Exceptiontype__name__)prompt
target_dir
gemini_binr~   resultparsedes          r!   _run_gemini_clir      s%    **//
CGGd#GGd#DL$
 !6v7H7H6IRXR_R_`dadReQfggg*6==9>5t| 32$$ B,-=,>bAAA D,T!W-=-=,>asCCCDs7   AB1 B1 -B1 1	D<DD$D DDc                   t         j                  d||       g }g }t        j                  j	                  |       s|j                  dd|  d       n2t        |       }|j                         s|j                  dd|  d       |D ]  }t        |      \  }	}
t        j                  j                  |	      r|	nt        j                  j                  ||	      }t        j                  j	                  |      rr|
r|j                  dd|	 d       |j                  dd	|	 d        |s|j                  d
       t        d |D              }| ||d|dS )uT   Gemini CLI 호출 실패 시 규칙 기반 fallback (codex maat fallback과 동질).z,gemini fallback: workspace_root=%s reason=%scriticalu   설계 문서 누락: severitydescriptionu   설계 문서 비어있음: infou"   신규 파일 (아직 미생성): highu   파일 누락 또는 오타: u'   Gemini CLI 폴백: 정적 검증 통과c              3  ,   K   | ]  }|d    dk(    yw)r   r   N .0r&   s     r!   	<genexpr>z)_gemini_fallback_check.<locals>.<genexpr>  s     Bqq}
2Bs   gemini_fallback_static)passriskssuggestionssourcefallback_reason)loggerdebugr   r+   r-   r[   r	   r   r   isabsr,   any)	task_filerR   workspace_rootr   r   r   r   contentrS   	file_pathis_newresolvedhas_criticals                r!   _gemini_fallback_checkr      sX    LL?Q`a"$EK77>>)$&!7	{C	
 "),}}LL *%A)#M  4T:	6 "i 89bggll:W`>aww~~h'$*)KI;'W $*)Fyk'R& DEBEBBL  "** r#   c           	        t        j                  | d       ddddd}|j                  |d| d	      }t         j                  j	                  | | |       }|d
z   }t        |dd      5 }t        j                  i |||t        j                  t        j                        j                         d|dd       ddd       t        j                  ||       |S # 1 sw Y   !xY w)uk  
    G4 marker 파일 생성 (atomic write — _write_fix_loop_count 와 동일 패턴).

    marker_kind:
        "soft"  → memory/events/<task_id>.g4-soft-warning.json
        "fail"  → memory/events/<task_id>.g4-failed
        "scope" → memory/events/<task_id>.g4-scope-violation.json
        "cap"   → memory/events/<task_id>.g4-fix-loop-cap.json
    Tr8   z.g4-soft-warning.jsonz
.g4-failedz.g4-scope-violation.jsonz.g4-fix-loop-cap.json)rj   failscopecapz.g4-z.jsonr:   r;   r'   r(   )r4   marker_kindts_utcFr   ensure_asciiindentN)r   r<   r   r+   r,   r.   jsondumpr   nowr   utc	isoformatr?   )	r3   r4   r   payload
suffix_mapsuffixr+   rA   r5   s	            r!   _save_g4_markerr   )  s     KK
T*'+&	J ^^K4}E)BCF77<<
wix$89D
-C	c3	) 
Q		"*",,x||4>>@	 
	

 JJsDK
 
s   ,ACC&c                   t                |t        |       }t        |t              sJ t        j
                  j                  |dd      }|xs |}t        |       }	t        |	      }
t        |
||	      }|d   }|d   }t        ||      \  }}|rt        ||      nd}|rPd}|r t        ||d|t        |xs g       |dd	      }d
dd|
ddt        |       d|dd  dgdgdd|t        d|d|dS |rD|t        k\  r;t        ||d|t        dd      }d
dd|
dd| dt         ddgdgdd|t        d
g d|dS g }|D ]  }t!        |      \  }}t        j
                  j#                  |      r|nt        j
                  j                  ||      }t        j
                  j%                  |      s|j'                  d| d        t        |      }|j'                  d| d!|         |rd"j                  |      nd#}d$|	 d%| d&}t)        |||'      \  }}|-t+        | |||xs d(|      }|d)   }|d*   }|d+   }|d,   } |d-   }!n=|j-                  d*g       }|j-                  d+g       }t/        d. |D              }"|" }d/} d}!d}#d0}$|}%|r|d1z   }%t1        |||%       |s4|d2k(  rd0}$|r+t        ||d2||||%d3      }#nd4}$|rt        ||d5||||%d3      }#||||
||| |!|%t        d
g |$|#dS )6u9  
    G4 Pre-PR Gemini CLI gate 메인 함수.

    Returns dict:
        pass: bool
        gate_mode: "soft" | "hard"
        gate_trigger: str
        level: "lv1" | "lv2" | "lv3plus"
        risks: list
        suggestions: list
        source: "gemini_cli" | "gemini_fallback_static"
        fallback_reason: str | None
        fix_loop_count: int
        fix_loop_max: int
        scope_violation: bool
        scope_violation_extras: list[str]
        action: "PR_OPEN_ALLOWED" | "PR_OPEN_BLOCKED" | "ESCALATED_OWNER_DECISION"
        marker_path: str | None
    Nmemoryeventsra   rb   r   r   u7   scope_violation → 즉시 ESCALATED, fix_loop 진입 0)scope_violation_extrasrr   fix_loop_count	rationaleFr`   scope_violationr   u+   scope_violation: expected_files 외 파일 u   개 — re   r   uM   expected_files 외 파일 제거 후 재시도 또는 owner_decision_requiredstatic_pre_checkTESCALATED_OWNER_DECISION)r   	gate_modegate_triggerrc   r   r   r   r   r   fix_loop_maxr   r   actionmarker_pathr   u2   FIX_LOOP_CAP_VIOLATION → OWNER_DECISION_REQUIRED)r   r   r   fix_loop_capzfix_loop_count=z >= max=u    — OWNER_DECISION_REQUIREDu/   회장 결정 필요 (fix_loop hard cap 도달)z--- u(    ---
(파일 없음 또는 디렉토리)z ---
z

u   (영향받는 파일 없음)u   다음 설계 문서와 코드를 Pre-PR 검증으로 리뷰하세요. JSON으로만 응답하세요.

응답 형식:
{"risks": [{"severity": "critical|high|medium|low", "description": "..."}], "suggestions": ["..."]}

설계 문서:
u   

영향받는 코드:

)r   unknownr   r   r   r   r   c              3  h   K   | ]*  }|j                  d       xs dj                         dk(   , yw)r   r   r   N)r   rK   r   s     r!   r   z(gemini_cli_gate_check.<locals>.<genexpr>  s1      
@AQUU:$"++-;
s   02
gemini_cliPR_OPEN_ALLOWEDrE   rj   )r   r   r   r   PR_OPEN_BLOCKEDr   )r"   r
   
isinstancer>   r   r+   r,   r	   rO   ro   rv   r6   r   listrl   FIX_LOOP_MAXr   r   r-   r[   r   r   r   r   rB   )&r   rR   r   r4   rr   r   r   r3   effective_targetrL   rc   
gate_classr   r   scope_vscope_extrasfix_loop_nowmarker
code_partsrS   r   rT   r   r   code_contentr   r   errfallback_result	gate_passr   r   r   r   r   r   r   	new_counts&                                         r!   r   r   R  s   8  !/	:nc***nhAJ!3^"9-L(6E$UNLIJ6"Ii(L 4NNSG\@G'
G<QL$.:&*>+?R&@&2!Z		
F - !+%PQTUaQbPccklxy{z{l|k}#~ ll(#*(#&20!'
 	
. <</ ". ,Q		
 * !+%4\N(<.Xt#u NN(#*($&(0!'
 	
. J =/5	1 "i 89bggllK[]f>gww~~h'YK/XYZ!(+D6';<= /96;;z*>\L	 (.(B<.PR	T  "&*:zRKFC~0~~s7GiIY
 $F+	(%m4 *)*;<

7B'jj3 
EJ
 
 %$	 #KFI 1$	j'9=&F-!&'2(4*3		
 'F-!&'2(4*3		
 $"*#$ "$" r#   c           
     X   t        j                  d      }|j                  dd        |j                  dd        |j                  ddg        |j                  d	dd        |j                  d
ddd        |j                  dd        |j                  dt               |j	                  |       }t        j                  t
        j                  d       	 t                |j                  }|K|j                  r?|j                   xs d}t"        j$                  j'                  |dd|j                   d      }||j)                  d       t+        ||j,                  |j                   |j                  |j.                  |j0                  |j2                        }t        t        j                  |dd             |j5                  d      dk(  ry|j5                  d      dk(  ry y!# t        $ r5}t        t        j                  ddt        |      d             Y d }~yd }~ww xY w)"Nz7G4 Pre-PR Gemini CLI Gate (OAuth-personal, single shot))r   z	--task-id)defaultz--task-filez--affected-files*)nargsr   z--expected-filesz--workspace-rootz--workspacer   )destr   z--target-dirz--gemini-binz1%(asctime)s [%(levelname)s] %(name)s: %(message)s)rc   formatFABORTED)r   r   errorr   z/home/jay/workspacer   tasksz.mdu#   --task-file 또는 --task-id 필요)r   rR   r   r4   rr   r   r   r   r   r   r   rE   r   )argparseArgumentParseradd_argumentGEMINI_BIN_DEFAULT
parse_argsloggingbasicConfigINFOr"   r   printr   dumpsr>   r   r4   r   r   r+   r,   r   r   rR   rr   r   r   r   )argvparserargsexcr   wsr   s          r!   mainr  (  s   $$MF T2
t4
*#rB
*#tD
*M@PZ^_
5
0BCT"Dll#V
#%
 IT\\  9$9GGLLXw4<<.8LM	:;"******????F 
$**V%
:;zz(99zz(005  djj%9s3xPQRs   
G+ +	H)4+H$$H)__main__)returnNone)r3   r>   r4   r>   r  r/   )r3   r>   r4   r>   r@   r/   r  r  )rL   r>   r  r>   )rR   	list[Any]r  rq   )rL   r>   r  z	list[str])rc   r>   rR   r  rL   r>   r  dict[str, Any])rR   r  rr   list[str] | Noner  ztuple[bool, list[str]])r   r>   r   r>   r   r>   r  ztuple[dict | None, str | None])r   r>   rR   r  r   r>   r   r>   r   r>   r  r  )
r3   r>   r4   r>   r   r>   r   r  r  r>   )r   r>   rR   r  r   
str | Noner4   r
  rr   r	  r   r
  r   r>   r  r  rY   )r   r	  r  r/   )1__doc__
__future__r   r   r   r   r   rH   r   sys_sysr   r   typingr   r+   insertr,   dirname__file__scripts.codex_gate_checkr   r   r	   r
   ImportErrorcodex_gate_check	getLoggerr   r   r   r   rZ   rQ   r"   r6   rB   rO   rW   r^   ro   rv   r   r   r   r   r  r   
SystemExitr   r#   r!   <module>r     sY  0 #    	 	   '  		  BGGLL!:DA B  
		2	3    
MMM M 	M:  $   & )#D#D#D #D $	#DL;;; ; 	;
 ; ;|&&& & 	&
 	&X "&'+!(SSS S 	S
 %S S S Sl.b z
TV
 U   s   D5 5E	E	