@@ -39,6 +39,7 @@ class Summary:
3939 restored_branch : str | None = None
4040 restoration_note : str | None = None
4141 outcome : str = ""
42+ changed_files_label : str = "Files changed"
4243 changed_files : list [str ] = field (default_factory = list )
4344 commits : list [str ] = field (default_factory = list )
4445 push_result : str | None = None
@@ -63,7 +64,7 @@ def print_text(self) -> None:
6364 for failure in self .failures :
6465 print (f"- { failure } " )
6566 if self .changed_files :
66- print ("Files changed :" )
67+ print (f" { self . changed_files_label } :" )
6768 for path in self .changed_files :
6869 print (f"- { path } " )
6970 if self .commits :
@@ -177,7 +178,16 @@ def restore_original_branch(summary: Summary) -> None:
177178 try :
178179 branch = current_branch ()
179180 except WorkflowError as e :
180- summary .restoration_note = str (e )
181+ if merge_in_progress ():
182+ summary .restoration_note = "not restored because a merge is in progress"
183+ return
184+ if status_porcelain ().strip ():
185+ summary .restoration_note = "not restored because the working tree has uncommitted changes"
186+ return
187+ progress (f"Restoring original branch from detached HEAD: { summary .original_branch } " )
188+ git (["checkout" , summary .original_branch ], summary )
189+ summary .restored_branch = summary .original_branch
190+ summary .restoration_note = "restored from detached HEAD"
181191 return
182192 if branch == summary .original_branch :
183193 summary .restored_branch = branch
@@ -240,14 +250,22 @@ def checkout_pr_no_push_check(pr: int, summary: Summary) -> None:
240250 remember_pr_url (pr_view (pr , summary ), summary )
241251
242252
243- def run_pr_workflow (pr : int , body : Callable [[Summary ], int ], * , push_required : bool = True ) -> int :
253+ def run_pr_workflow (
254+ pr : int ,
255+ body : Callable [[Summary ], int ],
256+ * ,
257+ push_required : bool = True ,
258+ checkout_required : bool = True ,
259+ ) -> int :
244260 summary = Summary (pr = pr )
245261 try :
246262 require_clean_worktree (summary )
247263 summary .original_branch = current_branch (summary )
264+ if push_required and not checkout_required :
265+ raise WorkflowError ("push workflows require checking out the PR branch" )
248266 if push_required :
249267 checkout_pr (pr , summary )
250- else :
268+ elif checkout_required :
251269 checkout_pr_no_push_check (pr , summary )
252270 return body (summary )
253271 except Exception as e :
@@ -328,8 +346,22 @@ def extract_job_id(check: dict[str, Any]) -> int | None:
328346 return int (value ) if isinstance (value , int ) else None
329347
330348
331- def invoke_copilot (prompt : str , summary : Summary ) -> str :
332- cmd = ["copilot" , "-p" , prompt , "--allow-all-tools" , "--model" , COPILOT_MODEL ]
349+ def invoke_copilot (
350+ prompt : str ,
351+ summary : Summary ,
352+ event_log_path : Path | None = None ,
353+ tool_usage_path : Path | None = None ,
354+ * ,
355+ allow_all_tools : bool = True ,
356+ extra_args : list [str ] | None = None ,
357+ ) -> str :
358+ cmd = ["copilot" , "-p" , prompt , "--model" , COPILOT_MODEL ]
359+ if allow_all_tools :
360+ cmd .append ("--allow-all-tools" )
361+ if extra_args is not None :
362+ cmd .extend (extra_args )
363+ if event_log_path is not None :
364+ cmd .extend (["--output-format" , "json" ])
333365 progress (f"Handing off to Copilot CLI using { COPILOT_MODEL } ; streaming output below" )
334366 proc = subprocess .Popen (
335367 cmd ,
@@ -342,19 +374,71 @@ def invoke_copilot(prompt: str, summary: Summary) -> str:
342374 bufsize = 1 ,
343375 )
344376 output_parts : list [str ] = []
377+ message_parts : list [str ] = []
378+ tool_uses : list [dict [str , Any ]] = []
379+ event_log = event_log_path .open ("w" , encoding = "utf-8" ) if event_log_path is not None else None
345380 if proc .stdout is not None :
346- for line in proc .stdout :
347- print (line , end = "" , flush = True )
348- output_parts .append (line )
381+ try :
382+ for line in proc .stdout :
383+ if event_log is not None :
384+ event_log .write (line )
385+ event_log .flush ()
386+ output_parts .append (line )
387+ try :
388+ event = json .loads (line )
389+ except json .JSONDecodeError :
390+ continue
391+ event_type = event .get ("type" )
392+ data = event .get ("data" ) or {}
393+ if event_type == "assistant.message" :
394+ content = data .get ("content" )
395+ if isinstance (content , str ) and content :
396+ print (content , flush = True )
397+ message_parts .append (content )
398+ elif event_type == "tool.execution_start" :
399+ tool_name = data .get ("toolName" )
400+ if isinstance (tool_name , str ):
401+ tool_uses .append ({"tool" : tool_name , "arguments" : data .get ("arguments" )})
402+ continue
403+ print (line , end = "" , flush = True )
404+ output_parts .append (line )
405+ finally :
406+ if event_log is not None :
407+ event_log .close ()
349408 returncode = proc .wait ()
350- output = "" .join (output_parts )
409+ output = "\n " . join ( message_parts ) if event_log_path is not None else " " .join (output_parts )
351410 if returncode != 0 :
352411 raise subprocess .CalledProcessError (
353412 returncode ,
354- ["copilot" , "-p" , "<generated prompt>" , "--allow-all-tools" , "-- model" , COPILOT_MODEL ],
355- output ,
413+ ["copilot" , "-p" , "<generated prompt>" , "--model" , COPILOT_MODEL ],
414+ "" . join ( output_parts ) ,
356415 "" ,
357416 )
417+ if tool_usage_path is not None :
418+ counts : dict [str , int ] = {}
419+ for tool_use in tool_uses :
420+ tool = tool_use ["tool" ]
421+ counts [tool ] = counts .get (tool , 0 ) + 1
422+ tool_usage_path .write_text (
423+ json .dumps (
424+ {
425+ "tools" : [{"tool" : tool , "count" : count } for tool , count in sorted (counts .items ())],
426+ "tool_uses" : tool_uses ,
427+ },
428+ indent = 2 ,
429+ sort_keys = True ,
430+ )
431+ + "\n " ,
432+ encoding = "utf-8" ,
433+ )
434+ summary .notes .append (f"Copilot tool usage summary: { display_path (str (tool_usage_path ))} " )
435+ if counts :
436+ tools = ", " .join (f"{ tool } ({ count } )" for tool , count in sorted (counts .items ()))
437+ summary .notes .append (f"Copilot tools used: { tools } " )
438+ else :
439+ summary .notes .append ("Copilot tools used: none" )
440+ if event_log_path is not None :
441+ summary .notes .append (f"Copilot event log: { display_path (str (event_log_path ))} " )
358442 progress ("Copilot CLI handoff completed" )
359443 return output .strip ()
360444
0 commit comments