Skip to content
17 changes: 17 additions & 0 deletions eval/fix/cases/001-human-fs-fix-add/annotations.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
state: open

expected_files:
- calc.py

labels:
forbidden: []

max_turns: 80
max_cost_usd: 8.00

# Human reference only; not consumed by judges (unlike triage/review quality).
fix_expectations: |
Comment thread
ascerra marked this conversation as resolved.
Human-triggered fix (TRIGGER_SOURCE=eval-human): case input.yaml supplies
human_instruction describing the add() bug. Success is a new commit on the
existing PR branch that touches calc.py (post-fix push). PRE_AGENT_HEAD is
the fixture commit SHA.
35 changes: 35 additions & 0 deletions eval/fix/cases/001-human-fs-fix-add/input.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
forge: github
# Single-line bug description supplied to the fix agent as HUMAN_INSTRUCTION
# (forwarded by setup-fixture.sh → run-fullsend.sh). Kept here with the
# fixture rather than hardcoded in the shared runner.
human_instruction: "calc.py's add() returns a - b instead of a + b. Change it to return a + b so the existing tests in tests/test_calc.py pass. Do not refactor beyond that fix."
fixture:
type: pull_request
title: "feat: add basic calculator"
body: |
Adds a tiny calculator module with tests.

Note: add() currently appears wrong in review — please fix via /fs-fix if needed.
base: main
files:
- path: calc.py
content: |
# Tiny calculator — intentional off-by-operator bug for the fix eval.


def add(a: int, b: int) -> int:
"""Return the sum of a and b."""
return a - b # BUG: should be a + b
- path: tests/test_calc.py
content: |
"""Tests for calc module."""

from calc import add


def test_add() -> None:
assert add(2, 3) == 5


def test_add_negative() -> None:
assert add(-1, -2) == -3
3 changes: 3 additions & 0 deletions eval/fix/cases/001-human-fs-fix-add/repo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# tiny-calc (fix eval)

Base branch is intentionally empty of product code. The PR under test adds a buggy calculator.
171 changes: 171 additions & 0 deletions eval/fix/eval.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
name: fix-eval
description: >
Functional test of the fullsend fix agent pipeline (pre → sandbox → post)
for a human /fs-fix trigger on an existing PR. Validates the post-script
pushes a new commit that touches the expected files.

skill: fix

execution:
mode: case
# Harness hard-kill. Keep slightly above runner.env.EVAL_TIMEOUT so the
# script-level timeout can fire first and trap cleanup EXIT still runs.
timeout: 1800 # fix harness timeout_minutes is 25; leave headroom
parallelism: 1
env:
EVAL_ORG: $EVAL_ORG
GH_TOKEN: $GH_TOKEN
FULLSEND_DIR: $FULLSEND_DIR
GOOGLE_APPLICATION_CREDENTIALS: $GOOGLE_APPLICATION_CREDENTIALS
ANTHROPIC_VERTEX_PROJECT_ID: $ANTHROPIC_VERTEX_PROJECT_ID
GOOGLE_CLOUD_PROJECT: $GOOGLE_CLOUD_PROJECT
CLOUD_ML_REGION: $CLOUD_ML_REGION

hooks:
before_each:
- command: "setup-fixture.sh"
timeout: 120
description: "Create ephemeral repo and PR fixture"

after_each:
- command: "capture-fixture.sh"
timeout: 60
description: "Capture PR head/files for judges"
- command: "teardown-fixture.sh"
timeout: 30
on_failure: continue
description: "Delete ephemeral repo"

runner:
type: cli
command:
- "run-fullsend.sh"
- "{agent}"
- "{workspace}"
- "{output_dir}"
env:
FULLSEND_DIR: $FULLSEND_DIR
GH_TOKEN: $GH_TOKEN
# Script-level timeout (slightly under execution.timeout) so trap cleanup runs.
EVAL_TIMEOUT: "1700"
GOOGLE_APPLICATION_CREDENTIALS: $GOOGLE_APPLICATION_CREDENTIALS
ANTHROPIC_VERTEX_PROJECT_ID: $ANTHROPIC_VERTEX_PROJECT_ID
GOOGLE_CLOUD_PROJECT: $GOOGLE_CLOUD_PROJECT
CLOUD_ML_REGION: $CLOUD_ML_REGION

models:
skill: claude-opus-4-6
judge: claude-opus-4-6

dataset:
path: cases
schema: |
Each case directory contains:
- input.yaml: PR fixture (base repo + PR files with the bug).
- annotations.yaml: Expected files and budgets.
- repo/: Base branch contents pushed to main before the PR.

outputs:
- path: output
schema: |
fixture-state.json: Captured PR state including head_sha, files,
and pre_agent_head for new_commit / expected_files judges.

# forbidden_labels / max_turns / max_cost below are shared verbatim with
# eval/code/eval.yaml and eval/review/eval.yaml — update all three if changing.
judges:
- name: new_commit
description: Post-script must push at least one new commit on the PR head
check: |
import json
state = json.loads(outputs["files"]["output/fixture-state.json"])
head = state.get("head_sha")
if not head:
return False, "head_sha missing from fixture-state.json"
baseline = state.get("pre_agent_head")
if not baseline:
# Fallback: annotations may declare fixture_head_sha after a dry run
baseline = outputs.get("annotations", {}).get("fixture_head_sha")
if not baseline:
return False, "pre_agent_head missing — cannot prove a new commit"
if str(head) == str(baseline):
return False, f"PR head unchanged ({head}) — fix post-script did not push"
return True, f"New commit on PR: {baseline} -> {head}"

- name: expected_files
description: PR must touch files listed in annotations.expected_files
check: |
import json
state = json.loads(outputs["files"]["output/fixture-state.json"])
expected = outputs.get("annotations", {}).get("expected_files") or []
if not expected:
return True, "No expected_files declared"
if state.get("files_fetch_failed"):
return False, "Could not fetch changed files for PR"
changed = set(state.get("files") or [])
missing = [p for p in expected if p not in changed]
if missing:
return False, f"Expected files missing from PR: {missing} (changed: {sorted(changed)})"
return True, f"All expected files present: {expected}"

- name: forbidden_labels
Comment thread
ascerra marked this conversation as resolved.
description: Labels listed in annotations.yaml forbidden list must NOT be present
check: |
import json
state = json.loads(outputs["files"]["output/fixture-state.json"])
actual = [l.lower() for l in state.get("labels", [])]
forbidden = outputs.get("annotations", {}).get("labels", {}).get("forbidden", [])
if not forbidden:
return True, "No forbidden labels specified"
present = [l for l in forbidden if l.lower() in actual]
if present:
return False, f"Forbidden labels present: {present} (actual: {actual})"
return True, f"No forbidden labels found (checked: {forbidden})"

- name: max_turns
description: Agent must complete within the declared turn budget
check: |
import json
raw = outputs["files"].get("output/metrics.json")
if not raw:
return False, "metrics.json not found"
metrics = json.loads(raw)
actual = metrics.get("num_turns")
if actual is None:
return False, "num_turns not present in metrics.json"
limit = outputs.get("annotations", {}).get("max_turns")
if limit is None:
return False, "max_turns not declared in annotations.yaml"
if int(actual) > int(limit):
return False, f"Exceeded max_turns: {actual} > {limit}"
return True, f"Turns OK: {actual} <= {limit}"

- name: max_cost
description: Agent must complete within the declared cost budget
check: |
import json
raw = outputs["files"].get("output/metrics.json")
if not raw:
return False, "metrics.json not found"
metrics = json.loads(raw)
actual = metrics.get("total_cost_usd")
if actual is None:
return False, "total_cost_usd not present in metrics.json"
limit = outputs.get("annotations", {}).get("max_cost_usd")
if limit is None:
return False, "max_cost_usd not declared in annotations.yaml"
if float(actual) > float(limit):
return False, f"Exceeded max_cost_usd: {actual} > {limit}"
return True, f"Cost OK: {actual} <= {limit}"

thresholds:
new_commit:
min_pass_rate: 1.0
expected_files:
min_pass_rate: 1.0
forbidden_labels:
min_pass_rate: 1.0
max_turns:
min_pass_rate: 1.0
max_cost:
min_pass_rate: 1.0
Loading
Loading