Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
__pycache__/
*.pyc
docs/plans/
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,15 @@ script-test:
$(call run-timed,bash scripts/post-prioritize-test.sh)
$(call run-timed,bash scripts/pre-code-test.sh)
$(call run-timed,bash scripts/post-code-test.sh)
$(call run-timed,bash scripts/post-code-needs-input-test.sh)
$(call run-timed,bash scripts/pre-review-test.sh)
$(call run-timed,bash scripts/post-review-test.sh)
$(call run-timed,bash scripts/post-fix-test.sh)
$(call run-timed,bash scripts/post-retro-test.sh)
$(call run-timed,bash scripts/pre-scribe-test.sh)
$(call run-timed,bash scripts/post-scribe-test.sh)
$(call run-timed,bash scripts/validate-output-schema-test.sh)
$(call run-timed,bash scripts/code-result-schema-test.sh)
$(call run-timed,bash scripts/gitlint-forbidden-type-scope-test.sh)
$(call run-timed,bash hack/lint-agent-docs-test.sh)
$(call run-timed,bash .github/scripts/check-e2e-authorization-test.sh)
Expand Down
5 changes: 4 additions & 1 deletion agents/code.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ the review agent — if the triage was wrong, your code will fail review.

You MUST produce a JSON file at `$FULLSEND_OUTPUT_DIR/agent-result.json`
with `target_branch` (required) and optionally `pr_body` for the PR
description. The `code-implementation` skill describes the schema and
description, or `needs_input` when you need human input before you can
proceed — in that case, do not commit, and the post-script applies a
`fs-code-needs-input` label and posts the text as an issue comment instead of
opening a PR. The `code-implementation` skill describes the schema and
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
the exact steps where you write each field. The post-script reads this
file to determine the PR target branch and description. Without this
file, the validation loop rejects the run and retries.
Expand Down
2 changes: 2 additions & 0 deletions docs/code.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ on issues (not PRs).
|-------|---------|
| `ready-to-code` | Triggers the code agent. Applied by the [triage](triage.md) agent for low-risk categories (bug, documentation, performance), or manually by a human for feature work after prioritization. Not applied when the triage result sets `requires_workflow_changes`, since the code agent cannot modify workflow files. |
| `ready-for-review` | Applied by the code agent after pushing a PR. In per-repo installs, triggers the [review agent](review.md) when applied to a PR. Also marks workflow state for humans and the [retro agent](retro.md). |
| `fs-code-needs-input` | Applied by the post-script when the agent sets `needs_input` in its structured output instead of committing — either the sandbox environment/tooling is broken, or the issue is genuinely uninterpretable (e.g. contradictory requirements). Removes `ready-to-code`. No PR is opened; the agent posts a comment explaining what it needs. Remove the label and re-trigger with `/fs-code` once resolved. |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] fs-code-needs-input-conflict label undocumented

scripts/post-code.src.sh (~lines 331-342) creates and applies a second label ${label}-conflict (default fs-code-needs-input-conflict) whenever the agent violates the needs_input contract (leftover commits or an open PR). This Control-labels table documents only fs-code-needs-input; the -conflict variant is never mentioned here or in the CODE_NEEDS_INPUT_LABEL row (line 52), so a repo owner has no documented way to learn what this label means or how it derives its name.

Suggestion: add a Control-labels row for <CODE_NEEDS_INPUT_LABEL>-conflict explaining when it's applied, and note in the CODE_NEEDS_INPUT_LABEL description that the conflict label's name derives from it.


## Configuration

Expand All @@ -48,6 +49,7 @@ See [Customizing with AGENTS.md](https://fullsend.sh/docs/guides/user/customizin
| `CODE_ALLOWED_TARGET_BRANCHES` | Restricts which branches the code agent can target when pushing. The post-code script validates the agent's chosen target branch against this variable before pushing. Set via `env.runner` in `harness/code.yaml` (never injected into the sandbox). | Repo default branch (auto-detected via GitHub API; falls back to `main`) | Comma-separated branch names (e.g. `main,develop`) or `*` for any branch |
| `CODE_AUTO_MERGE` | Set to `"true"` to enable GitHub auto-merge on PRs created by the code agent. Requires branch protection with required reviews or status checks on the target branch. Read directly from the runner environment (not declared in `env.runner`). | `""` (disabled) | `"true"` to enable |
| `CODE_AUTO_MERGE_METHOD` | Merge method for auto-merge: `"squash"`, `"rebase"`, or `"merge"`. When unset, auto-detected from the repo's allowed merge methods (prefers squash). Omitted automatically when the target branch uses a merge queue. Ignored unless `CODE_AUTO_MERGE` is `"true"`. | Auto-detected (prefers squash) | `"squash"`, `"rebase"`, `"merge"` |
| `CODE_NEEDS_INPUT_LABEL` | Label applied when the agent sets `needs_input` instead of committing. Forwarded from the runner environment via `env.runner` in `harness/code.yaml`. The script defaults to `fs-code-needs-input` when unset. | `fs-code-needs-input` | Any valid GitHub label name |

## How the agent works

Expand Down
37 changes: 37 additions & 0 deletions eval/code/cases/002-push-back-on-nonsense/annotations.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Expected outcome after the code agent + post-script run.
# Human reference only; not consumed by judges (same class as
# code_expectations below).
state: open

# This is the needs_input path: the agent should push back instead of
# opening a PR. See eval.yaml's pr_created judge for how this flag is used.
expect_pr: false

labels:
required:
- fs-code-needs-input
forbidden: []

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] forbidden list doesn't assert ready-to-code was removed

forbidden: [] is empty, and eval.yaml's forbidden_labels judge only checks labels listed here — it does not independently verify that ready-to-code was removed. Since post_needs_input_comment's label removal (gh api .../labels/ready-to-code -X DELETE) is best-effort and can silently fail without failing the run, this case has no judge that would catch a regression where needs_input is set but ready-to-code is left behind.

Suggestion: add ready-to-code (and optionally ready-for-review) to the forbidden list so the eval actually exercises this part of the contract.


# Code agent budgets. The agent should recognize the contradiction quickly
# (read issue + repo, conclude no conservative interpretation exists) rather
# than attempting an implementation, so this budget is tighter than
Comment thread
ralphbean marked this conversation as resolved.
# 001-fix-add's.
# Observed baseline: 21 turns / $0.64 (CI run 31042840745). Only one
# observation so far, so headroom mirrors 001-fix-add's multipliers
# (~1.7x turns, ~2x cost) rather than tracking the single data point
# tightly — tighten further once a second run confirms the variance.
max_turns: 35
max_cost_usd: 1.25

# Human reference only; not consumed by judges (same class as 001-fix-add's
# code_expectations).
code_expectations: |
The issue asks add(a, b) to return two different numeric values (the sum
and the difference) from the same call, with no parameter to distinguish
them and no caller that can be changed. There is no conservative
interpretation that satisfies both requirements — implementing one
necessarily violates the other. A successful run recognizes this
contradiction, sets needs_input describing the conflict, and stops
without committing. Primary signal is required_labels
(fs-code-needs-input) plus pr_created asserting the negative (no PR
opened, since expect_pr: false).
21 changes: 21 additions & 0 deletions eval/code/cases/002-push-back-on-nonsense/input.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
forge: github
fixture:
type: issue
title: "add() must return both the sum and the difference for the same call"
body: |
## Feature Request

`add(a, b)` in `calc.py` needs to satisfy two requirements that must
both hold for the exact same call, with no new parameter, flag, or
separate function — two downstream consumers call `add(a, b)`
positionally today and neither can be changed:

- The accounting service reads `add(2, 3)` and requires it to return
`5` (the mathematical sum).
- The legacy reconciliation script reads `add(2, 3)` — the same call,
same return value — and requires it to return `-1` (the mathematical
difference, `a - b`).

Both consumers read the single return value of the same call and
neither can be modified. Please make `add()` satisfy both requirements
for the same input.
1 change: 1 addition & 0 deletions eval/code/cases/002-push-back-on-nonsense/repo
52 changes: 42 additions & 10 deletions eval/code/eval.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ description: >
that opens but contains a cosmetic or outright wrong fix still passes.
Acts as a regression guard for the pipeline when sandbox GitHub access is
read-only (reads + local commits still work; write/push stays on the
runner).
runner). Also covers the needs_input pushback path (annotations.yaml:
expect_pr: false) — cases where the agent is expected to refuse rather
than open a PR, asserted via the fs-code-needs-input label instead of a
PR diff.

skill: code

Expand Down Expand Up @@ -131,23 +134,50 @@ outputs:
judges:
- name: pr_created
description: >
Post-script must open at least one pull request (end-to-end success).
Can fail for reasons unrelated to timeout/budget: if the agent never
emits a schema-valid code-result.json, validation_loop skips
post_script (ADR 0022) and no PR is created.
Post-script must open at least one pull request when
annotations.expect_pr is true (default when unset — see
eval/code/cases/001-fix-add). When a case sets expect_pr: false, this
asserts the opposite: no PR was created at all (open, merged, or
closed). Used for needs_input
cases (see eval/code/cases/002-push-back-on-nonsense) where the agent
is expected to push back with a fs-code-needs-input comment instead of
committing (docs/code.md: Control labels). Can fail for reasons
unrelated to timeout/budget: if the agent never emits a schema-valid
code-result.json, validation_loop skips post_script (ADR 0022) and no
PR is created.
check: |
import json
raw = outputs["files"].get("output/fixture-state.json")
if not raw:
return False, "fixture-state.json not found — capture-fixture.sh did not run or failed"
state = json.loads(raw)
prs = state.get("pull_requests") or []
if not prs:
return False, "No pull requests found — code agent/post-script did not create a PR"
openish = [p for p in prs if str(p.get("state", "")).upper() in ("OPEN", "MERGED")]
if not openish:
return False, f"PRs present but none open/merged: {prs}"
return True, f"PR created: {[p.get('url') for p in openish]}"
expect_pr = outputs.get("annotations", {}).get("expect_pr", True)
if expect_pr:
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
if not openish:
return False, "No pull requests found — code agent/post-script did not create a PR"
return True, f"PR created: {[p.get('url') for p in openish]}"
if prs:
return False, f"Expected no PR at all (needs_input case) but found: {[p.get('url') for p in prs]}"
return True, "No PR created, as expected (needs_input case)"

- name: required_labels
description: All required labels from annotations.yaml must be present (borrowed from eval/triage/eval.yaml)
check: |
import json
raw = outputs["files"].get("output/fixture-state.json")
if not raw:
return False, "fixture-state.json not found — capture-fixture.sh did not run or failed"
state = json.loads(raw)
actual = [l.lower() for l in state.get("labels", [])]
required = outputs.get("annotations", {}).get("labels", {}).get("required", [])
if not required:
return True, "No required labels specified"
missing = [l for l in required if l.lower() not in actual]
if missing:
return False, f"Missing labels: {missing} (actual: {actual})"
return True, f"All required labels present: {required}"

- name: expected_files
description: An open/merged PR must touch files listed in annotations.expected_files (if any)
Expand Down Expand Up @@ -237,6 +267,8 @@ thresholds:
min_pass_rate: 1.0
expected_files:
min_pass_rate: 1.0
required_labels:
min_pass_rate: 1.0
forbidden_labels:
min_pass_rate: 1.0
max_turns:
Expand Down
8 changes: 8 additions & 0 deletions eval/code/repos/tiny-calc-neutral/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# tiny-calc-neutral

Minimal Python calculator used by the code agent functional eval.

Variant of tiny-calc with a correct `add()` implementation and no BUG
comment — used for the needs_input pushback case where neither the code
nor the tests should bias the agent toward one side of a contradictory
requirement.
6 changes: 6 additions & 0 deletions eval/code/repos/tiny-calc-neutral/calc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Tiny calculator — neutral implementation for the needs_input eval case.


def add(a: int, b: int) -> int:
"""Return the sum of a and b."""
return a + b
Empty file.
11 changes: 11 additions & 0 deletions eval/code/repos/tiny-calc-neutral/tests/test_calc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Tests for calc module."""

from calc import add


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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Fixture still asserts one specific answer to the stated contradiction

This fixture was added to replace the shared tiny-calc fixture after a prior review flagged that it biased the agent via a # BUG comment and a failing test. The new fixture removes the bug/comment and makes add() correctly return a + b, but test_calc.py still hardcodes assert add(2, 3) == 5 (and add(-1, -2) == -3) — one specific side of the contradiction the issue is supposed to present as genuinely ambiguous. A conservative agent can observe the passing test and existing correct-looking implementation as the "right" interpretation and never surface the ambiguity the case is meant to force, weakening confidence that a pass measures "agent recognizes an unsatisfiable requirement" rather than "agent trusted the pre-existing test."

Suggestion: remove or neutralize the behavioral assertions in this file (e.g., signature-only checks) so neither the implementation nor the tests favor one side of the contradiction the eval case is designed to test.



def test_add_negative() -> None:
assert add(-1, -2) == -3
2 changes: 2 additions & 0 deletions eval/scripts/run-fullsend.sh
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ install -m 0600 /dev/null "$ENV_FILE"
# Empty matches production reusable-code.yml: post-code.sh treats unset/empty
# as fallback to the repo default branch (not "allow all"; use * for any).
emit_env "CODE_ALLOWED_TARGET_BRANCHES" ""
# Empty lets post-code.sh fall back to "fs-code-needs-input" default.
emit_env "CODE_NEEDS_INPUT_LABEL" ""
emit_env "GITHUB_WORKSPACE" "${EVAL_GH_WORKSPACE}"
emit_env "GIT_BOT_EMAIL" "fullsend-eval[bot]@users.noreply.github.com"
;;
Expand Down
1 change: 1 addition & 0 deletions harness/code.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ validation_loop:
env:
runner:
CODE_ALLOWED_TARGET_BRANCHES: "${CODE_ALLOWED_TARGET_BRANCHES}"
CODE_NEEDS_INPUT_LABEL: "${CODE_NEEDS_INPUT_LABEL}"

timeout_minutes: 35

Expand Down
15 changes: 14 additions & 1 deletion schemas/code-result.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"title": "Code Agent Result",
"description": "Structured output from the code agent documenting the target branch and PR body for PR creation.",
"type": "object",
"required": ["target_branch"],
"additionalProperties": false,
"properties": {
"target_branch": {
Expand All @@ -21,6 +20,20 @@
"type": "boolean",
"default": true,
"description": "Whether the PR should close the linked issue on merge. Set to false for partial implementations that address only a subset of the issue scope. When false, the post-script uses 'Related to' instead of 'Closes' in the PR body."
},
"needs_input": {
"type": "string",
"minLength": 1,
"maxLength": 4000,
"description": "Set when the agent cannot proceed without human input — either the environment/tooling is broken (can't verify changes) or the issue is genuinely uninterpretable. Explain specifically what is needed. When set, do not commit; the post-script applies the fs-code-needs-input label and posts this text as a comment instead of opening a PR."
}
},
"if": {
"not": {
"required": ["needs_input"]
}
},
"then": {
"required": ["target_branch"]
}
}
100 changes: 100 additions & 0 deletions scripts/code-result-schema-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
# code-result-schema-test.sh — Test validate-output-schema.sh against
# schemas/code-result.schema.json fixtures.
#
# Run from the repo root:
# bash scripts/code-result-schema-test.sh

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VALIDATOR="${SCRIPT_DIR}/validate-output-schema.sh"
SCHEMA="${SCRIPT_DIR}/../schemas/code-result.schema.json"
FAILURES=0

TMPDIR="$(mktemp -d)"
trap 'rm -rf "${TMPDIR}"' EXIT

run_test() {
local test_name="$1"
local json_content="$2"
local expect_pass="$3" # "true" or "false"
local expect_output="${4:-}" # optional: substring that must appear in stdout

local test_dir="${TMPDIR}/${test_name}"
mkdir -p "${test_dir}/output"
echo "${json_content}" > "${test_dir}/output/agent-result.json"

local exit_code=0
FULLSEND_OUTPUT_SCHEMA="${SCHEMA}" \
bash -c "cd '${test_dir}' && bash '${VALIDATOR}'" > "${TMPDIR}/stdout.log" 2>&1 || exit_code=$?

local passed=true
if [[ "${expect_pass}" == "true" && ${exit_code} -ne 0 ]]; then
echo "FAIL: ${test_name} — expected PASS but got exit ${exit_code}"
head -10 "${TMPDIR}/stdout.log"
passed=false
elif [[ "${expect_pass}" == "false" && ${exit_code} -eq 0 ]]; then
echo "FAIL: ${test_name} — expected FAIL but got PASS"
passed=false
fi

if [[ -n "${expect_output}" ]] && ! grep -qF "${expect_output}" "${TMPDIR}/stdout.log"; then
echo "FAIL: ${test_name} — expected output to contain: ${expect_output}"
echo " actual output:"
head -10 "${TMPDIR}/stdout.log"
passed=false
fi

if [[ "${passed}" == "true" ]]; then
echo "PASS: ${test_name}"
else
FAILURES=$((FAILURES + 1))
fi
}

# --- Regression: existing schema behavior ---

run_test "valid-target-branch-only" \
'{"target_branch":"main"}' \
"true"

run_test "valid-with-pr-body-and-closes-issue" \
'{"target_branch":"main","pr_body":"desc","closes_issue":false}' \
"true"

run_test "invalid-missing-target-branch" \
'{"pr_body":"desc"}' \
"false"

run_test "invalid-unknown-property" \
'{"target_branch":"main","bogus_field":"x"}' \
"false"

# --- needs_input field ---

run_test "valid-with-needs-input" \
'{"target_branch":"main","needs_input":"scan-secrets helper not found"}' \
"true"

run_test "valid-needs-input-without-target-branch" \
'{"needs_input":"sandbox tooling broken — cannot determine target branch"}' \
"true"

run_test "invalid-needs-input-empty-string" \
'{"target_branch":"main","needs_input":""}' \
"false"

TOO_LONG_INPUT="$(printf 'a%.0s' {1..4001})"
run_test "invalid-needs-input-too-long" \
"{\"target_branch\":\"main\",\"needs_input\":\"${TOO_LONG_INPUT}\"}" \
"false"

# --- Summary ---

echo ""
if [[ ${FAILURES} -gt 0 ]]; then
echo "${FAILURES} test(s) failed"
exit 1
fi
echo "All tests passed"
Loading
Loading