Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.0.23] - 2026-08-05

### Changed

- **`mini_swe_agent_v2` submits a pull request instead of writing `patch.txt`.** Submission was a unified diff the agent wrote to a local file, which nothing else could see — so the artifact that got graded was decoupled from anything a colleague could read, and an agent could submit work it had never shared. The agent now commits what it wants to submit, pushes its branch, and opens a PR; the PR is what is graded. A `gh` shim (`connectors/gh_shim.sh`) implements `gh pr create/list/view/diff/checkout` over the shared remote using plain git, so agents use the spelling they already know rather than a command invented for this benchmark. A PR **tracks its branch**, as on a forge: agents are told to open one early so a colleague has context, and later commits are included once pushed.

Consequences worth knowing: an agent that never opens a PR submits nothing and scores zero — that is an agent failure, and it is logged (`NO PR OPENED by <agent>`) so it stays attributable rather than looking like failed tests. Solo runs use the same path against a bare repo inside their own sandbox, so there is one submission mechanism rather than two that can drift. Both prompts got *smaller*: `patch.txt` needed ~30 lines teaching a diff incantation that exists nowhere in real engineering, while `gh pr create` needs none.

- **The shared remote is now `origin`, and the clone's upstream is removed.** It was `team`, while `origin` pointed at github.com and was unreachable — so the name agents reach for by reflex was the one that could not work.

### Fixed

- **Coop agents could read the entire upstream history, including the commits that came after the task commit.** A task image runs `git clone <upstream> && git checkout <task-sha>`, which leaves every later commit reachable through `refs/remotes/origin/*`, tags, and the local branch the clone left at the tip — `git checkout <sha>` only detaches HEAD, it does not move that branch. For any task derived from a real pull request, `git log --all -p` could therefore show the upstream implementation of the feature the agent was being asked to write. Setup now removes the remote, deletes remote refs, tags and leftover local branches, expires the reflog and prunes, so those objects are gone from the object database rather than merely unreferenced (verified with `git cat-file -e`). History *before* the task commit is kept — that is ordinary context, and `git log` still works.

- **A submission could be silently re-baselined by anyone pushing to `main`.** Submissions were diffed against `origin/main`, a movable ref on a daemon that runs `--enable=receive-pack` with no access control. One `git push origin HEAD:main`, from either agent, would change what both submissions contained. The base commit is now pinned at setup.

- **A failed `gh` shim install was a log warning.** Submission goes through `gh pr create`, so a sandbox without the shim cannot submit at all — and it would have surfaced hours later as an empty patch, indistinguishable from an agent that simply failed the task. Setup now raises.

### Fixed

- **Coop agents were told the shared git remote was read-only, so they never shared code.** The prompt titled the section "Shared Git Remote (read-only)", listed only `fetch`/`log`/`diff` under "Allowed (read-only)", never mentioned `push`, and instructed *"Do not merge, pull, or rebase their branch into yours"*. Both claims are wrong: `GitConnector` runs `git daemon --enable=receive-pack`, so the remote accepts writes, and grading never reads those branches — the evaluator applies each agent's submitted `patch.txt` to branches it creates itself (`eval/sandbox.py:473,480`). Measured across **117 trajectories in three `flash_10` runs: 292 `git fetch team` and 0 `git push team`.** Every one of those fetches returned the untouched baseline, because 0.0.22 only publishes an agent's patch at exit — by which point the peer can no longer act on it. Agents were following their instructions exactly; the instructions disabled the channel.

This prompt was the only surface that said so. `GitConnector`'s own docstring already advertised `git push team <branch>` and `git merge team/<agent>`, and the shared coop prompt used by the `claude_code` / `codex` adapters (`agents/_coop/prompt.py`) already instructs agents to commit and "push so peers can fetch you". Only `mini_swe_agent_v2`'s coop config disagreed with the infrastructure it runs on.

The section now describes a real read/write remote, tells agents to commit and `git push team HEAD:<agent_id>` as they work so a colleague can read their actual diff, and replaces the blanket prohibition with the one constraint that genuinely matters: **the submitted `patch.txt` must contain only that agent's own changes**, since the two submitted patches are merged and duplicated edits break the merge. Local `fetch`/`merge`/`cherry-pick` are now explicitly allowed — the rule is about what you submit, not what you do in your worktree — with `git diff -- <paths>` given as the way to scope a patch when both agents had to touch the same file.

### Changed

- Exit-time publication of `patch.txt` to `team/<agent_id>` (added in 0.0.22) is unchanged and still runs; it is now described as the *final* state of a branch agents are expected to have been pushing to all along, rather than the only thing that branch ever holds.

## [0.0.22] - 2026-08-04

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion src/cooperbench/__about__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Version information for CooperBench."""

__version__ = "0.0.22"
__version__ = "0.0.23"
30 changes: 16 additions & 14 deletions src/cooperbench/agents/mini_swe_agent_v2/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,14 +205,15 @@ def run(
model_cfg = {**model_cfg, "model_kwargs": model_kwargs}
model = LitellmModel(model_name=model_name, **model_cfg)

# Setup git connector if enabled
if git_enabled and git_server_url and agents:
git_connector = GitConnector(
agent_id=agent_id,
agents=agents,
server_url=git_server_url,
)
git_connector.setup(env)
# Always set up a git connector. Coop points it at the shared server; solo gets a
# bare repo in its own sandbox. Both then submit by opening a PR, so there is one
# submission path rather than two that can drift apart.
git_connector = GitConnector(
agent_id=agent_id,
agents=agents or [agent_id],
server_url=git_server_url if (git_enabled and git_server_url) else "",
)
git_connector.setup(env)

# Setup team CLI in the container if either of its consumers
# (the task_list or the typed protocol verbs) is active. Both
Expand Down Expand Up @@ -251,15 +252,16 @@ def run(
status = "Error"
error_msg = str(e)

# The submitted artifact is the agent's PR, not a local file. Reading a local
# patch.txt let an agent submit work it had never shared, so its colleague could not
# see what was coming and the shared remote had nothing to show. The PR is pushed, so
# what gets graded is exactly what the other agent could read.
patch = ""
try:
r = env.execute({"command": "cat patch.txt 2>/dev/null"})
if r.get("returncode") == 0:
# git apply rejects diffs without a terminal newline; normalize
# to one trailing newline (matches claude_code / codex adapters).
from cooperbench.agents._coop.runtime import normalize_patch
from cooperbench.agents._coop.runtime import normalize_patch

patch = normalize_patch(r.get("output") or "")
raw = git_connector.submitted_patch(env)
patch = normalize_patch(raw)
except Exception:
pass

Expand Down
76 changes: 14 additions & 62 deletions src/cooperbench/agents/mini_swe_agent_v2/agents/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

# Name of the shared git remote created by GitConnector. Referenced in the messages the
# agent sees, so it must match GitConnector.REMOTE_NAME.
GIT_REMOTE = "team"
GIT_REMOTE = "origin"


class AgentConfig(BaseModel):
Expand Down Expand Up @@ -201,73 +201,25 @@ def run(self, task: str = "", **kwargs) -> dict:
finally:
self.save(self.config.output_path)
if self.messages[-1].get("role") == "exit":
published = self._publish_final_work()
if self.comm:
self.comm.mark_exited(published=published)
# `published` means "the peer can see my work on the remote". That is now
# true exactly when the agent opened a PR, which it does itself -- there
# is no separate publish step to perform on its behalf.
self.comm.mark_exited(published=self._opened_pr())
break
return self.messages[-1].get("extra", {})

def _publish_final_work(self) -> bool:
"""Publish this agent's *submitted patch* to the shared remote before exiting.
def _opened_pr(self) -> bool:
"""Whether this agent's PR exists on the shared remote.

The remote is seeded with the base commit at setup and never updated again, so a
peer inspecting ``{GIT_REMOTE}/<agent>`` sees the untouched baseline no matter how
much work was done — and reasonably concludes their colleague has not started.
The prompt presents that remote as the sanctioned way to see a colleague's code,
so today it is a documented capability that silently returns nothing.

We publish ``patch.txt``, not the working tree: patch.txt is the artifact that
gets evaluated and merged, and agents are free to submit a subset of their edits.
Showing a peer the tree would show them something other than what will be merged.

Built in a detached worktree so the agent's own branch, index and working tree
are untouched — the adapter still reads patch.txt from the container afterwards.

Returns True only when the patch actually reached the remote. Peers are told to
read that branch, so a failed publication must not be reported as a success.
Best-effort otherwise: publication must never fail a run.
Replaces `_publish_final_work`, which pushed `patch.txt` to the agent's branch at
exit. Submission is now a PR the agent opens itself, so there is nothing left to
publish -- that method only logged `no patch.txt to publish` on every run.
"""
if not getattr(self, "comm", None):
return False # solo run: nobody to publish to
agent_id = self.comm.agent_id
script = (
"set -e; "
# the repo is normally /workspace/repo, but fall back rather than guess
'repo=/workspace/repo; [ -d "$repo/.git" ] || repo=/workspace; cd "$repo"; '
# No patch means nothing to publish. Exit non-zero so this is NOT reported as
# a successful publication -- claiming the branch holds their submission when
# it holds the baseline is the exact failure this change removes.
'test -s patch.txt || { echo "no patch.txt to publish"; exit 3; }; '
# Branch from the pristine base, not from HEAD: the evaluator applies patch.txt
# to the base, so mirroring that is what makes the branch equal the submission.
# If the agent committed its work, HEAD already contains it and applying the
# patch on top would double-apply. setup() seeds the remote's main with base.
f"git fetch -q {GIT_REMOTE} 2>/dev/null || true; "
f"base=$(git rev-parse {GIT_REMOTE}/main 2>/dev/null "
"|| git rev-parse main 2>/dev/null || git rev-parse HEAD); "
'tmp=$(mktemp -d); git worktree add -q --detach "$tmp" "$base"; '
'cp patch.txt "$tmp/.__submitted.patch"; cd "$tmp"; '
"git apply --ignore-whitespace .__submitted.patch || git apply --3way .__submitted.patch; "
"rm -f .__submitted.patch; git add -A; "
f"git -c user.email=agent@cooperbench -c user.name={agent_id} "
f'commit -q --allow-empty -m "submitted work by {agent_id}"; '
f"git push -q -f {GIT_REMOTE} HEAD:refs/heads/{agent_id}; "
# `worktree remove` matches on git's own resolved path, which differs from
# $tmp wherever the temp dir sits behind a symlink; deleting the directory and
# pruning is equivalent and does not depend on that matching.
'cd /; rm -rf "$tmp"; git -C "$repo" worktree prune 2>/dev/null || true'
)
try:
result = self.env.execute({"command": script})
if (result or {}).get("returncode") == 0:
self.log(f"PUBLISHED submitted patch to {GIT_REMOTE}/{agent_id}")
return True
self.logger.warning(
f"could not publish to {GIT_REMOTE}/{agent_id}: {(result or {}).get('output', '')[:300]}"
)
except Exception as e: # noqa: BLE001 - never fail a run over publication
self.logger.warning(f"could not publish to {GIT_REMOTE}/{agent_id}: {e}")
return False
if not self.comm:
return False
r = self.env.execute({"command": f"git ls-remote --tags {GIT_REMOTE} refs/tags/pr/{self.comm.agent_id}"})
return bool((r.get("output") or "").strip())

def step(self) -> list[dict]:
"""Query the LM, execute actions. Polls for inter-agent messages
Expand Down
62 changes: 28 additions & 34 deletions src/cooperbench/agents/mini_swe_agent_v2/config/coop.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ agent:
5. Test edge cases to ensure your fix is robust
6. Submit your changes — see the **Submission** section below for the exact procedure.

Working as a team, open your pull request as soon as you know which files you are
taking, not at the end — it is how your colleague finds out what you are doing.
Keep committing and pushing to it as you go.

But you are not working solo but rather as a team, so follow the workflow above for your individual tasks but you have complete freedom to communicate with your colleague in whatever way, whenever, and however often you see fit. Think about how experienced software engineers coordinate when working on the same codebase — and do that.

{% if messaging_enabled %}
Expand Down Expand Up @@ -61,28 +65,31 @@ agent:
{% endif %}

{% if git_enabled %}
## Shared Git Remote (read-only)

A shared remote called `team` lets you see your colleague's actual code changes. Use communication as your primary way to coordinate. Use git only to peek at your colleague's code in complex cases or to verify merging towards the end. Think about how experienced software engineers would make the most of direct communication and git at various stages — and do that.
## Shared Git Remote

**What that branch holds, and when.** `team/{{ agents | reject('equalto', agent_id) | first }}` stays at the repository's starting state until your colleague submits; their patch is published there automatically at that moment. So an empty diff early on means "they have not submitted yet" — it does NOT mean they have made no changes. Ask them directly rather than inferring from git. You are told explicitly when they finish.
`origin` is a real git remote you can push to and fetch from. Your colleague
cannot see your working tree — only what you push.

Your patches are merged automatically after you both submit. Do not merge, pull, or rebase their branch into yours, as the automatic merge will see duplicate changes and fail.
Share progress as you work:
```bash
git add <only the changes you want to share>
git commit -m "wip: <what changed>"
git push
```

Allowed (read-only):
See what your colleague is doing — their PR if they opened one, otherwise
whatever they have pushed:
```bash
git fetch team
git log team/{{ agents | reject('equalto', agent_id) | first }} --oneline
git diff HEAD...team/{{ agents | reject('equalto', agent_id) | first }} -- <file>
gh pr list
gh pr diff {{ agents | reject('equalto', agent_id) | first }}
git fetch origin && git diff origin/main origin/{{ agents | reject('equalto', agent_id) | first }}
```

Check for conflicts (dry-run merge):
Check whether you would collide, and settle it over messaging if so:
```bash
git merge --no-commit --no-ff team/{{ agents | reject('equalto', agent_id) | first }} && git merge --abort
git merge --no-commit --no-ff origin/{{ agents | reject('equalto', agent_id) | first }} && git merge --abort
```
If this reports conflicts, you may coordinate with your colleague via messaging to adjust your approaches.

Do NOT run: `git merge` (without --abort), `git pull`, `git rebase`, or `git reset --hard` against your colleague's branch or `origin/main`. These will corrupt your patch.
{% endif %}

## Command Execution Rules
Expand Down Expand Up @@ -167,37 +174,24 @@ agent:

## Submission

`patch.txt` is the artifact we evaluate — write whatever unified diff
you want to submit to that file, however it makes sense given how you
worked:

Write the patch (one common way — `git diff` of your in-place edits):
Your pull request is your submission. Open it early so your colleague knows
what you are taking.

```bash
git diff -- path/to/file1 path/to/file2 > patch.txt
git push
gh pr create --title "..." --body "..."
```

Verify it contains what you intend:
Both PRs are merged and both feature test suites run against the result, so
your PR must contain only your own work — do not commit your colleague's
changes into your branch.

```bash
cat patch.txt
```

Submit (EXACT command required)
You MUST use this EXACT command to submit:
End the task (EXACT command required):

```bash
echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT
```

The patch must be a unified diff and contain only source files you
intentionally modified. Exclude:

- reproduction or scratch test scripts you wrote
- helper scripts or tools you created
- installation, build, packaging, or configuration files
- binaries or compiled files

<CRITICAL>
Do NOT run `rm -rf .git`, `git init`, `git rm -rf .`, or `git reset --hard`
inside `/workspace/repo` — these corrupt `.git/` and your patch will be
Expand Down
28 changes: 6 additions & 22 deletions src/cooperbench/agents/mini_swe_agent_v2/config/solo.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -99,37 +99,21 @@ agent:

## Submission

`patch.txt` is the artifact we evaluate — write whatever unified diff
you want to submit to that file, however it makes sense given how you
worked:

Write the patch (one common way — `git diff` of your in-place edits):
Your pull request is your submission.

```bash
git diff -- path/to/file1 path/to/file2 > patch.txt
git add <only the changes you want to submit>
git commit -m "<what you did>"
git push
gh pr create --title "..." --body "..."
```

Verify it contains what you intend:

```bash
cat patch.txt
```

Submit (EXACT command required)
You MUST use this EXACT command to submit:
End the task (EXACT command required):

```bash
echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT
```

The patch must be a unified diff and contain only source files you
intentionally modified. Exclude:

- reproduction or scratch test scripts you wrote
- helper scripts or tools you created
- installation, build, packaging, or configuration files
- binaries or compiled files

<CRITICAL>
Do NOT run `rm -rf .git`, `git init`, `git rm -rf .`, or `git reset --hard`
inside `/workspace/repo` — these corrupt `.git/` and your patch will be
Expand Down
Loading
Loading