Skip to content

Benchmark — SWE-bench Evaluation System

1. Technical Background

1.1 Why We Need a Benchmark

The core value of harness9 lies in "orchestrating an Agent to solve real engineering problems" — a capability that cannot be validated by static unit tests alone. We therefore need an objective, quantifiable, and comparable evaluation system to answer:

Where are the capability boundaries of an LLM Agent driven by harness9 on real software engineering tasks?

1.2 SWE-bench Overview

SWE-bench (Software Engineering Benchmark), built by the Princeton NLP team, is the current industry-authoritative standard for Agent capability evaluation.

Data source: Real Issues and their corresponding PRs (commits) are collected from 12 mainstream Python open-source projects on GitHub (Django, Flask, Requests, Sympy, etc.), ensuring every Issue has a known correct fix.

Evaluation method:

  • Give the Agent an Issue description (problem_statement) and a repository snapshot (base_commit)
  • The Agent autonomously explores the code, locates the bug, and generates a fix patch
  • The official evaluator applies the patch to the repository inside a sandbox, runs the original test suite, and determines whether it is Resolved

Metric: % Resolved — number of successfully fixed Instances / total number of Instances.

1.3 Dataset Scale

VersionInstance CountNotes
SWE-bench2,294Full set, authoritative but expensive to run
SWE-bench Lite300Curated subset, balances difficulty, the mainstream evaluation default
SWE-bench Verified500Manually verified subset, highest signal-to-noise ratio

harness9 currently runs against SWE-bench Lite, and supports stratified sampling by repo (default 10 per category), balancing cost against coverage.

1.4 Reference Scores from Mainstream Systems (SWE-bench Lite)

System% ResolvedNotes
SWE-agent (GPT-4)~18%Classic ReAct agent
Devin~14%Early AI software engineer
Claude 3.5 Sonnet~49%Official Anthropic result
OpenHands (CodeAct)~26%Open-source framework
harness9TBDPending benchmark run

2. Core Benchmark Principles

SWE-bench execution is split into two fully decoupled phases:

┌─────────────────────────────────────────────────────────────────┐
│                     Phase 1: Inference                           │
│                                                                  │
│   ┌──────────┐    problem_statement    ┌─────────────────────┐  │
│   │ Dataset  │ ──────────────────────► │   harness9 Runner   │  │
│   │ (JSONL)  │    base_commit          │   (cmd/swebench/)   │  │
│   └──────────┘                        └──────────┬──────────┘  │
│                                                  │              │
│                                            git diff             │
│                                                  │              │
│                                                  ▼              │
│                                        predictions.jsonl        │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│                     Phase 2: Evaluation                           │
│                                                                  │
│  predictions.jsonl ──► swebench Python package ──► official Docker image │
│                                                      │           │
│                                              apply patch         │
│                                              run test suite      │
│                                                      │           │
│                                                      ▼           │
│                                            % Resolved score      │
└─────────────────────────────────────────────────────────────────┘

Benefits of decoupling:

  • Inference and Evaluation can run completely independently — no need to install the test suite during Agent execution
  • All predictions.jsonl entries can be accumulated first and then evaluated in a batch
  • The official Docker image guarantees a consistent evaluation environment, avoiding local environment differences from affecting scores

2.1 Dataset Format

SWE-bench Lite is distributed in JSONL format, one Instance per line:

json
{
  "instance_id": "django__django-11179",
  "repo": "django/django",
  "base_commit": "9b224f172a30d38d8b4e7b38a4e2ee47faaf4019",
  "problem_statement": "Autoreloader with StatReloader doesn't work properly...",
  "hints_text": "",
  "test_patch": "diff --git a/tests/utils_tests/test_autoreload.py..."
}
FieldDescriptionPassed to Agent?
instance_idUnique identifier (repo__hash format)No
repoGitHub repository path (owner/name)No (used for clone)
base_commitCommit hash where the issue existsNo (used for checkout)
problem_statementIssue description (the task input the Agent sees)Yes
hints_textOptional hints (present for some Instances)No (not passed to Agent)
test_patchTest changes used for verification (official evaluator only)Never passed

test_patch is the gold standard for evaluation and must never be leaked to the Agent, or it will contaminate the score.

2.2 Prediction Format

The Runner outputs predictions.jsonl, one entry per line:

json
{"instance_id": "django__django-11179", "model_patch": "diff --git a/django/utils/autoreload.py..."}

When model_patch is an empty string, the evaluator marks that Instance as Unresolved (the Agent made no changes or failed).


3. harness9 Design

3.1 Overall Architecture

cmd/swebench/
├── main.go       CLI entry point: flag parsing, preflight, dataset loading, concurrency orchestration
├── runner.go     Single-instance execution core: git → sandbox → engine → patch
├── dataset.go    JSONL loading, stratified random sampling by repo
├── prompt.go     SWE-bench-specific system prompt (structured workflow constraints)
└── report.go     predictions.jsonl append writes, run_summary.md generation

3.2 Single-Instance Execution Flow

runInstance(ctx, inst, cfg)

├─ 1. os.MkdirTemp → tmpDir (defer RemoveAll guarantees cleanup)

├─ 2. git clone https://github.com/<inst.Repo> tmpDir  [5min timeout]
│      git -C tmpDir checkout <inst.BaseCommit>         [30s timeout]

├─ 3. sandbox.Manager.Create(tmpDir)                    [60s timeout]
│      → DockerEnvironment (bind mount shares tmpDir)
│      → defer DestroyAll (independent cleanup context, unaffected by cancellation)

├─ 4. tools.Registry registers four tools:
│      bash (routed into the container)
│      read_file / write_file / edit_file (bind mount, host-side IO)

├─ 5. engine.NewAgentEngine(llm, hookReg, tmpDir,
│        WithPromptBuilder(&swebenchPromptBuilder{inst}))
│        // WithMaxTurns is only appended when MaxTurns > 0, otherwise falls back to the engine default (500)

├─ 6. runWithTrajectory(instanceCtx, eng, prompt, logPath, inst)  [per-instance timeout]
│      → engine.RunStream() consumes all events, writes to logs/<instance_id>.log
│      → Benchmark mode auto-approves all tool approvals (unattended)

├─ 7. git diff (independent context.Background, Ctrl+C safe)  [10s timeout]
│      → patch string

└─ 8. return RunResult{Instance, Patch, Error, Duration}

Key design decisions:

DecisionRationale
git clone runs on the hostNo git credentials inside the container; after bind mount, the container sees the same files
bash tool routed into the containerPython code executed by the Agent runs in an isolated environment, avoiding host contamination
bash timeout relaxed to 300sThe default 120s is still insufficient to finish running the test suite/installing dependencies; the runner raises it to 300s via WithBashTimeout, and the model can also temporarily relax it with timeout_secs (a key path for verifying the fix)
git add -A -N before collecting the patchPlain git diff only outputs tracked files; new fix files created by the Agent via write_file would be silently dropped; intent-to-add brings new files into the diff
git diff uses an independent contextAfter Ctrl+C cancels the main context, the already-modified patch can still be collected, avoiding discarding valid results
Explicitly wired in Compactor + ContextWindowPreviously no compactor was configured, so unbounded context growth in long trajectories hit the window limit → API 400 killed the instance; the runner uses TokenBudgetCompactor, which needs no LLM/Session (budget set at 55% of the window, leaving margin for tool definitions + output + estimation error)
Engine-level generation retry WithGenerateRetry(4, 2s)SDK retries only cover the period before the first byte; mid-stream disconnects/transient 429s escape to the engine layer and kill the instance. An application-layer bounded-backoff retry turns "one jitter kills the instance" into a recoverable event
WithPermissionMode(BypassAll)Unattended mode explicitly short-circuits approvals, zero latency, independent of whether a hook is registered
MaxTurns benchmark default 80Previously inherited the engine default of 500; a stuck instance would burn a huge amount of tokens within the per-instance timeout; 80 is enough for explore+fix+verify while still cutting off runaway loops (as observed in a 69-turn runaway) — still overridable via --max-turns N
Single-instance timeout default 30 minutesThe original 10 minutes needed to cover clone + sandbox startup + the entire agent loop, which was too tight for large repos
Fixed sampling seed (--seed, default 1)Originally used time.Now().UnixNano(), causing different samples on every run with no reproducibility/comparability; a fixed seed → the same instance set, keeping --resume naturally consistent
Logs namespaced by RunIDlogs/<RunID>/<instance>.log, avoiding multiple runs overwriting each other's identically-named logs and polluting analysis
--resume only skips non-empty patchesOriginally skipped by instance_id even for an accepted empty patch, causing the instances that most needed a rerun (failures) to be permanently skipped; changed to only skip instances that have already produced a non-empty patch
RunStream instead of RunCaptures the complete trajectory as an event stream, written to the logs/<RunID>/ directory for subsequent analysis
predictions.jsonl append writesFlushed immediately after each entry completes, working with --resume to support checkpoint-restart
Default dependency bootstrap (wiring up the bootstrap seam)The runner now sets BootstrapCmd for every instance by default (ensurepip + pip install -e . + pytest), making "real tests" runnable before the Agent starts — restoring the verification closed loop (trajectory analysis R1: previously 24/24 instances had zero test runs, relying entirely on static analysis). Explicitly setting SANDBOX_BOOTSTRAP_CMD overrides the default; repos requiring a compiler can set SANDBOX_IMAGE to point at the official per-instance image
Default image changed to python:3.11 (non-slim)slim often lacks pip and has incomplete runtime libraries; the full image ships with pip and can pull dependencies such as numpy/pandas from wheels, enabling real tests to run alongside the default bootstrap (trajectory analysis R1)
Verification gateWhen the Agent naturally concludes yet has "run zero tests throughout," the runner injects one continuation prompt demanding real verification (reusing the same engine + in-memory session to continue history, at most once, backstopped by the timeout/turn cap). Fixes "declaring done after 9 rounds of pure static self-certification" (trajectory analysis R2: 8/8 failed instances had zero verification before declaring done)
Stagnation nudge WithStallNudge(10, …)When 10 consecutive turns show no edits/test runs (only spinning on static re-reads/greps), the engine injects one nudge to break the idle loop (trajectory analysis R6: xarray-3364 and pylint-7080 burned through all 80 turns in exactly this pattern). Applies only to a temporary copy, not persisted
Injecting hints_text + dataset parsing of evaluation fieldsInstance now parses version/environment_setup_commit/FAIL_TO_PASS/test_patch; the prompt injects maintainer discussion (hints), which often contains decisive API design decisions (trajectory analysis R3: Flask's text=True, xarray's DeprecationWarning). ⚠️ FAIL_TO_PASS/test_patch are for analysis only and are never exposed to or applied during Agent runtime

3.3 Sampling Strategy

SWE-bench Lite covers 11 Python repositories; harness9 samples randomly after stratifying by repo:

allInstances (300 entries)

    ├─ Group by repo → 11 groups (astropy/astropy, django/django, ...)

    ├─ Shuffle randomly within each group (fixed seed → reproducible)

    ├─ Take the first min(n, groupSize) from each group

    └─ Merge and shuffle overall → balanced distribution, not concentrated on a single repo under concurrency

This controls the total volume while guaranteeing cross-repository diversity (different language features, project sizes, bug types).

3.4 Dedicated System Prompt Design

harness9 designs a dedicated system prompt for SWE-bench (in English, to improve quality on English-language code tasks), with the strategy:

Structured workflow constraints (5 sequential steps) + free exploration within each step (no restriction on how tools are called)

Step 1 — Understand the problem
  ↓ Identify the core bug, reproduction steps, expected behavior

Step 2 — Explore the repository
  ↓ Use grep to locate relevant files, read_file with line numbers; parallel multi-tool calls
  ↓ Read (never modify) relevant existing tests — they encode the maintainer's expected behavior/output/edge cases

Step 3 — Reproduce (when feasible)
  ↓ When python is available and the package is importable, write the minimal reproduction; execute via heredoc, never create a temporary .py inside the repo (would contaminate the patch)

Step 4 — Fix
  ↓ Make the minimal change at the exact line producing the wrong behavior (raise/return/branch), without adding a parallel code path,
  ↓ without hoisting an assignment/alias out of a loop (plausible-but-broader changes often fail hidden tests)
  ↓ grep and read tests by the "changed symbol name"; for ambiguous/unexpected API behavior, prefer checking the project's DeprecationWarning conventions first
  ↓ Never modify test files, never introduce new dependencies; grep -n to pinpoint exact line numbers before editing

Step 5 — Verify (behavior, not syntax)
  ↓ edit_file's diff only confirms "bytes were written," not that the behavior is correct
  ↓ Run real tests / the reproduction script to verify behavior; never "copy the class/function verbatim into an inline script to self-test"

Design principles behind the constraints:

  • "Do not modify test files" is a hard SWE-bench constraint; violating it invalidates the evaluation result — but reading existing tests is encouraged (the strongest behavioral signal), and searching by "changed symbol name" rather than topical keywords is required (trajectory analysis R7)
  • Minimal, error-site-local fix bias: several failures stemmed from "plausible but misplaced/overly broad" fixes (pylint editing the wrong file, requests hoisting an object out of a loop and changing its alias, xarray adding a parallel code path); the prompt explicitly requires making the minimal change at the exact site of the error
  • Behavioral verification priority + no more default fallback to static analysis: removed the escape hatch of "dependencies might be missing, pip might be unavailable → fall back to static analysis" (trajectory analysis R5: it effectively wrote "giving up on verification" into the official default); replaced with "the environment has already attempted to pre-install dependencies, prioritize running real tests; if imports fail, bootstrap-install first, and only fall back to a static review with explicit disclosure if it truly cannot run"
  • Injecting maintainer hints + deprecation convention hints: hints_text was previously parsed but never injected, even though it often contains decisive API design decisions; the prompt now injects it and notes that "discussion often supersedes the original issue proposal," and for ambiguous API behavior, prompts to prefer considering DeprecationWarning conventions over silently changing behavior (trajectory analysis R3/R7)
  • File tools always use relative paths: the injected absolute working directory previously tempted the model to pass absolute paths to read_file/edit_file, triggering path-concatenation errors (already fixed in safePath, with the prompt as a secondary safeguard)
  • Reasoning language switched to English + a single-line anti-drift constraint (evaluation only looks at the patch, and English better matches English-language code/Issues/stack traces)

3.5 Concurrency Control and Resilience

Main loop (main.go)

sem := semaphore.NewWeighted(N)   ← --parallel N controls maximum concurrency

for each instance:
    sem.Acquire(ctx, 1)            ← acquire a slot (blocks once N is exceeded)
    go func:
        result = runInstance(...)  ← each goroutine is fully independent
        mu.Lock()
        results = append(...)
        appendPrediction(...)      ← write immediately, don't wait for everything to finish
        mu.Unlock()
        sem.Release(1)             ← release the slot

wg.Wait()                          ← wait for all goroutines to finish
writeSummary(...)

Resilience mechanisms:

ScenarioHandling
git clone failureRecord Error, write an empty patch, continue to the next entry
Docker startup failureSame as above
LLM API transient error/rate limit/stream disconnectEngine-level bounded-backoff retry (WithGenerateRetry) + SDK built-in retry; most transient jitter is now recoverable, no longer kills the instance
Context approaching window limitTokenBudgetCompactor trims old Observations at 55% of the window, avoiding a 400 overflow
MaxTurns triggered (default 80)Collects the current git diff (including new files from git add -N), not marked as an error
Overall Ctrl+CWaits for the current instance to finish, collects the patch, then exits
--resume restartOnly skips instances that have already produced a non-empty patch; empty/errored instances are retried

4. Complete Operating Procedure

4.1 Prerequisites

Recommended approach: create a .env file at the project root (shares the same configuration as the harness9 main program; the runner automatically loads it from the current working directory at startup):

bash
# harness9/.env
OPENAI_API_KEY=sk-...
OPENAI_BASE_URL=https://openrouter.ai/api/v1   # optional, for connecting to OpenRouter / Azure etc.
LLM_MODEL=openai/gpt-4o
# Default is python:3.11 (ships with pip, can pull dependencies from wheels); the runner bootstraps dependencies by default to run real tests.
# High fidelity: set to the official per-instance image swebench/sweb.eval.x86_64.<instance> (repo + dependencies pre-installed).
SANDBOX_IMAGE=python:3.11
# Optional: override the default dependency bootstrap command (default: ensurepip + pip install -e . + pytest).
# SANDBOX_BOOTSTRAP_CMD=pip install -e . -q && pip install pytest -q

Can also be provided via system environment variables (system variables take priority over .env):

bash
export OPENAI_API_KEY=sk-...
export LLM_MODEL=openai/gpt-4o

Confirm the Docker daemon is running:

bash
docker info

4.2 Download the Dataset

bash
pip install datasets

python -c "
from datasets import load_dataset
ds = load_dataset('princeton-nlp/SWE-bench_Lite', split='test')
ds.to_json('swe-bench-lite.jsonl')
print(f'Download complete: {len(ds)} instances')
"

You can also download the JSONL file directly from the Hugging Face Hub: https://huggingface.co/datasets/princeton-nlp/SWE-bench_Lite

After configuring .env, run directly (the runner auto-loads it from the current directory):

bash
cd /path/to/harness9

go run ./cmd/swebench \
  --dataset swe-bench-lite.jsonl \
  --sample 10 \       # take 10 per repo, ~110 total
  --output ./swebench-results \
  --parallel 2 \      # run 2 instances concurrently
  --timeout 15        # 15-minute timeout per instance (--max-turns default 0 = unlimited turns)

If there is no .env file, you can also pass values directly via environment variables:

bash
OPENAI_API_KEY=sk-... LLM_MODEL=openai/gpt-4o go run ./cmd/swebench \
  --dataset swe-bench-lite.jsonl --sample 10

During the run, progress is printed to stderr:

Dataset loaded: 300 instances
Sampling complete: 110 (up to 10 per repo)
[start] django__django-11179
[start] astropy__astropy-12345
[done]  django__django-11179 (4m32s) patch=1842 bytes
[done]  astropy__astropy-12345 (6m10s) patch=0 bytes   ← empty patch
[error] flask__flask-5678 (15m0s): context deadline exceeded
...
Complete! Results written to ./swebench-results
Total instances: 110, elapsed: 3h21m

4.4 Checkpoint Resume

If the run is interrupted midway (network failure, Ctrl+C, etc.), use --resume to skip existing results:

bash
go run ./cmd/swebench \
  --dataset swe-bench-lite.jsonl \
  --output ./swebench-results \
  --resume              # automatically skips instance_ids already present in predictions.jsonl

4.5 Viewing Intermediate Results

swebench-results/
├── predictions.jsonl        # one entry per line {"instance_id":..., "model_patch":...}
├── run_summary.md           # run summary (totals/patch count/error count/distribution by repo)
└── logs/
    ├── django__django-12908.log   # complete trajectory for each instance
    ├── astropy__astropy-5678.log
    └── ...

Trajectory log format (logs/<instance_id>.log):

=== SWE-bench Instance: django__django-12908 ===
Repo:        django/django
BaseCommit:  abc123...
StartTime:   2026-06-09 14:30:00

--- Turn 1 ---
Let me start by exploring the repository structure to understand the codebase...

[Tool Call: bash]
{"command":"find . -type f -name \"*.py\" | grep -v __pycache__ | head -40"}

[Tool Result: abc12345 | 350ms | ok]
./django/utils/autoreload.py
./django/core/management/base.py
...

[Tokens: 4821]

--- Turn 2 ---
I can see the issue in autoreload.py. Let me read the relevant section...

The log contains: each turn's LLM output text, tool call arguments, tool return results (including duration and status), token usage, and context compaction events.

run_summary.md example:

markdown
# SWE-bench Lite Run Summary

- Start time: 2026-06-09 14:30:00
- End time: 2026-06-09 17:51:00
- Total instances: 110
- Patches successfully generated: 89 / 110
- Empty patches (agent made no changes): 14
- Run errors: 7

## Distribution by Repo
| Repo              | Instances | Has patch | Empty patch | Error |
|-------------------|--------|---------|---------|------|
| astropy/astropy   | 10     | 8       | 2       | 0    |
| django/django     | 10     | 9       | 1       | 0    |
| ...               | ...    | ...     | ...     | ...  |

4.6 Official Evaluation Scoring

After the Runner completes, use the official swebench tool to score the results:

bash
pip install swebench

python -m swebench.harness.run_evaluation \
    --dataset_name princeton-nlp/SWE-bench_Lite \
    --predictions_path ./swebench-results/predictions.jsonl \
    --max_workers 4 \           # number of instances evaluated concurrently
    --run_id harness9-lite-v1   # identifier for this run (affects output directory name)

View the results after evaluation completes:

bash
# The official tool writes results to logs/run_evaluation/harness9-lite-v1/
cat logs/run_evaluation/harness9-lite-v1/results.json
# {"resolved": 23, "unresolved": 77, "error": 10, "total": 110}
# Resolved Rate: 20.9%

Note: The official evaluator needs to pull the official Docker image corresponding to each Instance (roughly 1–5 GB per image); the first run will consume significant bandwidth and disk space (hundreds of GB). It is recommended to run in an environment with sufficient disk space, or use --max_workers 1 for serial evaluation to save resources.

4.7 Full Run (comparison against the public leaderboard)

Run all 300 instances to directly compare against the public results of systems like SWE-agent, Claude, etc.:

bash
go run ./cmd/swebench \
  --dataset swe-bench-lite.jsonl \
  --sample 300 \        # unlimited, take the full set
  --output ./swebench-results-full \
  --max-turns 30 \
  --parallel 3 \
  --timeout 15

Cost estimate (GPT-4o):

  • Each instance averages roughly 15–20 LLM calls, about $1–3 in API cost
  • 300 instances total approximately $300–900
  • It is recommended to validate the workflow with --sample 10 first before running the full set

5. Parameter Reference

bash
go run ./cmd/swebench --help
ParameterTypeDefaultDescription
--datasetstringrequiredPath to the SWE-bench Lite JSONL file
--sampleint10Number of instances to sample per repo (≥1)
--outputstring./swebench-resultsOutput directory
--max-turnsint0Maximum Turn count per instance (0 = benchmark default of 80; an explicit N overrides it)
--parallelint1Number of concurrent instances (≥1)
--resumeboolfalseSkip instances that have already produced a non-empty patch (checkpoint resume)
--timeoutint30Per-instance timeout (minutes)
--seedint641Random seed for per-repo sampling (fixed default guarantees reproducibility; same seed → same instance set)
--modelstring""LLM model (reads the LLM_MODEL environment variable if empty)

Environment variables (can be provided via a .env file or system environment variables, system variables take priority):

VariableDescriptionRecommended value
OPENAI_API_KEYLLM API Key (required)
OPENAI_BASE_URLCustom API endpoint (optional)https://openrouter.ai/api/v1
LLM_MODELModel nameopenai/gpt-4o
LLM_REQUEST_TIMEOUT_SECSTimeout for a single LLM request (seconds)600 (default)
LLM_MAX_RETRIESSDK built-in retry count (429/5xx)5 (default)
SANDBOX_IMAGEDocker imagepython:3.11 (default); use swebench/sweb.eval.x86_64.<instance> for high fidelity
SANDBOX_ENABLEDEnable Docker isolationtrue (default)
SANDBOX_BOOTSTRAP_CMDDependency install command run once the container is ready; when left empty, the runner automatically injects the default bootstrap (ensurepip + pip install -e . + pytest)leave empty
SANDBOX_BOOTSTRAP_TIMEOUT_SECSBootstrap command timeout (seconds)600 (default)

6. Trajectory-Driven Kernel Optimization Log (v1 → v2)

This section records a complete "evaluate → forensic analysis → kernel optimization → re-measure comparison" closed loop: based on complete trajectory forensics from the first 24-instance evaluation round, we located shortcomings in the framework kernel and optimized against them; after re-measurement, Resolved rose from 16/24 (66.7%) to 19/24 (79.2%), with zero regressions.

See docs/技术调研/swebench-轨迹分析与内核优化-v2.md for the complete root-cause report.

6.1 Methodology

For the 8 failed trajectories, we performed a three-way comparison one by one — our patch ↔ gold patch ↔ hidden tests (test_patch + FAIL_TO_PASS) — first pinpointing "why it fails the hidden tests," then tracing back through the trajectory to find "why the Agent produced this patch," and performing an adversarial review of every harness attribution (checking against the current kernel source code, discarding "already fixed / nonexistent" false root causes). Review conclusion: 21 confirmed / 6 partial / 2 refuted.

6.2 Decisive Finding: 100% Breakdown of the Verification Closed Loop

Not a single one of the 24 trajectories in the first round ever actually ran a test — all hit ModuleNotFoundError / No module named pip. All 16 resolved instances got lucky purely through static analysis; the 8 unresolved instances were largely problems that "can only converge with real test feedback."

The root cause was in the runner: after clone+checkout, there was no dependency installation step whatsoever, and .env mistakenly used a non-Python image, locking the Agent into an environment with no dependencies, no pip, unable to run any tests — the kernel's most important feedback signal (real test results) was permanently empty. The most ironic part: the sandbox package had already prepared the BootstrapCmd seam long ago, but the runner had never wired it in.

6.3 Root Cause Tiers (Adversarially Reviewed)

#Root CauseHit RateReview
R1Missing environment: sandbox has no dependencies/no pip, real tests cannot run8/8confirmed
R2Missing verification closed loop: the loop terminates purely on "no tool call," with no "run tests before finishing" gate, so static self-certification alone leads to declaring done8/8confirmed
R3HintsText discarded: dataset parses hints_text but the prompt never injects it; maintainer discussion often contains decisive API design (Flask's text=True)multipleconfirmed
R4Missing dataset fields: version/environment_setup_commit/FAIL_TO_PASS/test_patch were not parsed, nothing to provision from8/8confirmed
R5Prompt reverse-guidance: explicitly stated "dependencies might be missing → fall back to static analysis," writing "giving up on verification" as an official default8/8confirmed
R6Blind spinning to max_turns: repeated static re-reading with no feedback, xarray-3364 and pylint-7080 burned through all 80 turns before being cut off2/8confirmed
R7Plausible but misplaced fixes: wrong location (pylint editing the wrong file), invented API (Flask's mode=), changing behavior instead of adding a DeprecationWarning (xarray)6/8confirmed
R8edit_file prompt suppresses verification: on an exact match it outputs "no need to... confirm again," encouraging "done as soon as the edit is made"multipleconfirmed

6.4 Targeted Optimizations (mapped to root causes)

OptimizationFileRoot Cause
Wire up dependency bootstrap: the runner now sets BootstrapCmd by default for every instance (ensurepip + pip install -e . + pytest), default image changed to python:3.11 (full buildpack image, with pip and a compiler)runner.goR1/R4
Verification gate: when the Agent naturally concludes without ever having run a test, inject one continuation prompt demanding real verification (reusing the in-memory session to continue history, at most once, backstopped by timeout/turn cap)runner.goR2
HintsText injection + dataset parsing of evaluation fields (FAIL_TO_PASS/test_patch for analysis only, never exposed to or applied during runtime)prompt.go dataset.goR3/R4
Stagnation nudge WithStallNudge: injects one nudge to break the idle loop when N consecutive turns show no edit/write progress tool calls (defensive copy, not persisted)engine/agent_loop.goR6
Prompt rebalancing: removed the "default fallback to static analysis" escape hatch; added minimal/error-site-local fix bias, searching tests by symbol name, DeprecationWarning convention hintsprompt.goR5/R7
edit_file banner tightened: "no need to confirm again" → explicit "bytes written ≠ behavior correct, tests still required"tools/edit_file.goR8

All changes followed TDD (red→green), with accompanying unit tests + eval golden cases (including the Case.EngineOptions seam + a stagnation-nudge regression guardrail); go test ./... fully green.

6.5 Measured Comparison (same seed=1, same 24 instances, same model anthropic/claude-sonnet-4.6, same official scorer)

Metricv1 (before optimization)v2 (after optimization)Δ
Resolved16/24 (66.7%)19/24 (79.2%)+3, +12.5pp
Regressions (v1 passed → v2 failed)0zero regressions
Instances with real test runs1/2418/24+17
End-to-end wall clock~75 min23 min3.3× faster

During scoring, 3 instances "errored" due to Docker resource contention under 4-way concurrency on arm64 emulation (not a patch problem); after single-threaded re-scoring, astropy-12907 / django-14855 were confirmed still RESOLVED and seaborn-3407 still unresolved; 19/24 is the final number after a clean re-scoring.

6.6 Three Newly Resolved Instances = Each of the Three Optimizations Hitting Home

Instancev1v2Optimization that took effect
pylint-708080 turns (maxed out)/no tests → failed17 turns/tests → passedDependency bootstrap made pylint runnable → 80 turns of blind spinning turned into 17 turns converging on the correct 1-line os.path.normpath fix (the most representative case)
flask-499213 turns/no tests → failed14 turns/tests → passedHints injection exposed the maintainer-decided text=True API + real tests immediately exposed the ValueError from mode='t'
astropy-774617 turns/no tests → failed110 turns/tests → passedThe verification gate triggered one continuation (110 = main run + forced verification), real tests caught an asymmetric [],[1] regression

6.7 Five Still Unresolved: Honest Attribution

Instancev2Root cause category
requests-196311 turns/no testsEnvironment ceiling: 2014-era code requires Python 2.7, python:3.11 can't even import it → still cannot verify. The only solution: the official per-instance image (the SANDBOX_IMAGE interface is already prepared)
xarray-449340 turns/testsHidden-test-exclusive behavior: the gold fix needs to emit a DeprecationWarning, written only in the test_patch that gets injected at evaluation time, not visible at runtime
seaborn-340733 turns/testsSame as above: the hidden test asserts the exact type of diag_vars==list(cols)
flask-506316 turns/testsSame as above: the hidden test wants a Host/Subdomain header + host_matching mode
xarray-336480 turns (maxed out)/testsComplex localization: tests can now run, but the fix anchored on an existing assertion that the test_patch removes, still editing the wrong code path

6.8 Key Conclusions

  • Real test feedback is "necessary but not sufficient": it solved the problems that "could converge given feedback" (+3, zero regressions, and 3x faster), but was powerless against the category of problems where "the expected behavior only exists in hidden tests" (xarray-4493/seaborn-3407/flask-5063), even with the environment in place.
  • The remaining 5 cases have been precisely attributed to "environment ceiling (1) + hidden-test-exclusive behavior (3) + complex localization (1)," fully consistent with the root-cause analysis.
  • The next increment can only come from: ① the official per-instance image (solving version-ceiling cases like requests-1963); ② stronger scaffolding for "inferring hidden behavior from project conventions."

7. Code Location Reference

FileResponsibility
cmd/swebench/main.goCLI entry point, preflight, concurrency orchestration
cmd/swebench/runner.goSingle-instance execution core (git/sandbox/engine/patch)
cmd/swebench/dataset.goDataset loading, sampling by repo
cmd/swebench/prompt.goSWE-bench-specific system prompt
cmd/swebench/report.goOutput file management (predictions/summary)
cmd/swebench/*_test.goUnit tests for each module
internal/sandbox/Docker sandbox infrastructure
internal/engine/ReAct Agent Loop
docs/设计规格/2026-06-09-swebench-lite-runner-design.mdComplete design document

Released under the MIT License.