"""Run a frozen list of episodes with explicit per-episode input files. The manifest is JSON list. Each row supplies spec, registry, controller, trace. This runner records failures and never invents task inputs or fills results. """ import argparse,json,subprocess,sys,hashlib from pathlib import Path def main(): p=argparse.ArgumentParser();p.add_argument('manifest');p.add_argument('--output',required=True);p.add_argument('--dry-run',action='store_true');a=p.parse_args() rows=json.loads(Path(a.manifest).read_text());report=[] for r in rows: hashes={} for key in ['spec','registry','controller']: path=Path(r[key]);hashes[key]=hashlib.sha256(path.read_bytes()).hexdigest() command=[sys.executable,'-m','peppa.cli','run','--spec',r['spec'],'--registry',r['registry'],'--controller',r['controller'],'--trace',r['trace']] status={'episode':r.get('episode',r['trace']),'hashes':hashes,'command':command} if a.dry_run:status['status']='validated paths' else: result=subprocess.run(command,text=True,capture_output=True,check=False) status.update(returncode=result.returncode,stdout=result.stdout,stderr=result.stderr) report.append(status) Path(a.output).parent.mkdir(parents=True,exist_ok=True);Path(a.output).write_text(json.dumps(report,indent=2)) if __name__=='__main__':main()