2626 "feature:voice" ,
2727}
2828
29+ DETERMINISTIC_LABELS : Final [set [str ]] = {
30+ "documentation" ,
31+ "project" ,
32+ "dependencies" ,
33+ }
34+
35+ MODEL_ONLY_LABELS : Final [set [str ]] = {
36+ "bug" ,
37+ "enhancement" ,
38+ }
39+
40+ FEATURE_LABELS : Final [set [str ]] = ALLOWED_LABELS - DETERMINISTIC_LABELS - MODEL_ONLY_LABELS
41+
2942SOURCE_FEATURE_PREFIXES : Final [dict [str , tuple [str , ...]]] = {
3043 "feature:realtime" : ("src/agents/realtime/" ,),
3144 "feature:voice" : ("src/agents/voice/" ,),
@@ -201,27 +214,30 @@ def load_json(path: pathlib.Path) -> Any:
201214 return json .loads (path .read_text ())
202215
203216
204- def load_codex_labels (path : pathlib .Path ) -> list [str ]:
217+ def load_codex_labels (path : pathlib .Path ) -> tuple [ list [str ], bool ]:
205218 if not path .exists ():
206- return []
219+ return [], False
207220
208221 raw = path .read_text ().strip ()
209222 if not raw :
210- return []
223+ return [], False
211224
212225 try :
213226 payload = load_json (path )
214227 except json .JSONDecodeError :
215- return []
228+ return [], False
216229
217230 if not isinstance (payload , dict ):
218- return []
231+ return [], False
219232
220- labels = payload .get ("labels" , [] )
233+ labels = payload .get ("labels" )
221234 if not isinstance (labels , list ):
222- return []
235+ return [], False
223236
224- return [label for label in labels if isinstance (label , str )]
237+ if not all (isinstance (label , str ) for label in labels ):
238+ return [], False
239+
240+ return list (labels ), True
225241
226242
227243def fetch_existing_labels (pr_number : str ) -> set [str ]:
@@ -237,6 +253,7 @@ def compute_desired_labels(
237253 changed_files : Sequence [str ],
238254 diff_text : str ,
239255 codex_ran : bool ,
256+ codex_output_valid : bool ,
240257 codex_labels : Sequence [str ],
241258 base_sha : str | None ,
242259 head_sha : str | None ,
@@ -257,7 +274,7 @@ def compute_desired_labels(
257274 if dependencies_allowed :
258275 desired .add ("dependencies" )
259276
260- if codex_ran :
277+ if codex_ran and codex_output_valid :
261278 for label in codex_labels :
262279 if label == "dependencies" and not dependencies_allowed :
263280 continue
@@ -269,6 +286,13 @@ def compute_desired_labels(
269286 return desired
270287
271288
289+ def compute_managed_labels (* , codex_ran : bool , codex_output_valid : bool ) -> set [str ]:
290+ managed = DETERMINISTIC_LABELS | FEATURE_LABELS
291+ if codex_ran and codex_output_valid :
292+ managed |= MODEL_ONLY_LABELS
293+ return managed
294+
295+
272296def parse_args (argv : Sequence [str ] | None = None ) -> argparse .Namespace :
273297 parser = argparse .ArgumentParser ()
274298 parser .add_argument ("--pr-number" , default = os .environ .get ("PR_NUMBER" , "" ))
@@ -308,18 +332,29 @@ def main(argv: Sequence[str] | None = None) -> int:
308332 ]
309333
310334 diff_text = changes_diff_path .read_text () if changes_diff_path .exists () else ""
335+ codex_labels , codex_output_valid = load_codex_labels (codex_output_path )
336+ if codex_ran and not codex_output_valid :
337+ print (
338+ "Codex output missing or invalid; using fallback feature labels and preserving "
339+ "model-only labels."
340+ )
311341 desired = compute_desired_labels (
312342 changed_files = changed_files ,
313343 diff_text = diff_text ,
314344 codex_ran = codex_ran ,
315- codex_labels = load_codex_labels (codex_output_path ),
345+ codex_output_valid = codex_output_valid ,
346+ codex_labels = codex_labels ,
316347 base_sha = args .base_sha or None ,
317348 head_sha = args .head_sha or None ,
318349 )
319350
320351 existing = fetch_existing_labels (args .pr_number )
352+ managed_labels = compute_managed_labels (
353+ codex_ran = codex_ran ,
354+ codex_output_valid = codex_output_valid ,
355+ )
321356 to_add = sorted (desired - existing )
322- to_remove = sorted ((existing & ALLOWED_LABELS ) - desired )
357+ to_remove = sorted ((existing & managed_labels ) - desired )
323358
324359 if not to_add and not to_remove :
325360 print ("Labels already up to date." )
0 commit comments