"""Parallel execution and selection of opencode review lenses.""" import concurrent.futures as _cf from .opencode_synthesis import _normalize_lens_finding LENS_TIMEOUT_S = 540 def _run_one_lens(workdir, spec, model, factory_root, budget=None, budget_state=None): """Run one lens through the compatibility module's runtime seam.""" from . import opencode as oc bin_ = oc._opencode_bin() home = oc._shared_home() oc._warm_opencode(home, model) env = oc._build_env(home) agent_path = spec.agent_path(factory_root) prompt = ( f"You are the {spec.id} lens. Read .pragent/brief.md, load the " f"lens-orchestration skill (mandatory), and return STRICT JSON " f"findings per that skill. Cap at {spec.max_findings} findings, " f"severity >= {spec.severity_floor}. The agent markdown you should " f"load is at {agent_path} (it sets your role + permissions)." ) cmd = [ bin_, "run", "--pure", "--format", "json", "--agent", spec.id, "--dir", workdir, "--model", model, prompt, ] try: proc = oc._run_process( cmd, cwd=workdir, env=env, timeout=min(LENS_TIMEOUT_S, budget.max_duration_seconds) if budget else LENS_TIMEOUT_S, parse_events=oc.parse_opencode_events, budget=budget, budget_state=budget_state, model=model, ) except oc.subprocess.TimeoutExpired: print(f"pragent: lens {spec.id} timed out after {LENS_TIMEOUT_S}s", flush=True) return [], None, spec.id except Exception as e: print(f"pragent: lens {spec.id} crashed: {e}", flush=True) return [], None, spec.id text, usage = oc.parse_opencode_events(proc.stdout or "") if not text.strip(): print( f"pragent: lens {spec.id} empty text (rc={proc.returncode}); " f"stderr tail: {(proc.stderr or '')[-500:]}", flush=True, ) return [], usage, spec.id obj = oc._extract_json_object(text) if obj is None: print(f"pragent: lens {spec.id} produced no parseable JSON", flush=True) return [], usage, spec.id raw_findings = obj.get("findings") or [] if not isinstance(raw_findings, list): return [], usage, spec.id normalized = [ finding for raw in raw_findings if (finding := _normalize_lens_finding(raw, spec, model)) is not None ] print( f"pragent: lens {spec.id} findings={len(normalized)} " f"raw={len(raw_findings)} ok=1", flush=True, ) return normalized, usage, spec.id def run_lenses(workdir, reviewers, default_model, factory_root, budget=None, budget_state=None): """Run configured lenses in parallel and return results by lens id.""" if not reviewers: return {} from . import opencode as oc pool_size = min(len(reviewers), oc.MAX_PARALLEL_LENSES) out = {} with _cf.ThreadPoolExecutor(max_workers=pool_size) as ex: futures = { ex.submit( _run_one_lens, workdir, spec, spec.model or default_model, factory_root, budget, budget_state, ): spec for spec in reviewers } for fut in _cf.as_completed(futures): spec = futures[fut] try: findings, usage, _ = fut.result() except Exception as e: print(f"pragent: lens {spec.id} worker crashed: {e}", flush=True) findings, usage = [], None out[spec.id] = (findings, usage) return out def intersect_with_triage(reviewers, selected_ids): """Preserve reviewer order while applying the triage verdict.""" if selected_ids is None: return list(reviewers) selected = set(selected_ids) return [reviewer for reviewer in reviewers if reviewer.id in selected] def filter_by_skip_if(reviewers, changed_paths): """Drop lenses whose configured glob matches every changed path.""" import fnmatch out = [] for reviewer in reviewers: pattern = reviewer.skip_if_all_changed_paths.strip() if pattern and changed_paths and all( fnmatch.fnmatch(path, pattern) for path in changed_paths ): continue out.append(reviewer) return out def merge_usage(parts): """Sum per-lens usage, retaining the existing usage dictionary shape.""" from . import opencode as oc base = oc._new_usage() base["duration_s"] = 0.0 for usage in parts: if not usage: continue for key in base: if isinstance(base[key], (int, float)): base[key] += usage.get(key, 0) or 0 base["iterations"].extend(usage.get("iterations", []) or []) return base