11#!/usr/bin/env python3
22import argparse
3+ import hashlib
34import json
45import re
56from pathlib import Path
@@ -37,6 +38,34 @@ def _display_slug(value: str) -> str:
3738 return re .sub (r"[^A-Za-z0-9._-]+" , "_" , value .strip ()) or "Custom"
3839
3940
41+ def _normalize_task_ids (values : list [str ] | None ) -> list [str ]:
42+ task_ids : list [str ] = []
43+ for value in values or []:
44+ task_ids .extend (part .strip () for part in str (value ).split ("," ) if part .strip ())
45+ if not task_ids :
46+ return []
47+ invalid = [task_id for task_id in task_ids if not re .fullmatch (r"[A-Za-z0-9._-]+" , task_id )]
48+ if invalid :
49+ raise SystemExit (f"invalid task id(s): { ', ' .join (invalid )} " )
50+ duplicates = sorted ({task_id for task_id in task_ids if task_ids .count (task_id ) > 1 })
51+ if duplicates :
52+ raise SystemExit (f"duplicate task id(s): { ', ' .join (duplicates )} " )
53+ return task_ids
54+
55+
56+ def _selection_suffix (* , task_ids : list [str ], persona : str | None ) -> tuple [str , str ]:
57+ if task_ids :
58+ joined = "-" .join (task_ids )
59+ if len (task_ids ) <= 3 and len (joined ) <= 48 :
60+ return f"tasks-{ _safe_slug (joined )} " , f"Tasks-{ _display_slug (joined )} "
61+ digest = hashlib .sha256 ("\0 " .join (task_ids ).encode ("utf-8" )).hexdigest ()[:10 ]
62+ return f"tasks-{ len (task_ids )} -{ digest } " , f"Tasks-{ len (task_ids )} -{ digest } "
63+ if persona :
64+ slug = _safe_slug (persona )[:60 ]
65+ return f"persona-{ slug } " , f"Persona-{ _display_slug (persona )[:60 ]} "
66+ return "" , ""
67+
68+
4069def _normalize_harness (value : str ) -> str :
4170 mapping = {
4271 "codex" : "Codex" ,
@@ -102,10 +131,23 @@ def build_config(args: argparse.Namespace) -> Path:
102131 if dataset not in {"smoke" , "lite" , "full" }:
103132 raise SystemExit (f"unsupported dataset: { args .dataset } " )
104133
105- run_name = args .run_name or {"smoke" : "Smoke" , "lite" : "Lite" , "full" : "Full" }[dataset ]
134+ task_ids = _normalize_task_ids (getattr (args , "task_ids" , None ))
135+ persona_value = getattr (args , "persona" , None )
136+ persona = str (persona_value ).strip () if persona_value is not None else None
137+ if persona_value is not None and not persona :
138+ raise SystemExit ("--persona must not be empty" )
139+ task_limit = getattr (args , "task_limit" , None )
140+ selected = sum ([task_limit is not None , bool (task_ids ), persona is not None ])
141+ if selected > 1 :
142+ raise SystemExit ("--task-limit, --task-ids, and --persona are mutually exclusive" )
143+
144+ selection_slug , selection_name = _selection_suffix (task_ids = task_ids , persona = persona )
145+ default_run_name = {"smoke" : "Smoke" , "lite" : "Lite" , "full" : "Full" }[dataset ]
146+ run_name = args .run_name or (
147+ f"{ default_run_name } -{ selection_name } " if selection_name else default_run_name
148+ )
106149 task_path = eval_root / ("tasks" if dataset == "full" else "tasks_lite" )
107- task_limit = args .task_limit
108- if task_limit is None and dataset == "smoke" :
150+ if task_limit is None and not task_ids and persona is None and dataset == "smoke" :
109151 task_limit = 1
110152 task_parallel = not bool (args .no_task_parallel )
111153 task_parallel_workers = max (1 , int (args .task_parallel_workers or 10 ))
@@ -117,6 +159,10 @@ def build_config(args: argparse.Namespace) -> Path:
117159 fs_map_dir .mkdir (parents = True , exist_ok = True )
118160
119161 config_slug = f"{ harness .lower ()} -{ _safe_slug (args .model )} -{ dataset } "
162+ if selection_slug :
163+ config_slug = f"{ config_slug } -{ selection_slug } "
164+ elif args .run_name :
165+ config_slug = f"{ config_slug } -{ _safe_slug (args .run_name )} "
120166 fs_map_path = fs_map_dir / f"fs_map_{ harness } _{ _display_slug (model_name )} .json"
121167 fs_map_path .write_text (
122168 json .dumps (_fs_map (eval_root , harness , model_name ), ensure_ascii = False , indent = 2 ) + "\n " ,
@@ -146,6 +192,10 @@ def build_config(args: argparse.Namespace) -> Path:
146192 }
147193 if task_limit is not None :
148194 config ["task_limit" ] = int (task_limit )
195+ elif task_ids :
196+ config ["task_ids" ] = task_ids
197+ elif persona is not None :
198+ config ["persona" ] = persona
149199
150200 config_path = runs_dir / f"{ config_slug } .yaml"
151201 config_path .write_text (yaml .safe_dump (config , allow_unicode = True , sort_keys = False ), encoding = "utf-8" )
@@ -163,7 +213,14 @@ def main() -> None:
163213 parser .add_argument ("--model-name" , help = "Display name used in output directory" )
164214 parser .add_argument ("--env-prefix" , help = "Environment variable prefix for BASE_URL/API_KEY" )
165215 parser .add_argument ("--run-name" , help = "Output run name; defaults to Smoke/Lite/Full" )
166- parser .add_argument ("--task-limit" , type = int )
216+ selection = parser .add_mutually_exclusive_group ()
217+ selection .add_argument ("--task-limit" , type = int , help = "Run the first N tasks in deterministic order" )
218+ selection .add_argument (
219+ "--task-ids" ,
220+ nargs = "+" ,
221+ help = "Run exact task IDs; accepts spaces or comma-separated values" ,
222+ )
223+ selection .add_argument ("--persona" , help = "Run every task whose metadata persona exactly matches this value" )
167224 parser .add_argument ("--timeout-sec" , type = float , default = 2000.0 )
168225 parser .add_argument ("--task-parallel-workers" , type = int , help = "Number of isolated task-level workers; defaults to 10" )
169226 parser .add_argument ("--no-task-parallel" , action = "store_true" , help = "Disable isolated task-level parallelism" )
0 commit comments