narcolepticchicken commited on
Commit
366b543
·
verified ·
1 Parent(s): 43328de

Upload validate_cascade.py

Browse files
Files changed (1) hide show
  1. validate_cascade.py +673 -0
validate_cascade.py ADDED
@@ -0,0 +1,673 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cascade Validation Runner — proves the static cascade on SWE-bench.
3
+
4
+ This script runs the cascade agent on selected SWE-bench instances and
5
+ verifies the patches by applying test_patch and running FAIL_TO_PASS tests.
6
+
7
+ Strategy:
8
+ 1. Clone repo, set up conda environment from environment_setup_commit
9
+ 2. Run cascade agent (T1 Llama-3.1-8B → T2 Llama-3.3-70B)
10
+ 3. Apply model patch + test_patch
11
+ 4. Run FAIL_TO_PASS tests via pytest
12
+ 5. Record: resolved, cost, model tier used, token counts
13
+
14
+ The critical question this answers:
15
+ "Do the 10 cascade-only (T1/T2 solves where both T4 models fail)
16
+ instances produce valid patches, or are they weak-test passes?"
17
+
18
+ Requirements:
19
+ - conda (mamba preferred for speed)
20
+ - git
21
+ - HF_TOKEN (free inference via huggingface_hub)
22
+ - No Docker needed
23
+
24
+ Usage:
25
+ python validate_cascade.py --instance django__django-12308
26
+ python validate_cascade.py --batch 10 --target cascade-only
27
+ python validate_cascade.py --batch 50 --target all
28
+ """
29
+
30
+ import argparse
31
+ import json
32
+ import os
33
+ import re
34
+ import subprocess
35
+ import sys
36
+ import tempfile
37
+ import time
38
+ import traceback
39
+ from datetime import datetime
40
+ from pathlib import Path
41
+ from typing import Optional, Tuple, Dict, List
42
+
43
+ # ============================================================
44
+ # SWE-BENCH CONSTANTS
45
+ # ============================================================
46
+
47
+ # The 10 cascade-only instances (T1 or T2 solves, both T4 models fail)
48
+ # Extracted from the trace simulation in CORRECTED_REPORT.md
49
+ CASCADE_ONLY_INSTANCES = [
50
+ "astropy__astropy-14365",
51
+ "astropy__astropy-14995",
52
+ "django__django-11815",
53
+ "django__django-13089",
54
+ "django__django-13807",
55
+ "django__django-14315",
56
+ "matplotlib__matplotlib-25224",
57
+ "matplotlib__matplotlib-25311",
58
+ "sympy__sympy-19487",
59
+ "sympy__sympy-20590",
60
+ ]
61
+
62
+ # The full set includes 14 frontier-retry-only instances for comparison
63
+ FRONTIER_ONLY_INSTANCES = [
64
+ "django__django-12453",
65
+ "django__django-14030",
66
+ "django__django-14349",
67
+ "django__django-14855",
68
+ "django__django-15098",
69
+ "django__django-16235",
70
+ "matplotlib__matplotlib-26020",
71
+ "psf__requests-6028",
72
+ "pylint-dev__pylint-7080",
73
+ "scikit-learn__scikit-learn-13439",
74
+ "scikit-learn__scikit-learn-14087",
75
+ "sphinx-doc__sphinx-10323",
76
+ "sphinx-doc__sphinx-10466",
77
+ "sphinx-doc__sphinx-10614",
78
+ ]
79
+
80
+ REPO_URLS = {
81
+ "django/django": "https://github.com/django/django.git",
82
+ "pytest-dev/pytest": "https://github.com/pytest-dev/pytest.git",
83
+ "scikit-learn/scikit-learn": "https://github.com/scikit-learn/scikit-learn.git",
84
+ "sympy/sympy": "https://github.com/sympy/sympy.git",
85
+ "matplotlib/matplotlib": "https://github.com/matplotlib/matplotlib.git",
86
+ "sphinx-doc/sphinx": "https://github.com/sphinx-doc/sphinx.git",
87
+ "astropy/astropy": "https://github.com/astropy/astropy.git",
88
+ "psf/requests": "https://github.com/psf/requests.git",
89
+ "pylint-dev/pylint": "https://github.com/pylint-dev/pylint.git",
90
+ }
91
+
92
+
93
+ def run(cmd: list, cwd: str = None, timeout: int = 180, env: dict = None) -> Tuple[int, str, str]:
94
+ """Run a command, return (returncode, stdout, stderr)."""
95
+ try:
96
+ result = subprocess.run(
97
+ cmd, cwd=cwd, capture_output=True, text=True,
98
+ timeout=timeout, env=env or os.environ
99
+ )
100
+ return result.returncode, result.stdout, result.stderr
101
+ except subprocess.TimeoutExpired:
102
+ return 124, "", "TIMEOUT"
103
+ except Exception as e:
104
+ return -1, "", str(e)
105
+
106
+
107
+ # ============================================================
108
+ # CASCADE AGENT (using free HF Inference API)
109
+ # ============================================================
110
+
111
+ def call_hf_model(
112
+ model_id: str,
113
+ messages: list,
114
+ max_tokens: int = 4096,
115
+ temperature: float = 0.2
116
+ ) -> Tuple[str, int, int]:
117
+ """
118
+ Call a model via HF Inference API (free).
119
+ Returns (response_text, input_tokens, output_tokens).
120
+ """
121
+ from huggingface_hub import InferenceClient
122
+
123
+ client = InferenceClient(model_id)
124
+
125
+ completion = client.chat.completions.create(
126
+ model=model_id,
127
+ messages=messages,
128
+ max_tokens=max_tokens,
129
+ temperature=temperature,
130
+ )
131
+
132
+ response = completion.choices[0].message.content
133
+ input_tokens = getattr(completion.usage, "prompt_tokens", 0) if hasattr(completion, "usage") and completion.usage else len(messages) * 100
134
+ output_tokens = getattr(completion.usage, "completion_tokens", 0) if hasattr(completion, "usage") and completion.usage else len(response) // 4
135
+
136
+ return response, input_tokens, output_tokens
137
+
138
+
139
+ def build_cascade_messages(
140
+ instance: dict,
141
+ repo_dir: Path,
142
+ previous_failure: Optional[str] = None,
143
+ ) -> list:
144
+ """Build the message list for the cascade agent."""
145
+
146
+ problem = instance.get("problem_statement", "")
147
+ hint = instance.get("hints_text", "")
148
+
149
+ system_prompt = """You are a software engineer fixing a bug in an open-source project.
150
+ Your task is to produce a correct patch that fixes the issue described.
151
+
152
+ You have access to a bash shell in the repository directory. Use it to:
153
+ - Explore the codebase (ls, find, grep, git log)
154
+ - Read files (cat, head)
155
+ - Run existing tests (pytest)
156
+ - Edit files with sed or write tools
157
+
158
+ Output format:
159
+ - For bash commands: <bash>command here</bash>
160
+ - For your final patch: <patch>diff here</patch>
161
+ - When done and tests pass: <submit>Done</submit>
162
+
163
+ Be thorough. Read the relevant code, understand the bug, make a minimal fix,
164
+ and verify it passes the tests."""
165
+
166
+ # Build the task description
167
+ task = f"""Repository: {instance['repo']}
168
+ Base commit: {instance['base_commit'][:12]}
169
+
170
+ PROBLEM:
171
+ {problem}"""
172
+
173
+ if hint:
174
+ task += f"\n\nHINT: {hint}"
175
+
176
+ task += f"""
177
+
178
+ The repository is at {repo_dir}. Your bash commands will run from that directory.
179
+ Start by exploring the codebase to understand the issue, then implement and test your fix."""
180
+
181
+ if previous_failure:
182
+ system_prompt += f"\n\nYour previous attempt failed with the following issues:\n{previous_failure}\nPlease fix your approach."
183
+
184
+ return [
185
+ {"role": "system", "content": system_prompt},
186
+ {"role": "user", "content": task},
187
+ ]
188
+
189
+
190
+ def extract_patch(response: str) -> Optional[str]:
191
+ """Extract a git patch from the agent's response."""
192
+ # Try <patch> tags
193
+ m = re.search(r'<patch>(.*?)</patch>', response, re.DOTALL)
194
+ if m:
195
+ return m.group(1).strip()
196
+
197
+ # Try ```diff blocks
198
+ m = re.search(r'```diff\s*\n(.*?)```', response, re.DOTALL)
199
+ if m:
200
+ return m.group(1).strip()
201
+
202
+ # Try ```patch blocks
203
+ m = re.search(r'```patch\s*\n(.*?)```', response, re.DOTALL)
204
+ if m:
205
+ return m.group(1).strip()
206
+
207
+ # Try to find diff content directly
208
+ diff_match = re.search(r'(diff --git.*?)(?:\n```|\n<patch>|\n<submit>|\Z)', response, re.DOTALL)
209
+ if diff_match:
210
+ return diff_match.group(1).strip()
211
+
212
+ return None
213
+
214
+
215
+ def run_cascade_agent(
216
+ instance: dict,
217
+ repo_dir: Path,
218
+ max_turns: int = 30,
219
+ ) -> dict:
220
+ """
221
+ Run the cascade agent on one instance.
222
+
223
+ Tiers:
224
+ T1: meta-llama/Llama-3.1-8B-Instruct (free, fast, weak)
225
+ T2: meta-llama/Llama-3.3-70B-Instruct (free, moderate)
226
+
227
+ Returns: {patch, resolved, tier, tokens, turns, cost}
228
+ """
229
+
230
+ TIERS = [
231
+ {"name": "T1", "model": "meta-llama/Llama-3.1-8B-Instruct", "max_turns": max_turns, "cost_per_1k": 0.0},
232
+ {"name": "T2", "model": "meta-llama/Llama-3.3-70B-Instruct", "max_turns": max_turns, "cost_per_1k": 0.0},
233
+ ]
234
+
235
+ result = {
236
+ "instance_id": instance["instance_id"],
237
+ "patch": None,
238
+ "resolved": False,
239
+ "tier_used": None,
240
+ "total_turns": 0,
241
+ "total_input_tokens": 0,
242
+ "total_output_tokens": 0,
243
+ "error": None,
244
+ }
245
+
246
+ previous_failures = []
247
+
248
+ for tier in TIERS:
249
+ print(f"\n [{tier['name']}] {tier['model']} (max {tier['max_turns']} turns)")
250
+
251
+ messages = build_cascade_messages(
252
+ instance, repo_dir,
253
+ previous_failure="\n".join(previous_failures) if previous_failures else None
254
+ )
255
+
256
+ tier_input_tokens = 0
257
+ tier_output_tokens = 0
258
+
259
+ for turn in range(tier['max_turns']):
260
+ try:
261
+ response, in_tok, out_tok = call_hf_model(
262
+ tier['model'], messages,
263
+ max_tokens=4096,
264
+ temperature=0.2
265
+ )
266
+ tier_input_tokens += in_tok
267
+ tier_output_tokens += out_tok
268
+
269
+ # Add response to conversation
270
+ messages.append({"role": "assistant", "content": response})
271
+
272
+ print(f" Turn {turn+1}: {in_tok}+{out_tok} tokens")
273
+
274
+ # Check for patch submission
275
+ patch = extract_patch(response)
276
+ if patch:
277
+ print(f" → Patch found! ({len(patch)} chars)")
278
+ result["patch"] = patch
279
+ result["tier_used"] = tier["name"]
280
+ result["total_turns"] = turn + 1
281
+ result["total_input_tokens"] = tier_input_tokens
282
+ result["total_output_tokens"] = tier_output_tokens
283
+ return result
284
+
285
+ # Check for bash commands
286
+ bash_commands = re.findall(r'<bash>(.*?)</bash>', response, re.DOTALL)
287
+ for cmd in bash_commands:
288
+ cmd = cmd.strip()
289
+ print(f" $ {cmd[:100]}...")
290
+ rc, stdout, stderr = run(
291
+ ["bash", "-c", cmd],
292
+ cwd=str(repo_dir),
293
+ timeout=60
294
+ )
295
+ output = stdout.strip()[:2000]
296
+ if stderr.strip():
297
+ output += f"\n[stderr: {stderr.strip()[:500]}]"
298
+ if rc != 0:
299
+ output += f"\n[EXIT CODE: {rc}]"
300
+ messages.append({"role": "user", "content": f"<output>\n{output}\n</output>"})
301
+
302
+ # Check for submit
303
+ if "<submit>" in response:
304
+ print(f" → Submitted but no patch found")
305
+ break
306
+
307
+ except Exception as e:
308
+ print(f" Error: {e}")
309
+ previous_failures.append(f"[{tier['name']}] turn {turn+1} error: {str(e)[:200]}")
310
+ break
311
+
312
+ previous_failures.append(f"[{tier['name']}] failed after {tier['max_turns']} turns — no patch produced")
313
+
314
+ result["error"] = "All tiers exhausted without producing a patch"
315
+ return result
316
+
317
+
318
+ # ============================================================
319
+ # PATCH VERIFICATION
320
+ # ============================================================
321
+
322
+ def verify_cascade_patch(
323
+ instance: dict,
324
+ model_patch: str,
325
+ repo_dir: Path,
326
+ env_name: str = None,
327
+ ) -> dict:
328
+ """
329
+ Verify that a model-generated patch passes SWE-bench tests.
330
+
331
+ 1. Reset repo to base_commit
332
+ 2. Apply model patch
333
+ 3. Apply test_patch
334
+ 4. Run FAIL_TO_PASS tests
335
+ 5. Run PASS_TO_PASS tests (regression check)
336
+ """
337
+ result = {
338
+ "resolved": False,
339
+ "all_f2p_pass": False,
340
+ "all_p2p_pass": False,
341
+ "f2p_failures": [],
342
+ "p2p_failures": [],
343
+ "error": None,
344
+ }
345
+
346
+ try:
347
+ # Reset to base_commit
348
+ base_commit = instance.get("base_commit", "")
349
+ if base_commit:
350
+ rc, _, _ = run(["git", "checkout", "-f", base_commit], cwd=str(repo_dir))
351
+ if rc != 0:
352
+ result["error"] = f"could not checkout base_commit {base_commit[:12]}"
353
+ return result
354
+
355
+ # Apply model patch
356
+ patch_file = repo_dir / "aco_model.patch"
357
+ patch_file.write_text(model_patch)
358
+
359
+ rc, out, err = run(
360
+ ["git", "apply", "--check", str(patch_file)],
361
+ cwd=str(repo_dir)
362
+ )
363
+ if rc != 0:
364
+ result["error"] = f"patch --check failed: {err[:300]}"
365
+ return result
366
+
367
+ rc, out, err = run(
368
+ ["git", "apply", str(patch_file)],
369
+ cwd=str(repo_dir)
370
+ )
371
+ if rc != 0:
372
+ # Try with --reject
373
+ rc, out, err = run(
374
+ ["git", "apply", "--reject", str(patch_file)],
375
+ cwd=str(repo_dir)
376
+ )
377
+ if rc != 0:
378
+ result["error"] = f"patch apply failed: {err[:300]}"
379
+ return result
380
+
381
+ # Apply test_patch
382
+ test_patch = instance.get("test_patch", "")
383
+ if not test_patch:
384
+ result["error"] = "no test_patch in instance"
385
+ return result
386
+
387
+ test_file = repo_dir / "aco_test.patch"
388
+ test_file.write_text(test_patch)
389
+
390
+ rc, out, err = run(
391
+ ["git", "apply", "--check", str(test_file)],
392
+ cwd=str(repo_dir)
393
+ )
394
+ if rc == 0:
395
+ rc, out, err = run(
396
+ ["git", "apply", str(test_file)],
397
+ cwd=str(repo_dir)
398
+ )
399
+ if rc != 0:
400
+ rc, out, err = run(
401
+ ["git", "apply", "--reject", str(test_file)],
402
+ cwd=str(repo_dir)
403
+ )
404
+
405
+ # Run FAIL_TO_PASS tests
406
+ f2p = instance.get("FAIL_TO_PASS", [])
407
+ if not f2p:
408
+ result["error"] = "no FAIL_TO_PASS tests"
409
+ return result
410
+
411
+ print(f" Running {len(f2p)} FAIL_TO_PASS tests...")
412
+ cmd_prefix = ["conda", "run", "-n", env_name] if env_name else []
413
+ cmd = cmd_prefix + ["python", "-m", "pytest", "-v", "--tb=short", "-x"] + f2p
414
+ rc, out, err = run(cmd, cwd=str(repo_dir), timeout=300)
415
+
416
+ if rc == 0:
417
+ result["all_f2p_pass"] = True
418
+
419
+ # Run PASS_TO_PASS regression tests
420
+ p2p = instance.get("PASS_TO_PASS", [])
421
+ if p2p:
422
+ print(f" Running {len(p2p)} PASS_TO_PASS regression tests...")
423
+ cmd2 = cmd_prefix + ["python", "-m", "pytest", "-v", "--tb=short", "-x"] + p2p[:20]
424
+ rc2, out2, err2 = run(cmd2, cwd=str(repo_dir), timeout=300)
425
+
426
+ if rc2 == 0:
427
+ result["all_p2p_pass"] = True
428
+ result["resolved"] = True
429
+ else:
430
+ result["error"] = f"P2P regression: {(out2+err2)[:300]}"
431
+ result["p2p_failures"] = [l.strip() for l in (out2+err2).split('\n') if 'FAILED' in l and '::' in l]
432
+ else:
433
+ result["resolved"] = True
434
+ else:
435
+ result["error"] = f"F2P failures: {(out+err)[:500]}"
436
+ result["f2p_failures"] = [l.strip() for l in (out+err).split('\n') if 'FAILED' in l and '::' in l]
437
+
438
+ return result
439
+
440
+ except Exception as e:
441
+ result["error"] = f"verification error: {str(e)[:300]}"
442
+ return result
443
+
444
+
445
+ # ============================================================
446
+ # MAIN VALIDATION PIPELINE
447
+ # ============================================================
448
+
449
+ def validate_one(instance: dict) -> dict:
450
+ """
451
+ Full validation pipeline for one instance:
452
+ 1. Clone repo
453
+ 2. Set up conda environment
454
+ 3. Run cascade agent
455
+ 4. Verify patch
456
+ 5. Report results
457
+ """
458
+ inst_id = instance["instance_id"]
459
+ repo = instance.get("repo", "")
460
+ env_setup_commit = instance.get("environment_setup_commit", "")
461
+ base_commit = instance.get("base_commit", "")
462
+
463
+ result = {
464
+ "instance_id": inst_id,
465
+ "repo": repo,
466
+ "timestamp": datetime.now().isoformat(),
467
+ "stages": {},
468
+ "final_resolved": False,
469
+ "tier_used": None,
470
+ "total_cost": 0.0,
471
+ "error": None,
472
+ }
473
+
474
+ print(f"\n{'='*70}")
475
+ print(f"VALIDATING: {inst_id}")
476
+ print(f" Repo: {repo} Base: {base_commit[:12]} EnvSetup: {env_setup_commit[:12]}")
477
+ print(f"{'='*70}")
478
+
479
+ with tempfile.TemporaryDirectory(prefix=f"aco_valid_{inst_id.replace('/', '_')}_") as tmpdir:
480
+ work_dir = Path(tmpdir)
481
+ repo_dir = work_dir / "repo"
482
+ env_name = f"aco_{inst_id.replace('__', '_').replace('-', '_')[:40]}"
483
+
484
+ # STAGE 1: Clone repo
485
+ print("\n--- Stage 1: Clone repo ---")
486
+ repo_url = REPO_URLS.get(repo, f"https://github.com/{repo}.git")
487
+ t0 = time.time()
488
+ rc, out, err = run(["git", "clone", "--depth", "50", repo_url, str(repo_dir)], timeout=300)
489
+ if rc != 0:
490
+ # Retry without depth limit
491
+ rc, out, err = run(["git", "clone", repo_url, str(repo_dir)], timeout=600)
492
+ result["stages"]["clone"] = {"success": rc == 0, "duration": time.time() - t0}
493
+ if rc != 0:
494
+ result["error"] = f"clone failed: {err[:300]}"
495
+ return result
496
+
497
+ # STAGE 2: Set up environment
498
+ print("\n--- Stage 2: Set up conda environment ---")
499
+ t0 = time.time()
500
+
501
+ # Checkout env_setup_commit to find environment.yml
502
+ if env_setup_commit:
503
+ run(["git", "fetch", "origin", env_setup_commit], cwd=str(repo_dir), timeout=60)
504
+ run(["git", "checkout", env_setup_commit], cwd=str(repo_dir), timeout=30)
505
+
506
+ # Find environment.yml
507
+ env_candidates = [
508
+ "environment.yml", "dev/environment.yml", ".github/environment.yml",
509
+ "ci/environment.yml", ".azure-pipelines/environment.yml",
510
+ ]
511
+ env_yml = None
512
+ for c in env_candidates:
513
+ p = repo_dir / c
514
+ if p.exists():
515
+ env_yml = p
516
+ break
517
+ if not env_yml:
518
+ for p in repo_dir.rglob("environment.yml"):
519
+ if p.stat().st_size > 10:
520
+ env_yml = p
521
+ break
522
+
523
+ env_setup_ok = False
524
+ if env_yml:
525
+ print(f" Using environment.yml: {env_yml.relative_to(repo_dir)}")
526
+ rc, out, err = run(
527
+ ["conda", "env", "create", "-f", str(env_yml), "-n", env_name, "--quiet"],
528
+ timeout=600
529
+ )
530
+ env_setup_ok = (rc == 0)
531
+ else:
532
+ print(f" No environment.yml found, creating python=3.10 env")
533
+ rc, out, err = run(
534
+ ["conda", "create", "-n", env_name, "python=3.10", "pip", "-y", "--quiet"],
535
+ timeout=300
536
+ )
537
+ env_setup_ok = (rc == 0)
538
+
539
+ if not env_setup_ok:
540
+ result["stages"]["environment"] = {"success": False, "error": err[:300], "duration": time.time() - t0}
541
+ result["error"] = f"env setup failed: {err[:300]}"
542
+ return result
543
+
544
+ # Checkout base_commit and install
545
+ if base_commit:
546
+ run(["git", "fetch", "origin", base_commit], cwd=str(repo_dir), timeout=60)
547
+ run(["git", "checkout", base_commit], cwd=str(repo_dir), timeout=30)
548
+
549
+ run(["conda", "run", "-n", env_name, "pip", "install", "-e", ".", "--quiet"],
550
+ cwd=str(repo_dir), timeout=300)
551
+
552
+ result["stages"]["environment"] = {"success": True, "duration": time.time() - t0}
553
+ print(f" Environment ready in {time.time() - t0:.1f}s")
554
+
555
+ # STAGE 3: Run cascade agent
556
+ print("\n--- Stage 3: Run cascade agent ---")
557
+ t0 = time.time()
558
+ agent_result = run_cascade_agent(instance, repo_dir, max_turns=30)
559
+ result["stages"]["agent"] = {
560
+ "success": agent_result["patch"] is not None,
561
+ "tier": agent_result["tier_used"],
562
+ "turns": agent_result["total_turns"],
563
+ "input_tokens": agent_result["total_input_tokens"],
564
+ "output_tokens": agent_result["total_output_tokens"],
565
+ "duration": time.time() - t0,
566
+ }
567
+
568
+ if not agent_result["patch"]:
569
+ result["error"] = "No patch produced by any tier"
570
+ return result
571
+
572
+ # STAGE 4: Verify patch
573
+ print("\n--- Stage 4: Verify patch ---")
574
+ t0 = time.time()
575
+ verify_result = verify_cascade_patch(instance, agent_result["patch"], repo_dir, env_name)
576
+ result["stages"]["verify"] = {
577
+ "resolved": verify_result["resolved"],
578
+ "all_f2p_pass": verify_result.get("all_f2p_pass", False),
579
+ "all_p2p_pass": verify_result.get("all_p2p_pass", False),
580
+ "error": verify_result.get("error"),
581
+ "duration": time.time() - t0,
582
+ }
583
+ result["final_resolved"] = verify_result["resolved"]
584
+ result["tier_used"] = agent_result["tier_used"]
585
+
586
+ if verify_result.get("error"):
587
+ result["error"] = verify_result["error"]
588
+
589
+ # Cleanup
590
+ print(f"\n Cleaning up conda env {env_name}...")
591
+ run(["conda", "env", "remove", "-n", env_name, "-y", "--quiet"], timeout=30)
592
+
593
+ return result
594
+
595
+
596
+ def main():
597
+ parser = argparse.ArgumentParser(description="Cascade Validation Runner")
598
+ parser.add_argument("--instance", type=str, help="Single instance ID")
599
+ parser.add_argument("--batch", type=int, default=10, help="Number of instances to validate")
600
+ parser.add_argument("--target", choices=["cascade-only", "frontier-only", "all"], default="cascade-only")
601
+ parser.add_argument("--output", type=str, default="validation_results.jsonl")
602
+ args = parser.parse_args()
603
+
604
+ from datasets import load_dataset
605
+
606
+ print("Loading SWE-bench_Verified...")
607
+ ds = load_dataset("princeton-nlp/SWE-bench_Verified", split="test")
608
+
609
+ # Select instances
610
+ if args.instance:
611
+ instances = [dict(row) for row in ds if row["instance_id"] == args.instance]
612
+ if not instances:
613
+ print(f"Instance {args.instance} not found")
614
+ sys.exit(1)
615
+ elif args.target == "cascade-only":
616
+ instances = [dict(row) for row in ds if row["instance_id"] in CASCADE_ONLY_INSTANCES]
617
+ instances = instances[:args.batch]
618
+ elif args.target == "frontier-only":
619
+ instances = [dict(row) for row in ds if row["instance_id"] in FRONTIER_ONLY_INSTANCES]
620
+ instances = instances[:args.batch]
621
+ else:
622
+ instances = [dict(row) for row in ds][:args.batch]
623
+
624
+ print(f"Selected {len(instances)} instances for validation\n")
625
+
626
+ results = []
627
+ for i, instance in enumerate(instances):
628
+ print(f"\n{'#'*70}")
629
+ print(f" [{i+1}/{len(instances)}] {instance['instance_id']}")
630
+ print(f"{'#'*70}")
631
+
632
+ try:
633
+ result = validate_one(instance)
634
+ results.append(result)
635
+
636
+ # Print summary
637
+ status = "✅ RESOLVED" if result["final_resolved"] else "❌ FAILED"
638
+ print(f"\n {status} | Tier: {result['tier_used']} | error: {result.get('error', 'none')}")
639
+
640
+ except Exception as e:
641
+ print(f"\n ❌ CRASH: {e}")
642
+ traceback.print_exc()
643
+ results.append({
644
+ "instance_id": instance["instance_id"],
645
+ "final_resolved": False,
646
+ "error": str(e),
647
+ })
648
+
649
+ # Save incrementally
650
+ with open(args.output, "w") as f:
651
+ for r in results:
652
+ f.write(json.dumps(r) + "\n")
653
+
654
+ print(f"\n ← Saved to {args.output} ({len(results)} results so far)")
655
+
656
+ # Final report
657
+ resolved = [r for r in results if r["final_resolved"]]
658
+ t1_resolved = [r for r in resolved if r.get("tier_used") == "T1"]
659
+ t2_resolved = [r for r in resolved if r.get("tier_used") == "T2"]
660
+
661
+ print(f"\n{'='*70}")
662
+ print(f"VALIDATION COMPLETE")
663
+ print(f"{'='*70}")
664
+ print(f" Total: {len(results)}")
665
+ print(f" Resolved: {len(resolved)} ({len(resolved)/max(len(results),1)*100:.1f}%)")
666
+ print(f" T1 (Llama-3.1-8B): {len(t1_resolved)}")
667
+ print(f" T2 (Llama-3.3-70B): {len(t2_resolved)}")
668
+ print(f" Failed: {len(results) - len(resolved)}")
669
+ print(f" Results: {args.output}")
670
+
671
+
672
+ if __name__ == "__main__":
673
+ main()