55import argparse
66import base64
77import binascii
8+ import contextlib
89import dataclasses
910import datetime as dt
1011import hashlib
@@ -83,6 +84,34 @@ def _validate_provider(provider: str) -> str:
8384 return normalized
8485
8586
87+ def _resolve_child_path (root : Path , path : str | Path , * , description : str ) -> Path :
88+ root_resolved = root .resolve ()
89+ raw_path = Path (path )
90+ candidate = raw_path if raw_path .is_absolute () else root_resolved / raw_path
91+ candidate = candidate .resolve ()
92+ try :
93+ candidate .relative_to (root_resolved )
94+ except ValueError as exc :
95+ raise ValueError (f"{ description } must stay within { root_resolved } " ) from exc
96+ return candidate
97+
98+
99+ def _resolve_reference_checkout_path (workspace_path : Path , checkout_path : str | Path ) -> Path :
100+ reference_root = (workspace_path / ".reference" ).resolve ()
101+ candidate = _resolve_child_path (
102+ workspace_path ,
103+ checkout_path ,
104+ description = "reference checkout path" ,
105+ )
106+ try :
107+ candidate .relative_to (reference_root )
108+ except ValueError as exc :
109+ raise ValueError ("reference checkout path must stay within .reference" ) from exc
110+ if candidate == reference_root :
111+ raise ValueError ("reference checkout path must identify a child of .reference" )
112+ return candidate
113+
114+
86115def _runner_key (pr_number : int , head_sha : str , provider : str ) -> str :
87116 payload = f"{ provider } :{ pr_number } :{ head_sha } "
88117 return hashlib .sha256 (payload .encode ("utf-8" )).hexdigest ()
@@ -314,7 +343,7 @@ def _materialize_single_checkout_plan(
314343 )
315344 _run_git (["git" , "-C" , str (clone_dir ), "sparse-checkout" , "reapply" ], env = git_env )
316345
317- destination_root = workspace_path / checkout_path
346+ destination_root = _resolve_reference_checkout_path ( workspace_path , checkout_path )
318347 if destination_root .exists ():
319348 shutil .rmtree (destination_root )
320349 destination_root .mkdir (parents = True , exist_ok = True )
@@ -356,11 +385,6 @@ def materialize_orchestrator_skill(
356385 return None
357386
358387 if plan .pack :
359- materialize_reference_packs (
360- workspace_path ,
361- reference_pack_name = plan .pack ,
362- token = token ,
363- )
364388 reference_packs = _load_reference_packs_module ()
365389 snapshot = reference_packs .load_reference_packs (workspace_path )
366390 matching = [
@@ -370,7 +394,17 @@ def materialize_orchestrator_skill(
370394 ]
371395 if not matching :
372396 raise ValueError (f"orchestrator skill reference pack not found: { plan .pack } " )
373- checkout_path = workspace_path / matching [0 ].checkout_path
397+ checkout_path = _resolve_reference_checkout_path (
398+ workspace_path ,
399+ matching [0 ].checkout_path ,
400+ )
401+ with contextlib .suppress (FileNotFoundError ):
402+ shutil .rmtree (checkout_path )
403+ materialize_reference_packs (
404+ workspace_path ,
405+ reference_pack_name = plan .pack ,
406+ token = token ,
407+ )
374408 else :
375409 checkout_path = _materialize_single_checkout_plan (
376410 workspace_path ,
@@ -413,12 +447,23 @@ def assemble_prompt(
413447 )
414448
415449 if context .get ("materialize_orchestrator_skill" ):
416- materialize_orchestrator_skill (
450+ orchestrator_summary_path = materialize_orchestrator_skill (
417451 workspace ,
418452 pack_override = context .get ("orchestrator_skill_pack" ) or None ,
419453 enabled_override = context .get ("orchestrator_skill_enabled" ),
420454 token = token ,
421455 )
456+ else :
457+ orchestrator_summary_raw = context .get ("orchestrator_skill_summary_path" )
458+ orchestrator_summary_path = (
459+ Path (str (orchestrator_summary_raw )) if orchestrator_summary_raw else None
460+ )
461+ if orchestrator_summary_path :
462+ orchestrator_summary_path = _resolve_child_path (
463+ workspace ,
464+ orchestrator_summary_path ,
465+ description = "orchestrator_skill_summary_path" ,
466+ )
422467
423468 output_file = str (
424469 context .get ("output_file" ) or _prompt_output_name (provider , context .get ("pr_number" ))
@@ -448,12 +493,11 @@ def assemble_prompt(
448493 if reference_summary .is_file ():
449494 parts .extend (["\n \n ## Reference Packs\n " , _read_text (reference_summary ).rstrip ()])
450495
451- orchestrator_summary = workspace / ".reference" / "ORCHESTRATOR_SKILL.md"
452- if orchestrator_summary .is_file ():
496+ if orchestrator_summary_path and orchestrator_summary_path .is_file ():
453497 parts .extend (
454498 [
455499 "\n \n ## Orchestrator Skill Context\n " ,
456- _read_text (orchestrator_summary ).rstrip (),
500+ _read_text (orchestrator_summary_path ).rstrip (),
457501 ]
458502 )
459503
@@ -504,7 +548,14 @@ def _parse_jsonl_output(raw_output: str) -> tuple[list[str], list[str]]:
504548 event_type = str (event .get ("type" ) or event .get ("status" ) or "" ).lower ()
505549 text = _extract_text_from_json_event (event )
506550 if "error" in event_type or event .get ("error" ):
507- errors .append (text or json .dumps (event , sort_keys = True ))
551+ error_value = event .get ("error" )
552+ nested_error = (
553+ _extract_text_from_json_event (error_value )
554+ if isinstance (error_value , dict )
555+ else None
556+ )
557+ direct_error = error_value .strip () if isinstance (error_value , str ) else None
558+ errors .append (direct_error or nested_error or text or json .dumps (event , sort_keys = True ))
508559 elif text :
509560 messages .append (text )
510561 if not parsed_any :
@@ -522,7 +573,7 @@ def parse_runner_output(provider: str, raw_output: str) -> RunnerResult:
522573 clipped = raw [:64000 ] if len (raw ) > 64000 else raw
523574
524575 messages , errors = _parse_jsonl_output (clipped ) if provider == "codex" else ([], [])
525- final_message = messages [- 1 ] if messages else clipped .strip ()
576+ final_message = errors [ 0 ] if errors else ( messages [- 1 ] if messages else clipped .strip () )
526577
527578 if not errors and re .search (
528579 r"(^::error::|\bTraceback\b|\bError:|\bException\b)" ,
@@ -943,6 +994,7 @@ def _cmd_assemble(args: argparse.Namespace) -> int:
943994 "materialize_orchestrator_skill" : args .materialize_orchestrator_skill ,
944995 "orchestrator_skill_pack" : args .orchestrator_skill_pack or None ,
945996 "orchestrator_skill_enabled" : _parse_optional_bool (args .orchestrator_skill_enabled ),
997+ "orchestrator_skill_summary_path" : os .environ .get ("ORCHESTRATOR_SKILL_SUMMARY_PATH" ),
946998 "github_token" : os .environ .get ("GH_TOKEN" ) or os .environ .get ("GITHUB_TOKEN" ),
947999 }
9481000 prompt = assemble_prompt (args .reference_pack_name , context , args .provider )
0 commit comments