1- //! Claude Code 文件工具事件到 Rust 快检的受限转换 ;只回传脱敏对话摘要。
1+ //! Claude Code 生命周期事件到 Rust 路由的受限转换 ;只回传脱敏对话摘要。
22
33use crate :: hook_execute_command:: execute_host_input;
44use codeguard_core:: { HookEvent , HookTriggerInput , HookWriteOutcome } ;
@@ -11,21 +11,28 @@ use std::process::ExitCode;
1111const MAX_HOST_INPUT_BYTES : u64 = 1024 * 1024 ;
1212const MAX_CONTEXT_CHARS : usize = 1200 ;
1313
14- /// 从 Claude Code stdin 读取成功的文件工具事件并注入局部检查摘要 。
14+ /// 从 Claude Code stdin 读取生命周期事件并注入受限检查摘要 。
1515/// 参数包含项目根和原 `hook execute` 的显式选项;返回宿主退出码而非质量结论。
1616pub fn run ( args : & [ String ] ) -> ExitCode {
1717 let [ event, rest @ ..] = args else {
1818 eprintln ! ( "需要 Claude Code 事件与项目路径" ) ;
1919 return ExitCode :: from ( 2 ) ;
2020 } ;
21- if event != "post-tool-use" || rest. first ( ) . is_none_or ( |value| value. starts_with ( '-' ) ) {
22- eprintln ! ( "目前仅支持 post-tool-use PATH --timeout DURATION --format=json" ) ;
21+ let Some ( ( host_event, route_event) ) = mapped_event ( event) else {
22+ eprintln ! ( "支持 session-start、post-tool-use、post-tool-use-failure、stop" ) ;
23+ return ExitCode :: from ( 2 ) ;
24+ } ;
25+ if rest. first ( ) . is_none_or ( |value| value. starts_with ( '-' ) ) {
26+ eprintln ! ( "需要 PATH --timeout DURATION --format=json" ) ;
2327 return ExitCode :: from ( 2 ) ;
2428 }
2529 let root = match Path :: new ( & rest[ 0 ] ) . canonicalize ( ) {
2630 Ok ( root) if root. is_dir ( ) => root,
2731 _ => {
28- print_context ( "范围未确定:项目根不可读取;本次源码检查未运行,交付未评估。" ) ;
32+ print_context (
33+ host_event,
34+ "范围未确定:项目根不可读取;本次源码检查未运行,交付未评估。" ,
35+ ) ;
2936 return ExitCode :: SUCCESS ;
3037 }
3138 } ;
@@ -38,41 +45,135 @@ pub fn run(args: &[String]) -> ExitCode {
3845 || raw. is_empty ( )
3946 || raw. len ( ) as u64 > MAX_HOST_INPUT_BYTES
4047 {
41- print_context ( "范围未确定:宿主事件缺失或超出输入预算;本次源码检查未运行,交付未评估。" ) ;
48+ print_context (
49+ host_event,
50+ "范围未确定:宿主事件缺失或超出输入预算;本次源码检查未运行,交付未评估。" ,
51+ ) ;
4252 return ExitCode :: SUCCESS ;
4353 }
4454 let host = match codeguard_adapters:: parse_unique_json ( & raw ) {
4555 Ok ( host) => host,
4656 Err ( _) => {
47- print_context ( "范围未确定:宿主事件格式无效;本次源码检查未运行,交付未评估。" ) ;
48- return ExitCode :: SUCCESS ;
49- }
50- } ;
51- let path = match selected_file ( & root, & host) {
52- Some ( path) => path,
53- None => {
5457 print_context (
55- "范围未确定:成功编辑事件没有可核对的项目内普通文件;本次源码检查未运行,交付未评估。" ,
58+ host_event,
59+ "范围未确定:宿主事件格式无效;本次源码检查未运行,交付未评估。" ,
5660 ) ;
5761 return ExitCode :: SUCCESS ;
5862 }
5963 } ;
64+ if host[ "hook_event_name" ] != host_event || !valid_cwd ( & root, & host) {
65+ print_context (
66+ host_event,
67+ "范围未确定:宿主事件或工作目录不匹配;本次源码检查未运行,交付未评估。" ,
68+ ) ;
69+ return ExitCode :: SUCCESS ;
70+ }
71+ if route_event == HookEvent :: SessionStart
72+ && !matches ! (
73+ host[ "source" ] . as_str( ) ,
74+ Some ( "startup" | "resume" | "clear" | "compact" | "fork" )
75+ )
76+ {
77+ print_context (
78+ host_event,
79+ "范围未确定:会话来源无效;只读发现未运行,交付未评估。" ,
80+ ) ;
81+ return ExitCode :: SUCCESS ;
82+ }
83+ if route_event == HookEvent :: FileChanged
84+ && host_event == "PostToolUseFailure"
85+ && ( !matches ! (
86+ host[ "tool_name" ] . as_str( ) ,
87+ Some ( "Write" | "Edit" | "MultiEdit" )
88+ ) || !host[ "tool_input" ] . is_object ( )
89+ || !host[ "error" ] . is_string ( ) )
90+ {
91+ print_context (
92+ host_event,
93+ "范围未确定:失败编辑事件无效;本次源码检查未运行,交付未评估。" ,
94+ ) ;
95+ return ExitCode :: SUCCESS ;
96+ }
97+ let path = if host_event == "PostToolUse" {
98+ match selected_file ( & root, & host) {
99+ Some ( path) => Some ( path) ,
100+ None => {
101+ print_context (
102+ host_event,
103+ "范围未确定:成功编辑事件没有可核对的项目内普通文件;本次源码检查未运行,交付未评估。" ,
104+ ) ;
105+ return ExitCode :: SUCCESS ;
106+ }
107+ }
108+ } else {
109+ None
110+ } ;
111+ if route_event == HookEvent :: Stop && !host[ "stop_hook_active" ] . is_boolean ( ) {
112+ print_context (
113+ host_event,
114+ "范围未确定:Stop 状态缺失;只读指引未运行,交付未评估。" ,
115+ ) ;
116+ return ExitCode :: SUCCESS ;
117+ }
60118 let input = HookTriggerInput {
61- event : HookEvent :: FileChanged ,
62- changed_paths : vec ! [ path. clone ( ) ] ,
119+ event : route_event ,
120+ changed_paths : path. iter ( ) . cloned ( ) . collect ( ) ,
63121 task_id : None ,
64- write_outcome : HookWriteOutcome :: Confirmed ,
122+ write_outcome : if host_event == "PostToolUseFailure" {
123+ HookWriteOutcome :: Failed
124+ } else {
125+ HookWriteOutcome :: Confirmed
126+ } ,
65127 host_claims_blocking : false ,
66128 } ;
67129 match execute_host_input ( rest, & input) {
68- Ok ( ( report, _) ) => print_context ( & summarize ( & path, & report) ) ,
69- Err ( _) => {
70- print_context ( "CodeGuard:宿主事件参数或路由无效;本次源码检查未运行,交付未评估。" )
130+ Ok ( ( report, _) ) => {
131+ let context = match route_event {
132+ HookEvent :: SessionStart => summarize_discovery ( & report) ,
133+ HookEvent :: Stop => summarize_stop ( & report, host[ "stop_hook_active" ] == true ) ,
134+ HookEvent :: FileChanged if host_event == "PostToolUseFailure" => {
135+ "CodeGuard:写入失败,本次源码检查未运行;请先处理工具错误,交付未评估。"
136+ . to_owned ( )
137+ }
138+ HookEvent :: FileChanged => {
139+ summarize ( path. as_deref ( ) . unwrap_or ( "<changed-file>" ) , & report)
140+ }
141+ _ => "CodeGuard:宿主事件未运行,交付未评估。" . to_owned ( ) ,
142+ } ;
143+ if route_event == HookEvent :: Stop {
144+ print_stop_context (
145+ & context,
146+ safe_task_id ( & report) . is_some ( ) && host[ "stop_hook_active" ] == false ,
147+ ) ;
148+ } else {
149+ print_context ( host_event, & context) ;
150+ }
71151 }
152+ Err ( _) => print_context (
153+ host_event,
154+ "CodeGuard:宿主事件参数或路由无效;本次源码检查未运行,交付未评估。" ,
155+ ) ,
72156 }
73157 ExitCode :: SUCCESS
74158}
75159
160+ fn mapped_event ( event : & str ) -> Option < ( & ' static str , HookEvent ) > {
161+ match event {
162+ "session-start" => Some ( ( "SessionStart" , HookEvent :: SessionStart ) ) ,
163+ "post-tool-use" => Some ( ( "PostToolUse" , HookEvent :: FileChanged ) ) ,
164+ "post-tool-use-failure" => Some ( ( "PostToolUseFailure" , HookEvent :: FileChanged ) ) ,
165+ "stop" => Some ( ( "Stop" , HookEvent :: Stop ) ) ,
166+ _ => None ,
167+ }
168+ }
169+
170+ fn valid_cwd ( root : & Path , host : & Value ) -> bool {
171+ host[ "cwd" ] . as_str ( ) . is_some_and ( |cwd| {
172+ let cwd = Path :: new ( cwd) ;
173+ cwd. is_absolute ( ) && cwd. canonicalize ( ) . is_ok_and ( |cwd| cwd. starts_with ( root) )
174+ } )
175+ }
176+
76177fn selected_file ( root : & Path , host : & Value ) -> Option < String > {
77178 if host[ "hook_event_name" ] != "PostToolUse"
78179 || !matches ! (
@@ -82,10 +183,6 @@ fn selected_file(root: &Path, host: &Value) -> Option<String> {
82183 {
83184 return None ;
84185 }
85- let cwd = Path :: new ( host[ "cwd" ] . as_str ( ) ?) ;
86- if !cwd. is_absolute ( ) || !cwd. canonicalize ( ) . ok ( ) ?. starts_with ( root) {
87- return None ;
88- }
89186 let path = PathBuf :: from ( host[ "tool_input" ] [ "file_path" ] . as_str ( ) ?) ;
90187 if !host[ "tool_response" ] . is_object ( ) {
91188 return None ;
@@ -106,6 +203,63 @@ fn selected_file(root: &Path, host: &Value) -> Option<String> {
106203 Some ( relative. to_owned ( ) )
107204}
108205
206+ fn summarize_discovery ( report : & Value ) -> String {
207+ if report[ "execution" ] != "read_only_discovery" {
208+ return "CodeGuard:会话启动只读发现未运行;源码检查未运行,交付未评估。" . into ( ) ;
209+ }
210+ let languages = report[ "local_feedback" ] [ "languages" ]
211+ . as_array ( )
212+ . into_iter ( )
213+ . flat_map ( |items| items. iter ( ) )
214+ . filter_map ( Value :: as_str)
215+ . filter ( |language| {
216+ language. len ( ) <= 24
217+ && language
218+ . bytes ( )
219+ . all ( |byte| byte. is_ascii_lowercase ( ) || byte == b'-' )
220+ } )
221+ . take ( 8 )
222+ . collect :: < Vec < _ > > ( ) ;
223+ let languages = if languages. is_empty ( ) {
224+ "未识别" . to_owned ( )
225+ } else {
226+ languages. join ( ", " )
227+ } ;
228+ let count = report[ "local_feedback" ] [ "checker_count" ]
229+ . as_u64 ( )
230+ . unwrap_or ( 0 ) ;
231+ let complete = report[ "local_feedback" ] [ "observation_complete" ] == true ;
232+ let status = if complete { "完成" } else { "不完整" } ;
233+ format ! (
234+ "CodeGuard:会话启动只读发现{status};语言 {languages},检查器配置观察 {count} 项。源码检查未运行,交付未评估。"
235+ )
236+ }
237+
238+ fn safe_task_id ( report : & Value ) -> Option < & str > {
239+ report[ "local_feedback" ] [ "task_id" ] . as_str ( ) . filter ( |task| {
240+ task. len ( ) <= 64
241+ && task. starts_with ( "CG-" )
242+ && task[ 3 ..]
243+ . bytes ( )
244+ . all ( |byte| byte. is_ascii_alphanumeric ( ) || byte == b'-' )
245+ } )
246+ }
247+
248+ fn summarize_stop ( report : & Value , already_continuing : bool ) -> String {
249+ if report[ "execution" ] != "read_only_guidance" {
250+ return "CodeGuard:结束时只读指引未运行;请显式执行 codeguard next . --format=json,交付未评估。" . into ( ) ;
251+ }
252+ match safe_task_id ( report) {
253+ Some ( task) if !already_continuing => format ! (
254+ "CodeGuard:待处理任务 {task};请读取 codeguard next . --format=json 并按原检查器复检。结束指引不运行源码检查,交付未评估。"
255+ ) ,
256+ Some ( task) => format ! (
257+ "CodeGuard:待处理任务 {task};本轮已在 Stop 后继续,避免重复唤醒。结束指引不运行源码检查,交付未评估。"
258+ ) ,
259+ None => "CodeGuard:没有可展示的待处理任务不代表检查通过;交付前仍需新鲜完整检查。结束指引不运行源码检查,交付未评估。" . into ( ) ,
260+ }
261+ }
262+
109263fn summarize ( path : & str , report : & Value ) -> String {
110264 let label = if path. len ( ) <= 128
111265 && path
@@ -171,15 +325,35 @@ fn summarize(path: &str, report: &Value) -> String {
171325 summary. chars ( ) . take ( MAX_CONTEXT_CHARS ) . collect ( )
172326}
173327
174- fn print_context ( context : & str ) {
175- println ! (
176- "{}" ,
177- json!( {
178- "hookSpecificOutput" : {
179- "hookEventName" : "PostToolUse" ,
180- "additionalContext" : context
181- } ,
182- "systemMessage" : "CodeGuard 局部检查反馈"
183- } )
184- ) ;
328+ fn print_context ( event : & str , context : & str ) {
329+ if event == "Stop" {
330+ println ! ( "{}" , json!( { "systemMessage" : context} ) ) ;
331+ } else {
332+ let label = match event {
333+ "SessionStart" => "CodeGuard 项目发现反馈" ,
334+ "PostToolUseFailure" => "CodeGuard 写入失败反馈" ,
335+ _ => "CodeGuard 局部检查反馈" ,
336+ } ;
337+ println ! (
338+ "{}" ,
339+ json!( {
340+ "hookSpecificOutput" : { "hookEventName" : event, "additionalContext" : context} ,
341+ "systemMessage" : label
342+ } )
343+ ) ;
344+ }
345+ }
346+
347+ fn print_stop_context ( context : & str , continue_once : bool ) {
348+ if continue_once {
349+ println ! (
350+ "{}" ,
351+ json!( {
352+ "hookSpecificOutput" : { "hookEventName" : "Stop" , "additionalContext" : context} ,
353+ "systemMessage" : "CodeGuard 待处理任务指引"
354+ } )
355+ ) ;
356+ } else {
357+ println ! ( "{}" , json!( { "systemMessage" : context} ) ) ;
358+ }
185359}
0 commit comments