|
| 1 | +# Forge on SWE-Bench |
| 2 | + |
| 3 | +Forge's first cross-benchmark evaluation: running the full autonomous |
| 4 | +pipeline against [SWE-Bench](https://www.swebench.com/) tasks — real |
| 5 | +GitHub issues from mature Python projects, paired with hidden regression |
| 6 | +tests that the evaluator uses to decide whether the patch resolves the |
| 7 | +issue without breaking anything else. |
| 8 | + |
| 9 | +This doc covers: |
| 10 | + |
| 11 | +1. The S0 smoke run and its outcome. |
| 12 | +2. Every blocker SWE-Bench exposed and how we fixed it. |
| 13 | +3. Whether any of those fixes bent Forge away from its core |
| 14 | + autonomous-coding mission (spoiler: one surgical fix, pure |
| 15 | + improvement; everything else lives in an adapter layer). |
| 16 | +4. The Adapted vs Pure strategy comparison. |
| 17 | +5. Concrete S1 scope. |
| 18 | + |
| 19 | +For the harness code itself see `platform/forge/swebench/`. For the |
| 20 | +release/monitor meta-harness (scenario sandbox, quality scoring) see |
| 21 | +[EVAL-HARNESS.md](EVAL-HARNESS.md); the two are complementary, not |
| 22 | +duplicative. |
| 23 | + |
| 24 | +## S0: psf__requests-5414 — RESOLVED |
| 25 | + |
| 26 | +First SWE-Bench task Forge has ever solved end-to-end. |
| 27 | + |
| 28 | +| Field | Value | |
| 29 | +| --- | --- | |
| 30 | +| Instance | `psf__requests-5414` | |
| 31 | +| Strategy | Adapted | |
| 32 | +| Base commit | `39d0fdd9096f7dceccbc8f82e1eda7dd64717a8e` | |
| 33 | +| Wall clock | 2161.88s (~36 min) | |
| 34 | +| Patch size | 559 bytes | |
| 35 | +| F2P pass count | 1 / 1 | |
| 36 | +| P2P regressions | 0 | |
| 37 | +| Eval resolved | **true** | |
| 38 | +| Worker model | `amazon-bedrock/us.anthropic.claude-sonnet-4-5` | |
| 39 | +| Reviewer model | `amazon-bedrock/us.anthropic.claude-opus-4-6` | |
| 40 | + |
| 41 | +The patch (artefact at `.forge/swebench/psf__requests-5414/adapted/run/forge.patch`): |
| 42 | + |
| 43 | +```diff |
| 44 | +diff --git a/requests/models.py b/requests/models.py |
| 45 | +--- a/requests/models.py |
| 46 | ++++ b/requests/models.py |
| 47 | +@@ -405,6 +405,8 @@ class PreparedRequest(...): |
| 48 | + raise InvalidURL('URL has an invalid label.') |
| 49 | + elif host.startswith(u'*'): |
| 50 | + raise InvalidURL('URL has an invalid label.') |
| 51 | ++ elif host.startswith(u'.'): |
| 52 | ++ raise InvalidURL('URL has an invalid label.') |
| 53 | + |
| 54 | + # Carefully reconstruct the network location |
| 55 | + netloc = auth or '' |
| 56 | +``` |
| 57 | + |
| 58 | +Three layer attempts before landing: |
| 59 | + |
| 60 | +- **Attempt 1** (~7 min): worker produced the correct patch, reviewer |
| 61 | + approved, security-review clean, red-team clean. Merge failed with |
| 62 | + `merge_conflict_reject` — Forge tried to merge the worker branch (built |
| 63 | + on the 2020 `base_commit`) into a `main` that was cloned at 2024 |
| 64 | + GitHub tip. Genuine file conflict on `requests/models.py`. Retry. |
| 65 | +- **Attempt 2** (~14 min): worker added a new test file |
| 66 | + `tests/test_dot_host_invalid.py`. Reviewer correctly rejected — the |
| 67 | + adapted spec explicitly forbids adding tests. Retry. |
| 68 | +- **Attempt 3** (~8 min): worker produced the same patch as attempt 1, |
| 69 | + reviewer approved, merge fast-forwarded (we had patched `main` to |
| 70 | + `base_commit` mid-run by accident during debugging, which is exactly |
| 71 | + what `_pin_default_branch` now does permanently — see below). Patch |
| 72 | + extracted, evaluator ran F2P/P2P on the SWE-Bench test patch, both |
| 73 | + clean. |
| 74 | + |
| 75 | +## Blockers uncovered and fixes applied |
| 76 | + |
| 77 | +SWE-Bench's constraints — "pin to a 5-year-old commit, don't modify |
| 78 | +the upstream default branch, evaluate against a hidden held-out test |
| 79 | +patch" — are the inverse of Forge's default assumptions about |
| 80 | +green-field codebases with a living `main`. Every place Forge implicitly |
| 81 | +assumed "upstream is alive" needed a fix. |
| 82 | + |
| 83 | +### 1. TDD policy required verification tests |
| 84 | + |
| 85 | +Forge defaults to `features.require_verification_tests: true`: the |
| 86 | +worker must author a failing test, then make it pass. SWE-Bench |
| 87 | +*forbids* new tests — the evaluator applies its own `test_patch` after |
| 88 | +Forge finishes, and any extra worker-authored tests get scored as |
| 89 | +unexpected failures. |
| 90 | + |
| 91 | +**Fix** (harness-only): `build_forge_config` sets |
| 92 | +`features.require_verification_tests: false` in the synthesized |
| 93 | +`studio/config.yaml` for SWE-Bench runs, and the synthesized spec |
| 94 | +explicitly instructs the worker: *"DO NOT write, add, or modify any |
| 95 | +tests."* The default Forge config is untouched; normal autonomous |
| 96 | +coding still enforces TDD. |
| 97 | + |
| 98 | +### 2. Swap context manager lost on SIGKILL + Cursor auto-checkpoint |
| 99 | + |
| 100 | +`run_s0.py` swaps `studio/config.yaml` and `docs/TASKBOARD.md` to |
| 101 | +point at the cloned SWE-Bench repo for the duration of a run, then |
| 102 | +restores them in a `finally` block. Two interacting problems: |
| 103 | + |
| 104 | +1. SIGKILL bypasses Python's `finally`, leaving the repo with |
| 105 | + SWE-Bench config committed. |
| 106 | +2. Cursor's `auto-checkpoint.sh` stop hook runs `git add -A && git |
| 107 | + commit` between agent turns, which captured the mid-swap state. |
| 108 | + |
| 109 | +**Fix** (harness-only): `_git_skip_worktree` toggles the |
| 110 | +`skip-worktree` bit on the two swapped files for the duration of the |
| 111 | +run, so git (including the auto-checkpoint hook) can't see the |
| 112 | +modifications. `clear_stale_swap_sentinels()` runs at every harness |
| 113 | +startup to heal leftover bits from crashed previous runs. |
| 114 | + |
| 115 | +### 3. Worker's bare `pytest` loaded system Python's version |
| 116 | + |
| 117 | +Forge's claim-integrity check runs `pytest --collect-only -q` to verify |
| 118 | +worker-claimed tests actually exist. It resolves `pytest` via `PATH`. |
| 119 | +Without intervention, that resolved to system Python 3.11's pytest, |
| 120 | +which couldn't import the task's `conftest.py` because of a `urllib3` |
| 121 | +version mismatch — so zero tests collected, so *every* claim of |
| 122 | +"tests_added" was flagged as a lie. Worker was actually correct; |
| 123 | +Forge's integrity check was wrong about reality. |
| 124 | + |
| 125 | +**Fix** (harness-only): `_invoke_forge` prepends the task's venv |
| 126 | +`bin/` to `PATH` before exec'ing the Forge subprocess. The venv was |
| 127 | +already being built (`_build_venv`) with the right deps; we just |
| 128 | +weren't letting Forge see it. |
| 129 | + |
| 130 | +### 4. Two-dot diff notation over-reports vs merge-base — THE ONE CORE FIX |
| 131 | + |
| 132 | +Three call sites in `studio/workflows/orchestrator.py` computed |
| 133 | +diffs with two-dot notation: |
| 134 | + |
| 135 | +```python |
| 136 | +# Before |
| 137 | +run_shell(f"git diff {default_branch}..HEAD") |
| 138 | +# After |
| 139 | +run_shell(f"git diff {default_branch}...HEAD") |
| 140 | +``` |
| 141 | + |
| 142 | +Two-dot (`A..B`) compares endpoints directly: "everything in B that is |
| 143 | +not in A". Three-dot (`A...B`) anchors on `merge-base(A, B)`: "only |
| 144 | +what B introduced since diverging from A". In the common case where |
| 145 | +`main` hasn't moved since the branch was created, they are identical. |
| 146 | +**In every other case three-dot is strictly more correct** — the |
| 147 | +orchestrator was silently over-reporting whenever `main` moved during |
| 148 | +worker turn, which happens any time concurrent work is landing in the |
| 149 | +pipeline. The existing `platform/forge/branching/worktree.py::compare_branches` |
| 150 | +helper already used merge-base semantics via explicit `git merge-base`; |
| 151 | +the orchestrator just wasn't consistent with its own sibling code. |
| 152 | + |
| 153 | +Under SWE-Bench, `main` is **5 years ahead** of `base_commit`. Two-dot |
| 154 | +reported a 539 KB diff for a 1.2 KB actual change, which got truncated |
| 155 | +at `platform/forge/tools/shell.py:_MAX_OUTPUT_BYTES` (512 KiB) and |
| 156 | +auto-rejected as "too large". Secret scan also false-positived on |
| 157 | +upstream URL-ish strings. |
| 158 | + |
| 159 | +**Impact on autonomous coding:** strict improvement. Regression tests |
| 160 | +in `platform/tests/engine/test_git_workflow.py::TestDiffUsesMergeBaseNotation` |
| 161 | +reproduce the scenario and pin both that two-dot over-reports and that |
| 162 | +three-dot stays small. Source-level guard in |
| 163 | +`TestOrchestratorUsesMergeBaseNotation` prevents the two-dot pattern |
| 164 | +from creeping back in. 111 tests across the reviewer / security / |
| 165 | +merge / git_workflow suites stay green. |
| 166 | + |
| 167 | +### 5. Default branch stays at upstream tip after clone |
| 168 | + |
| 169 | +Even with the diff fix, `merge_node` still does a real `git merge` of |
| 170 | +the worker's branch into the default branch. `git clone` seeds `main` |
| 171 | +at upstream's current tip, which for `psf/requests` is ~365 commits |
| 172 | +ahead of `base_commit`. The merge of a `base_commit`-based branch into |
| 173 | +a 2024 `main` conflicts on exactly the file the worker modified. |
| 174 | + |
| 175 | +**Fix** (harness-only): `_pin_default_branch(repo_dir, base_commit)` |
| 176 | +runs in `_clone_repo` and force-resets `main` via `git checkout -B main |
| 177 | +<base_commit>`. Since SWE-Bench evaluation only needs the diff vs |
| 178 | +`base_commit` (see `_extract_patch`), the 2024 tip is dead weight; the |
| 179 | +harness discards it. Merges now fast-forward. |
| 180 | + |
| 181 | +## Did SWE-Bench bend Forge? |
| 182 | + |
| 183 | +**No.** Scope audit: |
| 184 | + |
| 185 | +| Surface | Change | Affects autonomous coding? | |
| 186 | +| --- | --- | --- | |
| 187 | +| `studio/workflows/orchestrator.py` | three-dot diff at 3 call sites | Yes — strict improvement | |
| 188 | +| `platform/forge/swebench/` (new dir) | entire harness | No — only runs under `python -m forge.swebench.run_s0` | |
| 189 | +| `platform/tests/swebench/` (new dir) | harness tests | No | |
| 190 | +| `platform/tests/engine/test_git_workflow.py` | 8 new regression tests | No | |
| 191 | + |
| 192 | +Core Forge gets **one** fix, and it's one it should have had since |
| 193 | +day one. Everything else is additive, gated behind the SWE-Bench |
| 194 | +harness entry point. |
| 195 | + |
| 196 | +## Adapted vs Pure |
| 197 | + |
| 198 | +Strategy defines how much scaffolding the harness synthesises between |
| 199 | +the raw SWE-Bench task and Forge's engine: |
| 200 | + |
| 201 | +- **Adapted**: harness synthesises a hand-crafted spec |
| 202 | + (`_SYNTHESIZED_SPECS` in `staging.py`) with explicit implementation |
| 203 | + guidance, scope bounds, and the "no tests" instruction. Models the |
| 204 | + upper bound of Forge's capability given good prompting. |
| 205 | +- **Pure**: harness uses the raw `problem_statement` from the |
| 206 | + SWE-Bench task as the spec. Models Forge's capability given only |
| 207 | + what the evaluator sees — zero human prompt engineering. |
| 208 | + |
| 209 | +### Results |
| 210 | + |
| 211 | +| Strategy | Resolved | Wall | Patch | F2P | P2P regressions | |
| 212 | +| --- | --- | --- | --- | --- | --- | |
| 213 | +| Adapted | yes | 2161s | 559 B | 1/1 | 0 | |
| 214 | +| Pure | _in progress_ | — | — | — | — | |
| 215 | + |
| 216 | +_This section is updated after each Pure run completes; see |
| 217 | +`.forge/swebench/psf__requests-5414/pure/run/report.json` for the |
| 218 | +latest._ |
| 219 | + |
| 220 | +## S1 scope |
| 221 | + |
| 222 | +Promoting SWE-Bench from S0 smoke to a recurring signal requires: |
| 223 | + |
| 224 | +1. **Task diversity.** Add 4-6 more SWE-Bench instances spanning: |
| 225 | + - Different repos (flask, django, pylint, scikit-learn) to |
| 226 | + shake out per-project install / dependency quirks. |
| 227 | + - Different difficulty tiers (easy / medium / hard) so the score |
| 228 | + doesn't saturate at 100% or 0%. |
| 229 | + - At least one instance whose gold patch touches >1 file, so we |
| 230 | + exercise multi-file coordination. |
| 231 | +2. **Multi-instance driver.** Move from per-instance shell invocations |
| 232 | + to a batch runner that handles fixture hydration, per-instance |
| 233 | + workspaces, and a rolling summary table. Reuse |
| 234 | + `forge.eval.harness_v2` patterns for consistency. |
| 235 | +3. **Pure-strategy delta as a tracked metric.** Record |
| 236 | + `adapted_resolved - pure_resolved` per instance; this quantifies |
| 237 | + how much value the harness's prompt synthesis is adding vs Forge's |
| 238 | + own decomposer. |
| 239 | +4. **Regression guard in CI.** Once we have ≥5 instances and >50% |
| 240 | + pass rate, wire into `.github/workflows/forge-canary.yml` so any |
| 241 | + orchestrator / reviewer / merge change that drops SWE-Bench pass |
| 242 | + count gets caught before merging. |
| 243 | +5. **Cost / time budgets.** S0 burned ~$5 worth of Sonnet + Opus over |
| 244 | + three attempts. At S1 scale (6 instances × 2 strategies × 3 |
| 245 | + attempts) that's ~$180/run. Cap per-instance wall clock at 30min |
| 246 | + and per-run cost at $30; emit a clear "budget exceeded" signal |
| 247 | + rather than silently spending. |
| 248 | + |
| 249 | +## References |
| 250 | + |
| 251 | +- Harness source: `platform/forge/swebench/` |
| 252 | +- Unit tests: `platform/tests/swebench/` |
| 253 | +- Integration tests: `platform/tests/engine/test_git_workflow.py::TestDiffUsesMergeBaseNotation` |
| 254 | +- Run artefacts: `.forge/swebench/<instance>/<strategy>/run/` |
| 255 | +- Full run log: `.forge/swebench/_logs/run-<timestamp>-<strategy>.log` |
| 256 | +- SWE-Bench upstream: <https://www.swebench.com/> |
| 257 | +- Adapter layer vs core Forge separation: this doc, "Did SWE-Bench |
| 258 | + bend Forge?" section above |
0 commit comments