From 11fac4c4b613a64b88535c49d714013c1f7e8432 Mon Sep 17 00:00:00 2001 From: Codevena Date: Fri, 12 Jun 2026 12:37:38 +0100 Subject: [PATCH 01/13] docs: design spec for agy agent migration + integration tests (v0.6.0) --- ...-agy-agent-and-integration-tests-design.md | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-12-agy-agent-and-integration-tests-design.md diff --git a/docs/superpowers/specs/2026-06-12-agy-agent-and-integration-tests-design.md b/docs/superpowers/specs/2026-06-12-agy-agent-and-integration-tests-design.md new file mode 100644 index 0000000..0781e6b --- /dev/null +++ b/docs/superpowers/specs/2026-06-12-agy-agent-and-integration-tests-design.md @@ -0,0 +1,146 @@ +# Design: agy agent (gemini replacement) + integration tests — v0.6.0 + +Date: 2026-06-12. Status: approved scope (user chose: replace gemini entirely; +ship agy support plus deterministic integration tests). + +## Background and research + +Google retires the Gemini CLI on **2026-06-18** for all non-Enterprise users +([transition announcement](https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/)). +The successor is the **Antigravity CLI**, binary `agy` — a closed-source Go +binary installed via `curl -fsSL https://antigravity.google/cli/install.sh | bash` +(no npm package). + +Behavior verified locally against the installed `agy` (2026-06-12): + +| Aspect | gemini (old) | agy (new) | +| --- | --- | --- | +| Non-interactive call | `gemini -p "" --approval-mode --output-format text` | `agy [flags] -p ""` — no `--output-format` flag | +| Read-only mode | `--approval-mode plan` | **none** — `--sandbox` enables "terminal restrictions" but still allows workspace file writes and benign shell commands | +| Workspace | implicit CWD | `--add-dir ` (repeatable) adds directories to the workspace | +| Internal timeout | none | `--print-timeout`, default **5m** (Go duration syntax, e.g. `1260s`) | +| Exit code on its internal timeout | n/a | **0** (!) with `Error: timed out waiting for response` on output | +| Permission prompts in print mode | n/a | workspace reads/writes/shell ran without prompting in tests; `--dangerously-skip-permissions` auto-approves everything | + +Two of these are correctness traps for fixbuddy: + +1. agy's default 5-minute print timeout fires **before** fixbuddy's 20-minute + watchdog, and +2. agy exits **0** on that timeout, so `run_agent`'s crash classifier would see + rc=0 with no `DONE-*` marker → the issue would be labeled `fix:needs-human` + (never retried) instead of `fix:blocked` (auto-requeue) for what is a + transient condition. + +## Decisions + +- **gemini is removed, not deprecated.** `agy` replaces it in the supported + agent set (`claude | codex | opencode | agy`). A user passing `gemini` gets a + targeted error explaining the retirement and pointing at `agy`, instead of a + generic "unsupported agent". +- **v0.6.0** (breaking change for `fix_agent = gemini` configs, documented in + the changelog). + +## Component changes + +### fixbuddy.sh + +- Header comment, agent validation list, and CLI presence check: `gemini` → + `agy`. The validation `case` gets an explicit `gemini)` arm that errors with + a migration message (Gemini CLI retired 2026-06-18 → install Antigravity CLI, + use `agy`). +- The "gemini as fix-agent is experimental" warning block is removed — agy is a + full coding agent and is treated like opencode. +- `run_agent`: + - `gem_mode` (plan/yolo) logic is removed. + - New invocation, flags before the prompt: + + ```bash + agy) + agy_args=(--dangerously-skip-permissions --add-dir "$PROJECT" + --print-timeout "$((AGENT_TIMEOUT+60))s") + case "$stage" in verify|review) agy_args+=(--sandbox) ;; esac + env -u GH_TOKEN -u GITHUB_TOKEN agy "${agy_args[@]}" -p "$prompt" \ + "$outfile" 2>&1 & + ;; + ``` + + `--print-timeout` sits 60s above the watchdog so the watchdog always fires + first and classifies rc=124 correctly. `--sandbox` on verify/review is + defense-in-depth replacing the old plan mode (and is documented honestly as + NOT read-only). `--add-dir "$PROJECT"` is required because fixbuddy launches + agents from the operator's CWD, not from the project. + - Belt-and-braces timeout reclassification after the watchdog marker check, + scoped to agy: rc=0 plus a `^Error: timed out waiting for response` line in + the output → rc=124. + +### fixbuddy-wizard.sh + +- Agent lists (`claude codex opencode gemini` ×3) → `claude codex opencode agy`. +- gemini-specific labels/notes removed; reviewer note for agy mentions the + sandbox. Banner/header bumped to v0.6.0. + +### action.yml / README.md + +- Input descriptions and docs: `claude | codex | opencode | agy`. +- README: badge, "Why fixbuddy" CLI list + comparison row, Supported Agents + table (agy invocation incl. sandbox note), the "Gemini as a read-only + reviewer" example becomes an agy cross-vendor-reviewer example, CI + prerequisites note that agy installs via the curl script (no npm), config + example unchanged keys. A short migration note (gemini → agy) in the + changelog section of the README is NOT needed; CHANGELOG.md carries it. + +### Integration tests (new) + +`tests/integration.sh` — pure Bash, zero new dependencies, deterministic and +offline. Reuses the proven approach of `docs/demo/bin/{agent,gh}`: + +- **Fixture**: a temp dir holding a bare repo (`origin.git`) plus a working + clone as `$PROJECT`, so branch creation, commits, and `git push` are real. +- **Stub `gh`**: first on `PATH`; serves canned JSON for `issue list/view`, + `pr list/view`, `repo view`; records every mutating call (`label create`, + `issue edit/comment/close`, `pr create/merge`) to a mutation log the + assertions read. +- **Stub agents** (`claude`, `codex`, `agy` — one script, behavior switched on + `$0` and scenario env var): parse the stage from the prompt (verify / fix / + review), emit the scenario's `DONE-*` markers, and for the fix stage create a + real commit in `$PROJECT`. The agy stub also asserts it received the expected + agy-specific flags (`--add-dir`, `--print-timeout`, sandbox on verify/review). +- **Scenarios**: + 1. happy path → PR created, auto-merge requested, `fix:pr-open` label + 2. false positive → issue closed + `fix:false-positive` + 3. review rejected (all attempts) → `fix:rejected`, branch cleaned up + 4. `--check-cmd` failure → treated as rejection, feedback fed back + 5. `--dry-run` → zero mutations recorded + 6. agent crash (rc=1, no marker) → `fix:blocked` (auto-requeue path) +- **CI**: new `integration` job in `.github/workflows/ci.yml` running the + suite on ubuntu-latest. + +### Release housekeeping + +- Version bumps: `fixbuddy.sh` (`VERSION` + header), wizard (header + banner), + `install.sh` `DEFAULT_REF="v0.6.0"`, README quick-start one-liner. +- Regenerate `SHA256SUMS`. +- `CHANGELOG.md`: v0.6.0 entry — Added (agy agent, integration tests), + Removed/**Breaking** (gemini agent; migration note for configs), Changed + (docs). +- `findings.md` → `docs/audit/2026-06-10-findings.md`; `NEXT_SESSION.md` + updated to the post-v0.6.0 state. +- Tagging (`v0.6.0`, floating `v1`) and pushing happen only after explicit user + approval, per workflow rules. + +## Error handling + +- agy missing at runtime → existing presence-check error path (exit 2). +- agy internal timeout → reclassified rc=124 → `fix:blocked` + auto-requeue + (same semantics as a watchdog kill). +- `gemini` passed via flag or config → exit 2 with migration message (config + files share the same validation, so stale configs fail fast and clearly). + +## Testing + +- `bash -n` + `shellcheck` (plain severity, mirroring CI) for both scripts and + the new test files. +- `tests/integration.sh` green locally and in CI. +- One real smoke check of the agy invocation shape against the installed CLI + (already performed during research; re-verified after implementation). +- Full Definition-of-Done review pipeline (Codex ×2, Claude ×2) before commit. From 3537458219e2751b71667f834454aa7f9b037ce7 Mon Sep 17 00:00:00 2001 From: Codevena Date: Fri, 12 Jun 2026 12:44:15 +0100 Subject: [PATCH 02/13] docs: implementation plan for agy migration + integration tests --- ...6-06-12-agy-agent-and-integration-tests.md | 751 ++++++++++++++++++ 1 file changed, 751 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-12-agy-agent-and-integration-tests.md diff --git a/docs/superpowers/plans/2026-06-12-agy-agent-and-integration-tests.md b/docs/superpowers/plans/2026-06-12-agy-agent-and-integration-tests.md new file mode 100644 index 0000000..3e1e4d6 --- /dev/null +++ b/docs/superpowers/plans/2026-06-12-agy-agent-and-integration-tests.md @@ -0,0 +1,751 @@ +# agy Agent + Integration Tests (v0.6.0) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the retired `gemini` agent with `agy` (Antigravity CLI) and add a deterministic, offline integration test suite, releasing as v0.6.0. + +**Architecture:** A pure-Bash test harness (`tests/integration.sh`) runs `fixbuddy.sh` against stub CLIs (`gh`, `claude`, `codex`, `agy`) on a prefixed `PATH` and a local bare git repo as `origin`, so branches/commits/pushes are real while GitHub and agents are canned. The harness lands first (TDD), then the agy swap is driven by failing tests. + +**Tech Stack:** Bash 3.2-compatible shell (matches existing scripts), git, jq, shellcheck. No new dependencies. + +Spec: `docs/superpowers/specs/2026-06-12-agy-agent-and-integration-tests-design.md` + +--- + +### Task 1: Test harness, stubs, happy-path scenario + +**Files:** +- Create: `tests/stubs/agent` (+ symlinks `tests/stubs/claude`, `tests/stubs/codex`, `tests/stubs/agy`) +- Create: `tests/stubs/gh` +- Create: `tests/integration.sh` + +- [ ] **Step 1: Create the agent stub** + +Write `tests/stubs/agent`: + +```bash +#!/usr/bin/env bash +# Deterministic test double for the agent CLIs, symlinked as claude/codex/agy. +# Behavior switches on the invoked name ($0) and $FIXBUDDY_TEST_SCENARIO. The +# pipeline stage is detected from the prompt text (mirrors docs/demo/bin/agent). +# Every invocation appends ":" to $FIXBUDDY_TEST_STAGELOG; agy +# invocations additionally record their argv and GH_TOKEN visibility to +# $FIXBUDDY_TEST_AGYLOG so tests can assert on flags and env stripping. +set -u + +name="$(basename "$0")" +scenario="${FIXBUDDY_TEST_SCENARIO:-happy}" + +# claude/codex receive the prompt on stdin; agy receives it as the value after +# -p, with all other flags positioned before it. +prompt="" +flags="" +if [ "$name" = "agy" ]; then + while [ $# -gt 0 ]; do + case "$1" in + -p) prompt="${2:-}"; shift 2 ;; + --add-dir|--print-timeout) flags="$flags $1=${2:-}"; shift 2 ;; + *) flags="$flags $1"; shift ;; + esac + done +else + prompt="$(cat)" +fi + +stage="verify" +case "$prompt" in + *"implementing a fix"*) stage="fix" ;; + *"independent senior code reviewer"*) stage="review" ;; +esac + +[ -n "${FIXBUDDY_TEST_STAGELOG:-}" ] && echo "$name:$stage" >> "$FIXBUDDY_TEST_STAGELOG" +if [ "$name" = "agy" ] && [ -n "${FIXBUDDY_TEST_AGYLOG:-}" ]; then + echo "stage=$stage gh_token=${GH_TOKEN:-unset} args=$flags" >> "$FIXBUDDY_TEST_AGYLOG" +fi + +# The fix stage makes a REAL commit in the target project, parsed from the prompt. +project="$(printf '%s\n' "$prompt" | sed -n 's/^\*\*Working directory:\*\* //p' | head -1)" +do_fix_commit() { + ( cd "$project" \ + && echo "fixed by $name" >> src/app.txt \ + && git add src/app.txt \ + && git commit -q -m "fix: correct app output + +Closes #7" ) +} + +case "$scenario:$stage" in + happy:verify|reject:verify|check:verify) + echo "Reproduced the problem. The issue is real." + echo "DONE-PROCEED" ;; + happy:fix|reject:fix|check:fix) + do_fix_commit + echo "DONE-FIX-APPLIED" ;; + happy:review) + echo "Diff is correct, minimal, in scope." + echo "DONE-APPROVED" ;; + reject:review) + echo "DONE-REJECTED: the fix lacks a regression test" ;; + falsepos:verify) + echo "DONE-FALSE-POSITIVE: the code already behaves correctly" ;; + crash:verify) + echo "transport error: connection reset" + exit 1 ;; + agytimeout:verify) + echo "Error: timed out waiting for response" + exit 0 ;; + *) + echo "DONE-BLOCKED: unexpected scenario '$scenario' at stage '$stage'" + exit 0 ;; +esac +``` + +- [ ] **Step 2: Create the gh stub** + +Write `tests/stubs/gh`: + +```bash +#!/usr/bin/env bash +# Deterministic test double for the GitHub CLI. Read-only queries return canned +# JSON; every MUTATING call is appended verbatim to $FIXBUDDY_TEST_MUTLOG so +# tests can assert exactly what fixbuddy would have changed on GitHub. +set -u + +mutate() { + [ -n "${FIXBUDDY_TEST_MUTLOG:-}" ] && echo "$*" >> "$FIXBUDDY_TEST_MUTLOG" + return 0 +} + +case "${1:-} ${2:-}" in + "label create") mutate "$@" ;; + "issue list") + case "$*" in + *"fix:pr-open"*) echo "[]" ;; # unstick scan: nothing stuck + *) cat <<'JSON' +[{"number":7,"title":"app outputs wrong text","labels":[{"name":"bug"},{"name":"severity:high"}],"url":"https://github.com/acme/app/issues/7","body":"src/app.txt should contain a fixed line. Please fix the output."}] +JSON + ;; + esac ;; + "issue view") + cat <<'JSON' +{"number":7,"title":"app outputs wrong text","labels":[{"name":"bug"},{"name":"severity:high"}],"url":"https://github.com/acme/app/issues/7","state":"OPEN","body":"src/app.txt should contain a fixed line. Please fix the output."} +JSON + ;; + "issue edit"|"issue comment"|"issue close") mutate "$@" ;; + "pr create") mutate "$@"; echo "https://github.com/acme/app/pull/12" ;; + "pr merge") mutate "$@" ;; + "pr view") echo "false" ;; # not merged yet -> fix:pr-open path + "pr list") echo "" ;; + "repo view") echo "main" ;; + *) : ;; # auth setup-git etc. -> no-op +esac +exit 0 +``` + +- [ ] **Step 3: Create the runner with the happy-path test** + +Write `tests/integration.sh`: + +```bash +#!/usr/bin/env bash +# Deterministic integration tests for fixbuddy.sh. No network, no real gh, no +# real agents: PATH is prefixed with tests/stubs (canned gh + scripted agent +# doubles) and the GitHub remote is a local bare repository, so branch +# creation, commits, and pushes are real git operations. Each scenario runs in +# a fresh mktemp fixture with HOME redirected (no ~/.fixbuddy leakage). +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +STUBS="$ROOT/tests/stubs" +PASS=0 +FAIL=0 +CURRENT="" + +fail() { FAIL=$((FAIL+1)); printf 'FAIL %s: %s\n' "$CURRENT" "$*"; } + +assert_grep() { grep -qE -- "$2" "$1" || fail "expected /$2/ in ${1##*/}"; } +assert_no_grep() { if grep -qE -- "$2" "$1"; then fail "did not expect /$2/ in ${1##*/}"; fi; } +assert_substr() { grep -qF -- "$2" "$1" || fail "expected '$2' in ${1##*/}"; } + +make_fixture() { + TMP="$(mktemp -d "${TMPDIR:-/tmp}/fixbuddy-itest.XXXXXX")" + mkdir -p "$TMP/home" + git init -q --bare "$TMP/origin.git" + git clone -q "$TMP/origin.git" "$TMP/project" 2>/dev/null + ( + cd "$TMP/project" + git config user.email "test@example.com" + git config user.name "fixbuddy-itest" + mkdir -p src + echo "hello" > src/app.txt + git add . + git commit -q -m "initial commit" + git branch -M main + git push -q -u origin main 2>/dev/null + git remote set-head origin main + ) + MUTLOG="$TMP/mutations.log"; : > "$MUTLOG" + STAGELOG="$TMP/stages.log"; : > "$STAGELOG" + AGYLOG="$TMP/agy.log"; : > "$AGYLOG" + RUNLOG="$TMP/run.log" +} + +run_fixbuddy() { + # GH_TOKEN is set on purpose: the agent stub records whether fixbuddy + # stripped it from the agent environment. HOME is redirected so the user's + # ~/.fixbuddy/config can never leak in and run logs never pollute the real + # home directory. + ( cd "$TMP" && \ + HOME="$TMP/home" \ + PATH="$STUBS:$PATH" \ + GH_TOKEN="test-token-must-not-leak" \ + FIXBUDDY_TEST_SCENARIO="$SCENARIO" \ + FIXBUDDY_TEST_MUTLOG="$MUTLOG" \ + FIXBUDDY_TEST_STAGELOG="$STAGELOG" \ + FIXBUDDY_TEST_AGYLOG="$AGYLOG" \ + bash "$ROOT/fixbuddy.sh" --repo acme/app --project "$TMP/project" --yes "$@" \ + ) > "$RUNLOG" 2>&1 + RC=$? +} + +# ---------------- Scenarios ---------------- + +test_happy_path() { + SCENARIO=happy; make_fixture + run_fixbuddy + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_grep "$STAGELOG" '^claude:verify$' + assert_grep "$STAGELOG" '^claude:fix$' + assert_grep "$STAGELOG" '^codex:review$' + assert_grep "$MUTLOG" '^pr create .*--head fix/issue-7' + assert_grep "$MUTLOG" '^pr merge .*--auto' + assert_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:pr-open' + # the push was real: the fix branch must exist in the bare origin + git -C "$TMP/origin.git" show-ref --verify --quiet refs/heads/fix/issue-7 \ + || fail "fix branch was not pushed to origin" + # local worktree restored: back on base, fix branch deleted + [ -z "$(git -C "$TMP/project" branch --list 'fix/issue-7')" ] \ + || fail "local fix branch not cleaned up" +} + +# ---------------- Runner ---------------- + +TESTS=(test_happy_path) + +for t in "${TESTS[@]}"; do + CURRENT="$t" + FAIL_BEFORE=$FAIL + "$t" + if [ "$FAIL" -eq "$FAIL_BEFORE" ]; then + PASS=$((PASS+1)); printf 'ok %s\n' "$t" + else + printf ' run log tail:\n'; tail -5 "$RUNLOG" 2>/dev/null | sed 's/^/ | /' + fi + rm -rf "$TMP" +done + +printf '\n%d passed, %d failed\n' "$PASS" "$FAIL" +[ "$FAIL" -eq 0 ] +``` + +- [ ] **Step 4: Make executable, create symlinks** + +```bash +chmod +x tests/integration.sh tests/stubs/agent tests/stubs/gh +ln -s agent tests/stubs/claude +ln -s agent tests/stubs/codex +ln -s agent tests/stubs/agy +``` + +- [ ] **Step 5: Run the suite** + +Run: `tests/integration.sh` +Expected: `ok test_happy_path` … `1 passed, 0 failed` (exit 0). Debug via the printed run-log tail on failure. + +- [ ] **Step 6: Static checks** + +Run: `bash -n tests/integration.sh tests/stubs/agent tests/stubs/gh && shellcheck tests/integration.sh tests/stubs/agent tests/stubs/gh` +Expected: no output, exit 0. + +- [ ] **Step 7: Commit** + +```bash +git add tests/ +git commit -m "test: deterministic integration harness with stubbed gh/agent CLIs" +``` + +--- + +### Task 2: Remaining non-agy scenarios + +**Files:** +- Modify: `tests/integration.sh` (append test functions before the Runner section; extend `TESTS`) + +- [ ] **Step 1: Add five test functions** + +Insert before `# ---------------- Runner ----------------`: + +```bash +test_false_positive() { + SCENARIO=falsepos; make_fixture + run_fixbuddy + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:false-positive' + assert_grep "$MUTLOG" '^issue close 7' + assert_no_grep "$STAGELOG" ':fix$' +} + +test_review_reject() { + SCENARIO=reject; make_fixture + run_fixbuddy + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:rejected' + [ "$(grep -c '^claude:fix$' "$STAGELOG")" -eq 2 ] || fail "expected 2 fix attempts" + [ "$(grep -c '^codex:review$' "$STAGELOG")" -eq 2 ] || fail "expected 2 review attempts" + assert_no_grep "$MUTLOG" '^pr create' + [ -z "$(git -C "$TMP/project" branch --list 'fix/issue-7')" ] \ + || fail "local fix branch not cleaned up" +} + +test_check_gate() { + SCENARIO=check; make_fixture + run_fixbuddy --check-cmd 'false' + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:rejected' + assert_no_grep "$STAGELOG" ':review$' + assert_no_grep "$MUTLOG" '^pr create' +} + +test_dry_run_read_only() { + SCENARIO=happy; make_fixture + run_fixbuddy --dry-run + [ "$RC" -eq 0 ] || fail "exit code $RC" + [ ! -s "$MUTLOG" ] || fail "dry-run made mutations: $(tr '\n' ';' < "$MUTLOG")" + [ ! -s "$STAGELOG" ] || fail "dry-run invoked an agent" + assert_grep "$RUNLOG" '#7' +} + +test_crash_labels_blocked() { + SCENARIO=crash; make_fixture + run_fixbuddy + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:blocked' + assert_no_grep "$MUTLOG" 'fix:needs-human' +} +``` + +- [ ] **Step 2: Extend the test list** + +```bash +TESTS=(test_happy_path test_false_positive test_review_reject test_check_gate + test_dry_run_read_only test_crash_labels_blocked) +``` + +- [ ] **Step 3: Run the suite** + +Run: `tests/integration.sh` +Expected: `6 passed, 0 failed`, exit 0. + +- [ ] **Step 4: Commit** + +```bash +git add tests/integration.sh +git commit -m "test: cover false-positive, reject, check gate, dry-run, crash paths" +``` + +--- + +### Task 3: agy agent in fixbuddy.sh (TDD) + +**Files:** +- Modify: `tests/integration.sh` +- Modify: `fixbuddy.sh:26-29` (header), `:190-214` (validation/presence/warning), `:464-484` (run_agent), `:515-517` (timeout classification) + +- [ ] **Step 1: Add three failing tests** + +Insert before the Runner section of `tests/integration.sh`: + +```bash +test_agy_full_pipeline() { + SCENARIO=happy; make_fixture + run_fixbuddy --fix-agent agy --review-agent agy + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:pr-open' + # agy invocation contract: workspace dir, print-timeout above the watchdog + # (default 1200+60), sandbox on verify/review but NOT on fix, GH_TOKEN stripped + assert_substr "$AGYLOG" "--add-dir=$TMP/project" + assert_substr "$AGYLOG" "--print-timeout=1260s" + assert_grep "$AGYLOG" '^stage=verify .*--sandbox' + assert_grep "$AGYLOG" '^stage=review .*--sandbox' + assert_no_grep "$AGYLOG" '^stage=fix .*--sandbox' + assert_grep "$AGYLOG" 'gh_token=unset' +} + +test_gemini_rejected_with_migration_hint() { + SCENARIO=happy; make_fixture + run_fixbuddy --fix-agent gemini + [ "$RC" -eq 2 ] || fail "expected exit 2, got $RC" + assert_grep "$RUNLOG" "agy" + assert_grep "$RUNLOG" "[Gg]emini CLI" +} + +test_agy_internal_timeout_is_blocked() { + # agy exits 0 on its own --print-timeout with an error line instead of a + # DONE marker; fixbuddy must classify that as a crash/timeout (fix:blocked, + # auto-requeue) — not as the never-retried fix:needs-human path. + SCENARIO=agytimeout; make_fixture + run_fixbuddy --fix-agent agy --review-agent agy + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:blocked' + assert_no_grep "$MUTLOG" 'fix:needs-human' +} +``` + +Extend the list: + +```bash +TESTS=(test_happy_path test_false_positive test_review_reject test_check_gate + test_dry_run_read_only test_crash_labels_blocked + test_agy_full_pipeline test_gemini_rejected_with_migration_hint + test_agy_internal_timeout_is_blocked) +``` + +- [ ] **Step 2: Run to verify the new tests fail** + +Run: `tests/integration.sh` +Expected: 6 passed, 3 failed — `test_agy_full_pipeline` and `test_agy_internal_timeout_is_blocked` fail with "unsupported agent: agy"; `test_gemini_rejected_with_migration_hint` fails because gemini is currently accepted (no exit 2 / no hint). + +- [ ] **Step 3: fixbuddy.sh — header comment** + +Replace lines 26-29 (keep the line count stable — `--help` prints `sed -n '2,47p'`): + +```text +# --fix-agent claude | codex | opencode | agy (default: claude) +# --review-agent claude | codex | opencode | agy (default: codex — cross-agent) +# Note: agy (Antigravity CLI) runs verify/review with +# --sandbox (terminal restrictions) as defense in depth. +``` + +- [ ] **Step 4: fixbuddy.sh — agent validation with migration error** + +Replace the validation loop (lines 190-195): + +```bash +for agent in "$FIX_AGENT" "$REVIEW_AGENT"; do + case "$agent" in + claude|codex|opencode|agy) ;; + gemini) + err "agent 'gemini' is no longer supported: Google retired the Gemini CLI on 2026-06-18." + err "Install the Antigravity CLI (curl -fsSL https://antigravity.google/cli/install.sh | bash)" + err "and use 'agy' instead — also in fix_agent/review_agent config keys." + exit 2 ;; + *) err "unsupported agent: $agent (valid: claude, codex, opencode, agy)"; exit 2 ;; + esac +done +``` + +- [ ] **Step 5: fixbuddy.sh — presence check + drop gemini warning** + +In the presence-check loop, replace the gemini line: + +```bash + agy) command -v agy >/dev/null || { err "agy CLI not found"; exit 2; } ;; +``` + +Delete the entire "Gemini is less reliable…" warning block (the comment and the `if [ "$FIX_AGENT" = "gemini" ] … fi`, lines 207-213) — agy is a full coding agent, treated like opencode. Keep the enclosing `if ! $DRY_RUN; then … fi` intact. + +- [ ] **Step 6: fixbuddy.sh — run_agent invocation** + +Replace the `gem_mode` comment+assignment (lines 464-468) with: + +```bash + # agy (Antigravity CLI) has no read-only mode; verify/review add --sandbox + # (terminal restrictions) as defense in depth. --add-dir grants workspace access + # to the project (agents are launched from the operator's CWD, not $PROJECT). + # --print-timeout sits 60s ABOVE the fixbuddy watchdog so the watchdog always + # fires first and the timeout is classified rc=124 (fix:blocked, auto-requeue). + local agy_args=(--dangerously-skip-permissions --add-dir "$PROJECT" --print-timeout "$((AGENT_TIMEOUT+60))s") + case "$stage" in verify|review) agy_args+=(--sandbox) ;; esac +``` + +Replace the `gemini)` case arm in the agent launch: + +```bash + agy) + env -u GH_TOKEN -u GITHUB_TOKEN agy "${agy_args[@]}" -p "$prompt" "$outfile" 2>&1 & + ;; +``` + +- [ ] **Step 7: fixbuddy.sh — reclassify agy's silent timeout** + +Directly after the watchdog-marker check (`if grep -q "^\[fixbuddy-watchdog\]" …; then rc=124; fi`), insert: + +```bash + # agy exits 0 (!) when its own --print-timeout fires, printing this line + # instead of a DONE marker. Reclassify as timeout so the issue is labeled + # fix:blocked (auto-requeue) rather than the never-retried fix:needs-human. + # Normally unreachable (our --print-timeout sits above the watchdog) — belt + # and braces. + if [ "$agent" = "agy" ] && [ "$rc" -eq 0 ] \ + && ! grep -qE '^DONE-' "$outfile" 2>/dev/null \ + && grep -q '^Error: timed out waiting for response' "$outfile" 2>/dev/null; then + rc=124 + fi +``` + +- [ ] **Step 8: Run the suite to verify all pass** + +Run: `bash -n fixbuddy.sh && tests/integration.sh` +Expected: `9 passed, 0 failed`, exit 0. + +- [ ] **Step 9: Static checks + real-CLI smoke check** + +Run: `shellcheck fixbuddy.sh && ./fixbuddy.sh --help | grep -- 'agy'` +Expected: shellcheck clean; help text shows the agy agent line. + +- [ ] **Step 10: Commit** + +```bash +git add fixbuddy.sh tests/integration.sh +git commit -m "feat: replace retired gemini agent with agy (Antigravity CLI)" +``` + +--- + +### Task 4: Wizard update + +**Files:** +- Modify: `fixbuddy-wizard.sh:56-68` (prereq scan), `:195-208` (fixer menu), `:219-233` (reviewer menu) + +- [ ] **Step 1: Prerequisite scan** + +Line 57: `for a in claude codex opencode gemini; do` → `for a in claude codex opencode agy; do` +Line 66: `fail "no agent CLI installed — need at least one of: claude, codex, opencode, gemini"` → `… claude, codex, opencode, agy"` + +- [ ] **Step 2: Fixer menu (step 7a)** + +Replace the two notes (lines 196-197) with one: + +```bash +note "claude is the most reliable fixer; codex, opencode, and agy are strong alternatives." +``` + +In the menu loop (line 200): `for a in claude codex opencode gemini; do` → `for a in claude codex opencode agy; do`, and delete the gemini label line: + +```bash + [ "$a" = "gemini" ] && label="$a ${DIM}(experimental — often writes incomplete fixes)${RST}" +``` + +- [ ] **Step 3: Reviewer menu (step 7b)** + +Line 221: `note "gemini in review mode runs read-only — safer but less thorough."` → + +```bash +note "agy runs verify/review with a sandbox (terminal restrictions)." +``` + +Line 224: `for a in codex claude opencode gemini; do` → `for a in codex claude opencode agy; do` +Line 228: `[ "$a" = "gemini" ] && label="$a ${DIM}(read-only, experimental — quick second opinion)${RST}"` → + +```bash + [ "$a" = "agy" ] && label="$a ${DIM}(sandboxed verify/review)${RST}" +``` + +- [ ] **Step 4: Verify** + +Run: `bash -n fixbuddy-wizard.sh && shellcheck fixbuddy-wizard.sh && grep -c gemini fixbuddy-wizard.sh` +Expected: syntax/shellcheck clean; grep prints `0` (exit 1 from grep -c is fine). + +- [ ] **Step 5: Commit** + +```bash +git add fixbuddy-wizard.sh +git commit -m "feat(wizard): offer agy instead of retired gemini" +``` + +--- + +### Task 5: action.yml + README + +**Files:** +- Modify: `action.yml:18,22` +- Modify: `README.md` (badge, comparison, agents table, options, examples, CI notes, roadmap) + +- [ ] **Step 1: action.yml input descriptions** + +Both `fix-agent` and `review-agent` descriptions: `claude | codex | opencode | gemini` → `claude | codex | opencode | agy`. + +- [ ] **Step 2: README — all gemini references** + +1. Badge (line 9): `agents-claude%20%7C%20codex%20%7C%20opencode%20%7C%20gemini` → `…%7C%20agy`. +2. "Why fixbuddy" (line 25): `(claude, codex, opencode, gemini)` → `(claude, codex, opencode, agy)`. +3. Comparison table row: `claude · codex · opencode · gemini` → `claude · codex · opencode · agy`. +4. Supported Agents table — replace the gemini row with: + +```markdown +| `agy` | `agy --dangerously-skip-permissions --add-dir -p ...` | Antigravity CLI (Gemini's successor). Verify/review add `--sandbox` (terminal restrictions — not read-only). | +``` + +5. Options table: `--fix-agent` / `--review-agent` descriptions `claude, codex, opencode, or gemini` → `…or agy` (both rows). +6. Replace the example "Use Gemini as a read-only reviewer" with: + +```markdown +Use agy (Antigravity CLI) as a cross-vendor reviewer: + +​```bash +./fixbuddy.sh --repo owner/repo --project ~/code/repo \ + --fix-agent claude --review-agent agy +​``` +``` + +7. CI prerequisites: agent list `(claude, codex, opencode, gemini)` → `(claude, codex, opencode, agy)`; add after the pinning sentence: `agy has no npm package — install it with the vendor script: curl -fsSL https://antigravity.google/cli/install.sh | bash`. +8. Roadmap: drop "More deterministic integration tests with mocked CLIs" (now shipped); keep the other two items. + +- [ ] **Step 3: Verify no stale references** + +Run: `grep -rn gemini README.md action.yml` +Expected: no matches (exit 1). + +- [ ] **Step 4: Commit** + +```bash +git add README.md action.yml +git commit -m "docs: document agy agent, remove gemini from README and action" +``` + +--- + +### Task 6: CI — integration job + shellcheck coverage + +**Files:** +- Modify: `.github/workflows/ci.yml` + +- [ ] **Step 1: Extend shell checks to the new files** + +In the `Bash syntax` step add the test files; in `ShellCheck` likewise: + +```yaml + - name: Bash syntax + run: | + bash -n fixbuddy.sh + bash -n fixbuddy-wizard.sh + bash -n tests/integration.sh tests/stubs/agent tests/stubs/gh + + - name: ShellCheck + run: shellcheck fixbuddy.sh fixbuddy-wizard.sh tests/integration.sh tests/stubs/agent tests/stubs/gh +``` + +- [ ] **Step 2: Add the integration job** + +```yaml + integration: + name: Integration tests (mocked CLIs) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Run integration tests + run: tests/integration.sh +``` + +- [ ] **Step 3: Verify YAML + suite locally** + +Run: `bash -n fixbuddy.sh && tests/integration.sh && python3 -c "import yaml,sys; yaml.safe_load(open('.github/workflows/ci.yml'))"` +Expected: 9 passed; no YAML error. + +- [ ] **Step 4: Commit** + +```bash +git add .github/workflows/ci.yml +git commit -m "ci: run integration tests, lint test scripts" +``` + +--- + +### Task 7: Version bumps, changelog, housekeeping + +**Files:** +- Modify: `fixbuddy.sh:2,50`, `fixbuddy-wizard.sh:2,35`, `install.sh:5,10,19,43,47`, `README.md:60,68`, `CHANGELOG.md`, `NEXT_SESSION.md`, `SHA256SUMS` +- Move: `findings.md` → `docs/audit/2026-06-10-findings.md` + +- [ ] **Step 1: Version bumps (v0.5.0 → v0.6.0)** + +- `fixbuddy.sh` line 2 header and line 50 `VERSION="0.6.0"` +- `fixbuddy-wizard.sh` line 2 header and line 35 banner +- `install.sh` lines 5, 10, 19 (`DEFAULT_REF="v0.6.0"`), 43, 47 +- `README.md` quick-start one-liner (line 60) and inspect-first snippet (line 68) + +- [ ] **Step 2: CHANGELOG entry** + +Insert after the intro block of `CHANGELOG.md`: + +```markdown +## [0.6.0] - 2026-06-12 + +Google retires the Gemini CLI on 2026-06-18; its successor is the Antigravity +CLI (`agy`). fixbuddy v0.6.0 swaps the agent and gains an offline integration +test suite. + +### Added +- **`agy` agent** (Antigravity CLI) as fix or review agent. Invocation details + that matter: `--add-dir ` (agents launch from the operator's CWD), + `--print-timeout` pinned 60s above `--agent-timeout` so fixbuddy's watchdog + classifies timeouts (agy itself exits 0 on its internal timeout — fixbuddy + also detects that output and treats it as `fix:blocked`/auto-requeue), and + `--sandbox` on verify/review as defense in depth (agy has no read-only mode). +- **Integration tests** (`tests/integration.sh`) — deterministic, offline, + zero new dependencies: stub `gh`/agent CLIs plus a local bare repo as + `origin`, covering happy path, false positive, review rejection, check gate, + dry-run read-only, and crash classification. Run in CI. + +### Removed (breaking) +- **`gemini` agent.** Passing `gemini` (flag or config) now exits with a + migration message. Replace `fix_agent`/`review_agent` values with `agy`. + Note: agy in verify/review runs sandboxed but NOT read-only — the old + `--approval-mode plan` has no equivalent in the Antigravity CLI. + +[0.6.0]: https://github.com/Codevena/fixbuddy/compare/v0.5.0...v0.6.0 +``` + +(Also update the bottom link block accordingly.) + +- [ ] **Step 3: Housekeeping** + +```bash +mkdir -p docs/audit +git mv findings.md docs/audit/2026-06-10-findings.md +``` + +Rewrite `NEXT_SESSION.md`: status = v0.6.0 ready on branch, agy swap + integration tests done; release checklist (regenerate SHA256SUMS on script changes, tag v0.6.0, move floating v1) and remaining roadmap (notifications, resume mode). + +- [ ] **Step 4: Regenerate SHA256SUMS** + +Run: `shasum -a 256 fixbuddy.sh fixbuddy-wizard.sh > SHA256SUMS && cat SHA256SUMS` +Expected: two hash lines. + +- [ ] **Step 5: Full verification** + +Run: `bash -n fixbuddy.sh fixbuddy-wizard.sh install.sh && shellcheck fixbuddy.sh fixbuddy-wizard.sh install.sh tests/integration.sh tests/stubs/agent tests/stubs/gh && tests/integration.sh` +Expected: all clean, 9 passed. + +- [ ] **Step 6: Commit** + +```bash +git add -A +git commit -m "release: v0.6.0 (version bumps, changelog, SHA256SUMS, housekeeping)" +``` + +--- + +### Task 8: Definition-of-Done review pipeline + +Per `~/.claude/CLAUDE.md`. The work is committed incrementally, so reviewers +review the **branch diff** (`git diff main...HEAD`), not uncommitted changes. + +- [ ] **Step 1: Static checks** — `bash -n` ×3, `shellcheck` (plain severity, mirrors CI), `tests/integration.sh`: all green. +- [ ] **Step 2: Codex availability** — `codex exec "Hello"` responds <10s; if it hangs at 0% CPU, fall back to `agy -p "$(<.review/codex-prompt.txt)" --dangerously-skip-permissions --add-dir .`. +- [ ] **Step 3: Codex review (Agent A)** — prompt via `.review/codex-prompt.txt` (file-based, foreground), reviewing `git diff main...HEAD` for quality/correctness/security/consistency; findings to `.review/codex-a-findings.md`, FINDINGS/VERDICT format. +- [ ] **Step 4: Claude review (Agent A)** — spawn claude agent reviewing the same branch diff; findings to `.review/claude-a-findings.md`. +- [ ] **Step 5: Gate** — both VERDICT: PASS (zero CRITICAL/WARN). On FAIL: fix all findings, re-run all reviewers from scratch. +- [ ] **Step 6: `rm -rf .review/`** then final commit if fixes were made. Stop — ask the user before pushing or tagging. From 8b6ea739b1bfdc3ffa38649ecd3dd221dc7cf88a Mon Sep 17 00:00:00 2001 From: Codevena Date: Fri, 12 Jun 2026 12:45:48 +0100 Subject: [PATCH 03/13] test: deterministic integration harness with stubbed gh/agent CLIs --- tests/integration.sh | 99 ++++++++++++++++++++++++++++++++++++++++++++ tests/stubs/agent | 74 +++++++++++++++++++++++++++++++++ tests/stubs/agy | 1 + tests/stubs/claude | 1 + tests/stubs/codex | 1 + tests/stubs/gh | 35 ++++++++++++++++ 6 files changed, 211 insertions(+) create mode 100755 tests/integration.sh create mode 100755 tests/stubs/agent create mode 120000 tests/stubs/agy create mode 120000 tests/stubs/claude create mode 120000 tests/stubs/codex create mode 100755 tests/stubs/gh diff --git a/tests/integration.sh b/tests/integration.sh new file mode 100755 index 0000000..3aaf2fa --- /dev/null +++ b/tests/integration.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# Deterministic integration tests for fixbuddy.sh. No network, no real gh, no +# real agents: PATH is prefixed with tests/stubs (canned gh + scripted agent +# doubles) and the GitHub remote is a local bare repository, so branch +# creation, commits, and pushes are real git operations. Each scenario runs in +# a fresh mktemp fixture with HOME redirected (no ~/.fixbuddy leakage). +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +STUBS="$ROOT/tests/stubs" +PASS=0 +FAIL=0 +CURRENT="" + +fail() { FAIL=$((FAIL+1)); printf 'FAIL %s: %s\n' "$CURRENT" "$*"; } + +assert_grep() { grep -qE -- "$2" "$1" || fail "expected /$2/ in ${1##*/}"; } +assert_no_grep() { if grep -qE -- "$2" "$1"; then fail "did not expect /$2/ in ${1##*/}"; fi; } +assert_substr() { grep -qF -- "$2" "$1" || fail "expected '$2' in ${1##*/}"; } + +make_fixture() { + TMP="$(mktemp -d "${TMPDIR:-/tmp}/fixbuddy-itest.XXXXXX")" + mkdir -p "$TMP/home" + git init -q --bare "$TMP/origin.git" + git clone -q "$TMP/origin.git" "$TMP/project" 2>/dev/null + ( + cd "$TMP/project" || exit 1 + git config user.email "test@example.com" + git config user.name "fixbuddy-itest" + mkdir -p src + echo "hello" > src/app.txt + git add . + git commit -q -m "initial commit" + git branch -M main + git push -q -u origin main 2>/dev/null + git remote set-head origin main + ) + MUTLOG="$TMP/mutations.log"; : > "$MUTLOG" + STAGELOG="$TMP/stages.log"; : > "$STAGELOG" + AGYLOG="$TMP/agy.log"; : > "$AGYLOG" + RUNLOG="$TMP/run.log" +} + +run_fixbuddy() { + # GH_TOKEN is set on purpose: the agent stub records whether fixbuddy + # stripped it from the agent environment. HOME is redirected so the user's + # ~/.fixbuddy/config can never leak in and run logs never pollute the real + # home directory. + ( cd "$TMP" && \ + HOME="$TMP/home" \ + PATH="$STUBS:$PATH" \ + GH_TOKEN="test-token-must-not-leak" \ + FIXBUDDY_TEST_SCENARIO="$SCENARIO" \ + FIXBUDDY_TEST_MUTLOG="$MUTLOG" \ + FIXBUDDY_TEST_STAGELOG="$STAGELOG" \ + FIXBUDDY_TEST_AGYLOG="$AGYLOG" \ + bash "$ROOT/fixbuddy.sh" --repo acme/app --project "$TMP/project" --yes "$@" \ + ) > "$RUNLOG" 2>&1 + RC=$? +} + +# ---------------- Scenarios ---------------- + +test_happy_path() { + SCENARIO=happy; make_fixture + run_fixbuddy --auto-merge + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_grep "$STAGELOG" '^claude:verify$' + assert_grep "$STAGELOG" '^claude:fix$' + assert_grep "$STAGELOG" '^codex:review$' + assert_grep "$MUTLOG" '^pr create .*--head fix/issue-7' + assert_grep "$MUTLOG" '^pr merge .*--auto' + assert_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:pr-open' + # the push was real: the fix branch must exist in the bare origin + git -C "$TMP/origin.git" show-ref --verify --quiet refs/heads/fix/issue-7 \ + || fail "fix branch was not pushed to origin" + # local worktree restored: back on base, fix branch deleted + [ -z "$(git -C "$TMP/project" branch --list 'fix/issue-7')" ] \ + || fail "local fix branch not cleaned up" +} + +# ---------------- Runner ---------------- + +TESTS=(test_happy_path) + +for t in "${TESTS[@]}"; do + CURRENT="$t" + FAIL_BEFORE=$FAIL + "$t" + if [ "$FAIL" -eq "$FAIL_BEFORE" ]; then + PASS=$((PASS+1)); printf 'ok %s\n' "$t" + else + printf ' run log tail:\n'; tail -5 "$RUNLOG" 2>/dev/null | sed 's/^/ | /' + fi + rm -rf "$TMP" +done + +printf '\n%d passed, %d failed\n' "$PASS" "$FAIL" +[ "$FAIL" -eq 0 ] diff --git a/tests/stubs/agent b/tests/stubs/agent new file mode 100755 index 0000000..294a284 --- /dev/null +++ b/tests/stubs/agent @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Deterministic test double for the agent CLIs, symlinked as claude/codex/agy. +# Behavior switches on the invoked name ($0) and $FIXBUDDY_TEST_SCENARIO. The +# pipeline stage is detected from the prompt text (mirrors docs/demo/bin/agent). +# Every invocation appends ":" to $FIXBUDDY_TEST_STAGELOG; agy +# invocations additionally record their argv and GH_TOKEN visibility to +# $FIXBUDDY_TEST_AGYLOG so tests can assert on flags and env stripping. +set -u + +name="$(basename "$0")" +scenario="${FIXBUDDY_TEST_SCENARIO:-happy}" + +# claude/codex receive the prompt on stdin; agy receives it as the value after +# -p, with all other flags positioned before it. +prompt="" +flags="" +if [ "$name" = "agy" ]; then + while [ $# -gt 0 ]; do + case "$1" in + -p) prompt="${2:-}"; shift 2 ;; + --add-dir|--print-timeout) flags="$flags $1=${2:-}"; shift 2 ;; + *) flags="$flags $1"; shift ;; + esac + done +else + prompt="$(cat)" +fi + +stage="verify" +case "$prompt" in + *"implementing a fix"*) stage="fix" ;; + *"independent senior code reviewer"*) stage="review" ;; +esac + +[ -n "${FIXBUDDY_TEST_STAGELOG:-}" ] && echo "$name:$stage" >> "$FIXBUDDY_TEST_STAGELOG" +if [ "$name" = "agy" ] && [ -n "${FIXBUDDY_TEST_AGYLOG:-}" ]; then + echo "stage=$stage gh_token=${GH_TOKEN:-unset} args=$flags" >> "$FIXBUDDY_TEST_AGYLOG" +fi + +# The fix stage makes a REAL commit in the target project, parsed from the prompt. +project="$(printf '%s\n' "$prompt" | sed -n 's/^\*\*Working directory:\*\* //p' | head -1)" +do_fix_commit() { + ( cd "$project" \ + && echo "fixed by $name" >> src/app.txt \ + && git add src/app.txt \ + && git commit -q -m "fix: correct app output + +Closes #7" ) +} + +case "$scenario:$stage" in + happy:verify|reject:verify|check:verify) + echo "Reproduced the problem. The issue is real." + echo "DONE-PROCEED" ;; + happy:fix|reject:fix|check:fix) + do_fix_commit + echo "DONE-FIX-APPLIED" ;; + happy:review) + echo "Diff is correct, minimal, in scope." + echo "DONE-APPROVED" ;; + reject:review) + echo "DONE-REJECTED: the fix lacks a regression test" ;; + falsepos:verify) + echo "DONE-FALSE-POSITIVE: the code already behaves correctly" ;; + crash:verify) + echo "transport error: connection reset" + exit 1 ;; + agytimeout:verify) + echo "Error: timed out waiting for response" + exit 0 ;; + *) + echo "DONE-BLOCKED: unexpected scenario '$scenario' at stage '$stage'" + exit 0 ;; +esac diff --git a/tests/stubs/agy b/tests/stubs/agy new file mode 120000 index 0000000..98ea470 --- /dev/null +++ b/tests/stubs/agy @@ -0,0 +1 @@ +agent \ No newline at end of file diff --git a/tests/stubs/claude b/tests/stubs/claude new file mode 120000 index 0000000..98ea470 --- /dev/null +++ b/tests/stubs/claude @@ -0,0 +1 @@ +agent \ No newline at end of file diff --git a/tests/stubs/codex b/tests/stubs/codex new file mode 120000 index 0000000..98ea470 --- /dev/null +++ b/tests/stubs/codex @@ -0,0 +1 @@ +agent \ No newline at end of file diff --git a/tests/stubs/gh b/tests/stubs/gh new file mode 100755 index 0000000..fb433b6 --- /dev/null +++ b/tests/stubs/gh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Deterministic test double for the GitHub CLI. Read-only queries return canned +# JSON; every MUTATING call is appended verbatim to $FIXBUDDY_TEST_MUTLOG so +# tests can assert exactly what fixbuddy would have changed on GitHub. +set -u + +mutate() { + [ -n "${FIXBUDDY_TEST_MUTLOG:-}" ] && echo "$*" >> "$FIXBUDDY_TEST_MUTLOG" + return 0 +} + +case "${1:-} ${2:-}" in + "label create") mutate "$@" ;; + "issue list") + case "$*" in + *"fix:pr-open"*) echo "[]" ;; # unstick scan: nothing stuck + *) cat <<'JSON' +[{"number":7,"title":"app outputs wrong text","labels":[{"name":"bug"},{"name":"severity:high"}],"url":"https://github.com/acme/app/issues/7","body":"src/app.txt should contain a fixed line. Please fix the output."}] +JSON + ;; + esac ;; + "issue view") + cat <<'JSON' +{"number":7,"title":"app outputs wrong text","labels":[{"name":"bug"},{"name":"severity:high"}],"url":"https://github.com/acme/app/issues/7","state":"OPEN","body":"src/app.txt should contain a fixed line. Please fix the output."} +JSON + ;; + "issue edit"|"issue comment"|"issue close") mutate "$@" ;; + "pr create") mutate "$@"; echo "https://github.com/acme/app/pull/12" ;; + "pr merge") mutate "$@" ;; + "pr view") echo "false" ;; # not merged yet -> fix:pr-open path + "pr list") echo "" ;; + "repo view") echo "main" ;; + *) : ;; # auth setup-git etc. -> no-op +esac +exit 0 From 2f5518459345457a7c8e2563b4bf6a0a4e0bf6b6 Mon Sep 17 00:00:00 2001 From: Codevena Date: Fri, 12 Jun 2026 12:46:44 +0100 Subject: [PATCH 04/13] test: cover false-positive, reject, check gate, dry-run, crash paths --- tests/integration.sh | 51 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/tests/integration.sh b/tests/integration.sh index 3aaf2fa..30ccf6b 100755 --- a/tests/integration.sh +++ b/tests/integration.sh @@ -79,9 +79,58 @@ test_happy_path() { || fail "local fix branch not cleaned up" } +test_false_positive() { + SCENARIO=falsepos; make_fixture + run_fixbuddy --auto-merge + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:false-positive' + assert_grep "$MUTLOG" '^issue close 7' + assert_no_grep "$STAGELOG" ':fix$' +} + +test_review_reject() { + SCENARIO=reject; make_fixture + run_fixbuddy --auto-merge + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:rejected' + [ "$(grep -c '^claude:fix$' "$STAGELOG")" -eq 2 ] || fail "expected 2 fix attempts" + [ "$(grep -c '^codex:review$' "$STAGELOG")" -eq 2 ] || fail "expected 2 review attempts" + assert_no_grep "$MUTLOG" '^pr create' + [ -z "$(git -C "$TMP/project" branch --list 'fix/issue-7')" ] \ + || fail "local fix branch not cleaned up" +} + +test_check_gate() { + SCENARIO=check; make_fixture + run_fixbuddy --check-cmd 'false' + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:rejected' + assert_no_grep "$STAGELOG" ':review$' + assert_no_grep "$MUTLOG" '^pr create' +} + +test_dry_run_read_only() { + SCENARIO=happy; make_fixture + run_fixbuddy --dry-run + [ "$RC" -eq 0 ] || fail "exit code $RC" + [ ! -s "$MUTLOG" ] || fail "dry-run made mutations: $(tr '\n' ';' < "$MUTLOG")" + [ ! -s "$STAGELOG" ] || fail "dry-run invoked an agent" + assert_grep "$RUNLOG" '#7' +} + +test_crash_labels_blocked() { + SCENARIO=crash; make_fixture + run_fixbuddy --auto-merge + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:blocked' + # the label-create bootstrap lists every label; only issue edits matter here + assert_no_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:needs-human' +} + # ---------------- Runner ---------------- -TESTS=(test_happy_path) +TESTS=(test_happy_path test_false_positive test_review_reject test_check_gate + test_dry_run_read_only test_crash_labels_blocked) for t in "${TESTS[@]}"; do CURRENT="$t" From 9a68f452ba352b25ab240554314dcf7237808824 Mon Sep 17 00:00:00 2001 From: Codevena Date: Fri, 12 Jun 2026 12:48:12 +0100 Subject: [PATCH 05/13] feat: replace retired gemini agent with agy (Antigravity CLI) --- fixbuddy.sh | 54 ++++++++++++++++++++++++++------------------ tests/integration.sh | 38 ++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 23 deletions(-) diff --git a/fixbuddy.sh b/fixbuddy.sh index 7bd5a2b..d58a132 100755 --- a/fixbuddy.sh +++ b/fixbuddy.sh @@ -23,10 +23,10 @@ # directly; dedup filters and --label/--severity still apply. # --severity Only issues with label severity: # --max Stop after N issues processed -# --fix-agent claude | codex | opencode | gemini (default: claude) -# --review-agent claude | codex | opencode | gemini (default: codex — cross-agent) -# Note: gemini runs read-only (--approval-mode plan) when -# used as verify/review agent. Warned about as fix-agent. +# --fix-agent claude | codex | opencode | agy (default: claude) +# --review-agent claude | codex | opencode | agy (default: codex — cross-agent) +# Note: agy (Antigravity CLI) runs verify/review with +# --sandbox (terminal restrictions) as defense in depth. # --check-cmd Deterministic test gate (repeatable). Run in the project dir # after the fix commit and before review; a non-zero exit is # treated like a review rejection (retried, then fix:rejected). @@ -189,8 +189,13 @@ done # dry-run on a runner without any agent CLI installed). for agent in "$FIX_AGENT" "$REVIEW_AGENT"; do case "$agent" in - claude|codex|opencode|gemini) ;; - *) err "unsupported agent: $agent (valid: claude, codex, opencode, gemini)"; exit 2 ;; + claude|codex|opencode|agy) ;; + gemini) + err "agent 'gemini' is no longer supported: Google retired the Gemini CLI on 2026-06-18." + err "Install the Antigravity CLI (curl -fsSL https://antigravity.google/cli/install.sh | bash)" + err "and use 'agy' instead — also in fix_agent/review_agent config keys." + exit 2 ;; + *) err "unsupported agent: $agent (valid: claude, codex, opencode, agy)"; exit 2 ;; esac done @@ -200,17 +205,9 @@ if ! $DRY_RUN; then claude) command -v claude >/dev/null || { err "claude CLI not found"; exit 2; } ;; codex) command -v codex >/dev/null || { err "codex CLI not found"; exit 2; } ;; opencode) command -v opencode >/dev/null || { err "opencode CLI not found"; exit 2; } ;; - gemini) command -v gemini >/dev/null || { err "gemini CLI not found"; exit 2; } ;; + agy) command -v agy >/dev/null || { err "agy CLI not found"; exit 2; } ;; esac done - - # Gemini is less reliable at independent reasoning than claude/codex/opencode — when used - # as the fix-agent it sometimes commits incomplete patches. We allow it (user's choice) - # but nudge toward using it read-only (verify/review) where it's much safer. - if [ "$FIX_AGENT" = "gemini" ]; then - warn "gemini as fix-agent is experimental — it may produce incomplete or wrong fixes." - warn "Consider --fix-agent claude (or codex/opencode) with --review-agent gemini instead." - fi fi command -v gh >/dev/null || { err "gh CLI not found"; exit 2; } @@ -461,11 +458,13 @@ run_agent() { local outfile outfile=$(mktemp) - # Gemini is restricted to read-only (plan mode) for verify/review — it should observe - # and report, not write. Only the fix stage grants --yolo (tool-use). Claude/codex/ - # opencode manage their own permissions via their own flags. - local gem_mode="yolo" - case "$stage" in verify|review) gem_mode="plan" ;; esac + # agy (Antigravity CLI) has no read-only mode; verify/review add --sandbox + # (terminal restrictions) as defense in depth. --add-dir grants workspace access + # to the project (agents are launched from the operator's CWD, not $PROJECT). + # --print-timeout sits 60s ABOVE the fixbuddy watchdog so the watchdog always + # fires first and the timeout is classified rc=124 (fix:blocked, auto-requeue). + local agy_args=(--dangerously-skip-permissions --add-dir "$PROJECT" --print-timeout "$((AGENT_TIMEOUT+60))s") + case "$stage" in verify|review) agy_args+=(--sandbox) ;; esac # Launch agent pipeline in background; $! captures the PID of the last command. case "$agent" in @@ -478,8 +477,8 @@ run_agent() { opencode) env -u GH_TOKEN -u GITHUB_TOKEN opencode run --dangerously-skip-permissions "$prompt" "$outfile" 2>&1 & ;; - gemini) - env -u GH_TOKEN -u GITHUB_TOKEN gemini -p "$prompt" --approval-mode "$gem_mode" --output-format text "$outfile" 2>&1 & + agy) + env -u GH_TOKEN -u GITHUB_TOKEN agy "${agy_args[@]}" -p "$prompt" "$outfile" 2>&1 & ;; esac local agent_pid=$! @@ -516,6 +515,17 @@ run_agent() { rc=124 fi + # agy exits 0 (!) when its own --print-timeout fires, printing this line + # instead of a DONE marker. Reclassify as timeout so the issue is labeled + # fix:blocked (auto-requeue) rather than the never-retried fix:needs-human. + # Normally unreachable (our --print-timeout sits above the watchdog) — belt + # and braces. + if [ "$agent" = "agy" ] && [ "$rc" -eq 0 ] \ + && ! grep -qE '^DONE-' "$outfile" 2>/dev/null \ + && grep -q '^Error: timed out waiting for response' "$outfile" 2>/dev/null; then + rc=124 + fi + # Detect agent crash — nonzero exit without any DONE-* marker. # Covers codex usage-limit (rc=1 + "You've hit your usage limit"), MCP transport # errors, and any other hard exit that prevents the agent from completing its task. diff --git a/tests/integration.sh b/tests/integration.sh index 30ccf6b..4f7ac4b 100755 --- a/tests/integration.sh +++ b/tests/integration.sh @@ -127,10 +127,46 @@ test_crash_labels_blocked() { assert_no_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:needs-human' } +test_agy_full_pipeline() { + SCENARIO=happy; make_fixture + run_fixbuddy --fix-agent agy --review-agent agy + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:pr-open' + # agy invocation contract: workspace dir, print-timeout above the watchdog + # (default 1200+60), sandbox on verify/review but NOT on fix, GH_TOKEN stripped + assert_substr "$AGYLOG" "--add-dir=$TMP/project" + assert_substr "$AGYLOG" "--print-timeout=1260s" + assert_grep "$AGYLOG" '^stage=verify .*--sandbox' + assert_grep "$AGYLOG" '^stage=review .*--sandbox' + assert_no_grep "$AGYLOG" '^stage=fix .*--sandbox' + assert_grep "$AGYLOG" 'gh_token=unset' +} + +test_gemini_rejected_with_migration_hint() { + SCENARIO=happy; make_fixture + run_fixbuddy --fix-agent gemini + [ "$RC" -eq 2 ] || fail "expected exit 2, got $RC" + assert_grep "$RUNLOG" "agy" + assert_grep "$RUNLOG" "[Gg]emini CLI" +} + +test_agy_internal_timeout_is_blocked() { + # agy exits 0 on its own --print-timeout with an error line instead of a + # DONE marker; fixbuddy must classify that as a crash/timeout (fix:blocked, + # auto-requeue) — not as the never-retried fix:needs-human path. + SCENARIO=agytimeout; make_fixture + run_fixbuddy --fix-agent agy --review-agent agy + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:blocked' + assert_no_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:needs-human' +} + # ---------------- Runner ---------------- TESTS=(test_happy_path test_false_positive test_review_reject test_check_gate - test_dry_run_read_only test_crash_labels_blocked) + test_dry_run_read_only test_crash_labels_blocked + test_agy_full_pipeline test_gemini_rejected_with_migration_hint + test_agy_internal_timeout_is_blocked) for t in "${TESTS[@]}"; do CURRENT="$t" From 9af1ca5365139c14acde3e259be299b0b0b3deb1 Mon Sep 17 00:00:00 2001 From: Codevena Date: Fri, 12 Jun 2026 12:48:35 +0100 Subject: [PATCH 06/13] feat(wizard): offer agy instead of retired gemini --- fixbuddy-wizard.sh | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/fixbuddy-wizard.sh b/fixbuddy-wizard.sh index 354a354..620b196 100755 --- a/fixbuddy-wizard.sh +++ b/fixbuddy-wizard.sh @@ -53,7 +53,7 @@ done # Agent CLIs — track which are available so we can offer only the installed ones. AGENTS_AVAILABLE=() -for a in claude codex opencode gemini; do +for a in claude codex opencode agy; do if command -v "$a" >/dev/null 2>&1; then ok "$a found" AGENTS_AVAILABLE+=("$a") @@ -63,7 +63,7 @@ for a in claude codex opencode gemini; do done if [ "${#AGENTS_AVAILABLE[@]}" -eq 0 ]; then - fail "no agent CLI installed — need at least one of: claude, codex, opencode, gemini" + fail "no agent CLI installed — need at least one of: claude, codex, opencode, agy" missing=1 fi @@ -193,14 +193,12 @@ is_available() { } step "7a." "Which agent writes the fixes?" -note "claude is the most reliable fixer; codex and opencode are strong alternatives." -note "gemini is read-only-ish and not recommended as a fixer." +note "claude is the most reliable fixer; codex, opencode, and agy are strong alternatives." FIX_CHOICES=() n=1 -for a in claude codex opencode gemini; do +for a in claude codex opencode agy; do if is_available "$a"; then label="$a" - [ "$a" = "gemini" ] && label="$a ${DIM}(experimental — often writes incomplete fixes)${RST}" printf " [%d] %b\n" "$n" "$label" FIX_CHOICES+=("$a") n=$((n+1)) @@ -218,14 +216,14 @@ ok "fix agent: $FIX_AGENT" # -------- Step 7b: reviewer -------- step "7b." "Which reviewer agent?" note "Cross-agent review (different from the fixer) catches more bugs." -note "gemini in review mode runs read-only — safer but less thorough." +note "agy runs verify/review with a sandbox (terminal restrictions)." REV_CHOICES=() n=1 -for a in codex claude opencode gemini; do +for a in codex claude opencode agy; do if is_available "$a"; then label="$a" [ "$a" = "$FIX_AGENT" ] && label="$a ${DIM}(same-agent — less adversarial)${RST}" - [ "$a" = "gemini" ] && label="$a ${DIM}(read-only, experimental — quick second opinion)${RST}" + [ "$a" = "agy" ] && label="$a ${DIM}(sandboxed verify/review)${RST}" printf " [%d] %b\n" "$n" "$label" REV_CHOICES+=("$a") n=$((n+1)) From a6a7d93d37e12fd71ec2ebd034e28414789c32a6 Mon Sep 17 00:00:00 2001 From: Codevena Date: Fri, 12 Jun 2026 12:49:43 +0100 Subject: [PATCH 07/13] docs: document agy agent, remove gemini from README and action --- CONTRIBUTING.md | 9 +++++++-- README.md | 19 +++++++++---------- action.yml | 4 ++-- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 308c18f..59f0eb0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,9 +21,13 @@ Run these before opening a pull request: ```bash bash -n fixbuddy.sh bash -n fixbuddy-wizard.sh -shellcheck fixbuddy.sh fixbuddy-wizard.sh +shellcheck fixbuddy.sh fixbuddy-wizard.sh tests/integration.sh tests/stubs/agent tests/stubs/gh +tests/integration.sh ``` +`tests/integration.sh` runs the full pipeline offline against stubbed `gh`/agent +CLIs and a local bare repository — no network, no API keys, a few seconds. + If `shellcheck` is not available locally, the GitHub Actions workflow will run it for pull requests. ## Pull Request Guidelines @@ -36,7 +40,8 @@ If `shellcheck` is not available locally, the GitHub Actions workflow will run i ## Manual Test Checklist -For behavior changes, test against a disposable repository when possible: +Most of these paths are covered by `tests/integration.sh`; for behavior changes, +additionally test against a disposable repository when possible: 1. `--dry-run` lists expected issues. 2. A false-positive verification closes or labels the issue correctly. diff --git a/README.md b/README.md index ada2e7a..af30773 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![version](https://img.shields.io/github/v/tag/Codevena/fixbuddy?label=version)](https://github.com/Codevena/fixbuddy/tags) [![license](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) [![shell](https://img.shields.io/badge/shell-bash-black.svg)](fixbuddy.sh) -[![agents](https://img.shields.io/badge/agents-claude%20%7C%20codex%20%7C%20opencode%20%7C%20gemini-purple.svg)](#supported-agents) +[![agents](https://img.shields.io/badge/agents-claude%20%7C%20codex%20%7C%20opencode%20%7C%20agy-purple.svg)](#supported-agents) fixbuddy reads open GitHub issues, asks one agent to verify and fix each issue, asks a second agent to review the committed diff, then opens a pull request. If enabled, it requests auto-merge after review approval. @@ -22,12 +22,12 @@ The goal is controlled automation: one issue per branch, one issue per PR, expli Most AI issue-fixers let a single agent write a fix and, at best, review its own work. fixbuddy splits the job across **two different agents from two different vendors**: by default `claude` writes the fix and `codex` reviews the committed diff with a fresh context. The fixer never approves its own work. -It needs no cloud service, no Docker, and no separate API-key broker — it drives the AI coding CLIs you already have installed (`claude`, `codex`, `opencode`, `gemini`), so it runs on the subscriptions you already pay for. The orchestrator is ~1,200 lines of readable Bash. +It needs no cloud service, no Docker, and no separate API-key broker — it drives the AI coding CLIs you already have installed (`claude`, `codex`, `opencode`, `agy`), so it runs on the subscriptions you already pay for. The orchestrator is ~1,200 lines of readable Bash. | | fixbuddy | Copilot coding agent | claude-code-action | OpenHands resolver | |---|---|---|---|---| | Fix **and** review | two agents, cross-vendor (fixer ≠ reviewer) | one vendor | one vendor | one agent | -| Choice of agent | claude · codex · opencode · gemini | Copilot's models | Claude only | bring your own LLM | +| Choice of agent | claude · codex · opencode · agy | Copilot's models | Claude only | bring your own LLM | | Where it runs | your machine **or** a GitHub Action | GitHub cloud | GitHub Action | local / Docker | | Infra required | bash · git · gh · jq | none (hosted) | GitHub Actions | Docker + API keys | | Cost | your existing CLI subscriptions | paid Copilot (premium requests) | API / subscription | your API + compute | @@ -121,7 +121,7 @@ Both `--fix-agent` and `--review-agent` must be installed. They may point to the | `claude` | `claude --dangerously-skip-permissions -p -` | Full tool access. | | `codex` | `codex exec --dangerously-bypass-approvals-and-sandbox` | Full tool access. | | `opencode` | `opencode run --dangerously-skip-permissions` | Full tool access. | -| `gemini` | `gemini -p ... --approval-mode {plan\|yolo}` | Read-only style `plan` mode for verify/review; `yolo` for fix. | +| `agy` | `agy --dangerously-skip-permissions --add-dir -p ...` | Antigravity CLI (Gemini's successor). Verify/review add `--sandbox` (terminal restrictions — not read-only). | These agent invocations are intentionally powerful. Run fixbuddy only against repositories and issue content you trust. @@ -134,8 +134,8 @@ These agent invocations are intentionally powerful. Run fixbuddy only against re | `--label ` | Include only issues with this label. Repeatable | none | | `--severity ` | Include issues labeled `severity:` | none | | `--max ` | Maximum issues to process in this run | unlimited | -| `--fix-agent ` | `claude`, `codex`, `opencode`, or `gemini` | `claude` | -| `--review-agent ` | `claude`, `codex`, `opencode`, or `gemini` | `codex` | +| `--fix-agent ` | `claude`, `codex`, `opencode`, or `agy` | `claude` | +| `--review-agent ` | `claude`, `codex`, `opencode`, or `agy` | `codex` | | `--max-retries ` | Retry count after review rejection | `1` | | `--agent-timeout ` | Wall-clock timeout per agent call | `1200` | | `--crash-abort ` | Abort after consecutive agent crashes | `3` | @@ -275,11 +275,11 @@ Use one agent for both roles: --fix-agent claude --review-agent claude --max 3 ``` -Use Gemini as a read-only reviewer: +Use agy (Antigravity CLI) as a cross-vendor reviewer: ```bash ./fixbuddy.sh --repo owner/repo --project ~/code/repo \ - --fix-agent claude --review-agent gemini + --fix-agent claude --review-agent agy ``` ## Use in GitHub Actions @@ -343,7 +343,7 @@ If `github-token` is empty the action fails fast with a clear error. Pass a diff ### Prerequisites in CI -GitHub-hosted runners do **not** ship the agent CLIs (`claude`, `codex`, `opencode`, `gemini`). Install whichever ones you pass to `fix-agent` / `review-agent` in a step *before* the `Codevena/fixbuddy` step — pinning them to a known version is recommended. Consult each agent's own documentation for the current install command. +GitHub-hosted runners do **not** ship the agent CLIs (`claude`, `codex`, `opencode`, `agy`). Install whichever ones you pass to `fix-agent` / `review-agent` in a step *before* the `Codevena/fixbuddy` step — pinning them to a known version is recommended. Consult each agent's own documentation for the current install command. Note that `agy` has no npm package — install it with the vendor script: `curl -fsSL https://antigravity.google/cli/install.sh | bash`. The action does not read API keys itself; each agent CLI reads its own environment variable (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, and so on). Provide them from `secrets` at the job or workflow level, as shown above. @@ -392,7 +392,6 @@ Native Windows is not tested. WSL2 is the recommended Windows environment. ## Roadmap -- More deterministic integration tests with mocked CLIs - Optional notifications for run summaries - Explicit resume mode for interrupted runs (interrupting a run cleans up the in-progress branch; the issue is retried automatically on the next run) diff --git a/action.yml b/action.yml index a7888c1..6790704 100644 --- a/action.yml +++ b/action.yml @@ -15,11 +15,11 @@ inputs: required: false default: "." fix-agent: - description: "Fix agent: claude | codex | opencode | gemini" + description: "Fix agent: claude | codex | opencode | agy" required: false default: "claude" review-agent: - description: "Review agent: claude | codex | opencode | gemini" + description: "Review agent: claude | codex | opencode | agy" required: false default: "codex" severity: From 50f2fe253e50fac49a8dbfa93c7b3fc9f3cd6043 Mon Sep 17 00:00:00 2001 From: Codevena Date: Fri, 12 Jun 2026 12:50:20 +0100 Subject: [PATCH 08/13] ci: run integration tests, lint test scripts --- .github/workflows/ci.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80c1b13..f2480b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,9 +19,20 @@ jobs: run: | bash -n fixbuddy.sh bash -n fixbuddy-wizard.sh + bash -n tests/integration.sh tests/stubs/agent tests/stubs/gh - name: Install ShellCheck run: sudo apt-get update && sudo apt-get install -y shellcheck - name: ShellCheck - run: shellcheck fixbuddy.sh fixbuddy-wizard.sh + run: shellcheck fixbuddy.sh fixbuddy-wizard.sh tests/integration.sh tests/stubs/agent tests/stubs/gh + + integration: + name: Integration tests (mocked CLIs) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Run integration tests + run: tests/integration.sh From f63d612d4aa8e4f29b47f43e85f69474b24e3f4d Mon Sep 17 00:00:00 2001 From: Codevena Date: Fri, 12 Jun 2026 12:51:34 +0100 Subject: [PATCH 09/13] release: v0.6.0 (version bumps, changelog, SHA256SUMS, housekeeping) --- CHANGELOG.md | 25 +++++++++++ NEXT_SESSION.md | 42 ++++++++++++------- README.md | 6 +-- SHA256SUMS | 4 +- .../audit/2026-06-10-findings.md | 0 fixbuddy-wizard.sh | 4 +- fixbuddy.sh | 4 +- install.sh | 10 ++--- 8 files changed, 67 insertions(+), 28 deletions(-) rename findings.md => docs/audit/2026-06-10-findings.md (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f3c28e..349dcbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ All notable changes to fixbuddy are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project aims to follow [Semantic Versioning](https://semver.org/). +## [0.6.0] - 2026-06-12 + +Google retires the Gemini CLI on 2026-06-18; its successor is the Antigravity +CLI (`agy`). fixbuddy v0.6.0 swaps the agent and gains an offline integration +test suite. + +### Added +- **`agy` agent** (Antigravity CLI) as fix or review agent. Invocation details + that matter: `--add-dir ` (agents launch from the operator's CWD), + `--print-timeout` pinned 60s above `--agent-timeout` so fixbuddy's watchdog + classifies timeouts (agy itself exits 0 on its internal timeout — fixbuddy + also detects that output and treats it as `fix:blocked`/auto-requeue), and + `--sandbox` on verify/review as defense in depth (agy has no read-only mode). +- **Integration tests** (`tests/integration.sh`) — deterministic, offline, + zero new dependencies: stub `gh`/agent CLIs plus a local bare repo as + `origin`, covering happy path, false positive, review rejection, check gate, + dry-run read-only, and crash classification. Run in CI. + +### Removed (breaking) +- **`gemini` agent.** Passing `gemini` (flag or config) now exits with a + migration message. Replace `fix_agent`/`review_agent` values with `agy`. + Note: agy in verify/review runs sandboxed but NOT read-only — the old + `--approval-mode plan` has no equivalent in the Antigravity CLI. + ## [0.5.0] - 2026-06-12 Security hardening from a full audit, plus five new features. No breaking changes @@ -49,4 +73,5 @@ to existing flags. Predate this changelog. See the git history and the `v0.4.0` / `v0.3.2` tags. +[0.6.0]: https://github.com/Codevena/fixbuddy/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/Codevena/fixbuddy/compare/v0.4.0...v0.5.0 diff --git a/NEXT_SESSION.md b/NEXT_SESSION.md index f79ea6e..cb0b83c 100644 --- a/NEXT_SESSION.md +++ b/NEXT_SESSION.md @@ -1,25 +1,39 @@ # Next session -All P0/P1 review items and the full P2 backlog (#6 repo topics, #7 `install.sh`, #8 GitHub Action wrapper) are done. +v0.6.0 is implemented on branch `feat/agy-agent-v0.6.0`: the retired `gemini` +agent is replaced by `agy` (Antigravity CLI — Gemini CLI shuts down 2026-06-18) +and a deterministic offline integration test suite landed. ## Status snapshot -- Branch `main`, working tree clean. -- **`v0.4.0` is tagged and pushed**, with a floating **`v1`** ref pointing at it. Consumers use `uses: Codevena/fixbuddy@v1`. -- `action.yml` (composite action, pure bash) + `.github/workflows/action-smoke.yml` (dry-run smoke test) shipped in #8. README has a "Use in GitHub Actions" section. -- `fixbuddy.sh` `--dry-run` skips the agent-CLI presence check (agent-name validation still runs) so the smoke test works on bare runners. -- `install.sh` pins `DEFAULT_REF="v0.4.0"`; README Quick Start one-liner points at `v0.4.0`. -- `SHA256SUMS` holds hashes of `fixbuddy.sh` + `fixbuddy-wizard.sh`. **Regenerate it (`shasum -a 256 fixbuddy.sh fixbuddy-wizard.sh > SHA256SUMS`) whenever either script changes, before cutting a new tag** — `install.sh` verifies checksums and fails closed. -- When cutting a new release: bump `VERSION` in `fixbuddy.sh`, the header comment, `install.sh` `DEFAULT_REF`, the README one-liner; regenerate `SHA256SUMS`; tag `vX.Y.Z`; then `git tag -f v1 vX.Y.Z && git push origin v1 --force` to move the floating major ref. -- Definition-of-Done gate from `~/.claude/CLAUDE.md` still applies. Note from #8: `codex exec` hung at 0% CPU again — the OpenCode fallback (`opencode run --dangerously-skip-permissions "$( SHA256SUMS`) whenever either +script changes, before tagging — `install.sh` verifies checksums fail-closed. + +## What's next (README Roadmap) -- Config file support -- More deterministic integration tests with mocked CLIs - Optional notifications for run summaries - Explicit resume mode for interrupted runs -Pick one and run it through the brainstorm → plan → DoD-gate flow. Verify the action-smoke workflow went green on GitHub after the v0.4.0 push. +Pick one and run it through the brainstorm → plan → DoD-gate flow. diff --git a/README.md b/README.md index af30773..7ae6d7e 100644 --- a/README.md +++ b/README.md @@ -57,15 +57,15 @@ VERIFY -> FIX -> REVIEW -> PUSH/PR -> optional auto-merge Install with the one-liner (macOS and Linux, including WSL2): ```bash -curl -fsSL https://raw.githubusercontent.com/Codevena/fixbuddy/v0.5.0/install.sh | bash +curl -fsSL https://raw.githubusercontent.com/Codevena/fixbuddy/v0.6.0/install.sh | bash ``` -This downloads the pinned `v0.5.0` scripts into `~/.local/bin` (or `/usr/local/bin`), makes them executable, and prints a PATH hint if needed. Override the location with `| bash -s -- --prefix /custom/bin` or track the latest commit with `--ref main`. +This downloads the pinned `v0.6.0` scripts into `~/.local/bin` (or `/usr/local/bin`), makes them executable, and prints a PATH hint if needed. Override the location with `| bash -s -- --prefix /custom/bin` or track the latest commit with `--ref main`. **Prefer to read before you run?** The installer is short — inspect it first, then run it: ```bash -curl -fsSL https://raw.githubusercontent.com/Codevena/fixbuddy/v0.5.0/install.sh -o install.sh +curl -fsSL https://raw.githubusercontent.com/Codevena/fixbuddy/v0.6.0/install.sh -o install.sh less install.sh # read it bash install.sh # then run it ``` diff --git a/SHA256SUMS b/SHA256SUMS index 931d51f..29c840d 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,2 +1,2 @@ -288eda7831f6b3bd7078417e46ac8270a7f8b0175a288748cf9f3bac55c23cb8 fixbuddy.sh -d6868daf05471c46ab969fe50b441af004d6622f664c529912da82d91665ac73 fixbuddy-wizard.sh +563bdbf7265948ad878c2cedcebb92ab734e17116eb173e1b1b58af835e8bacd fixbuddy.sh +ba3ba121a60ecb37fc5ca796f3cf5279a24a54566aeb2bb0dca3c3a1f4b7bd2f fixbuddy-wizard.sh diff --git a/findings.md b/docs/audit/2026-06-10-findings.md similarity index 100% rename from findings.md rename to docs/audit/2026-06-10-findings.md diff --git a/fixbuddy-wizard.sh b/fixbuddy-wizard.sh index 620b196..2336207 100755 --- a/fixbuddy-wizard.sh +++ b/fixbuddy-wizard.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# fixbuddy-wizard.sh v0.5.0 — beginner-friendly launcher for fixbuddy.sh +# fixbuddy-wizard.sh v0.6.0 — beginner-friendly launcher for fixbuddy.sh # # Walks a user through the required flags via interactive prompts, validates # prerequisites, shows a preview of the exact command, and then exec's fixbuddy.sh. @@ -32,7 +32,7 @@ printf "%s" "${MAG}${BOLD}" cat <<'EOF' ╔═══════════════════════════════════════════════════╗ - ║ fixbuddy wizard v0.5.0 ║ + ║ fixbuddy wizard v0.6.0 ║ ║ Turn GitHub issues into reviewed PRs ║ ╚═══════════════════════════════════════════════════╝ EOF diff --git a/fixbuddy.sh b/fixbuddy.sh index d58a132..fd0f1f2 100755 --- a/fixbuddy.sh +++ b/fixbuddy.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# fixbuddy v0.5.0 — two-agent pipeline for autonomous issue fixing +# fixbuddy v0.6.0 — two-agent pipeline for autonomous issue fixing # # Pipeline per issue: # 1. VERIFY (fix-agent) — is this real? → PROCEED / FALSE-POSITIVE / BLOCKED @@ -47,7 +47,7 @@ # config-provided label/check cannot be removed from the CLI. set -uo pipefail -VERSION="0.5.0" +VERSION="0.6.0" # -------- Defaults -------- REPO="" diff --git a/install.sh b/install.sh index d02e77c..7066694 100755 --- a/install.sh +++ b/install.sh @@ -2,12 +2,12 @@ # install.sh — installer for fixbuddy (https://github.com/Codevena/fixbuddy) # # Quick install: -# curl -fsSL https://raw.githubusercontent.com/Codevena/fixbuddy/v0.5.0/install.sh | bash +# curl -fsSL https://raw.githubusercontent.com/Codevena/fixbuddy/v0.6.0/install.sh | bash # # Options (pass after the URL as: | bash -s -- ): # --prefix PATH Install into PATH instead of the auto-detected location # --ref TAG Install the fixbuddy scripts from a specific git ref. -# Default: v0.5.0. Use --ref main for the latest commit. +# Default: v0.6.0. Use --ref main for the latest commit. # -y, --yes Skip the sudo confirmation prompt # -h, --help Show this help and exit # @@ -16,7 +16,7 @@ set -euo pipefail REPO_SLUG="Codevena/fixbuddy" -DEFAULT_REF="v0.5.0" +DEFAULT_REF="v0.6.0" RAW_BASE="https://raw.githubusercontent.com/${REPO_SLUG}" SCRIPTS=(fixbuddy.sh fixbuddy-wizard.sh) @@ -40,11 +40,11 @@ usage() { cat >&2 <<'EOF' install.sh — installer for fixbuddy - curl -fsSL https://raw.githubusercontent.com/Codevena/fixbuddy/v0.5.0/install.sh | bash + curl -fsSL https://raw.githubusercontent.com/Codevena/fixbuddy/v0.6.0/install.sh | bash Options (pass as: | bash -s -- ): --prefix PATH Install into PATH instead of the auto-detected location - --ref TAG Install fixbuddy scripts from a specific git ref (default: v0.5.0; + --ref TAG Install fixbuddy scripts from a specific git ref (default: v0.6.0; use --ref main for the latest commit) -y, --yes Skip the sudo confirmation prompt -h, --help Show this help and exit From 44b4f233dfceb903f9c9e8ef9e39dee0ccf1d602 Mon Sep 17 00:00:00 2001 From: Codevena Date: Fri, 12 Jun 2026 13:32:44 +0100 Subject: [PATCH 10/13] =?UTF-8?q?fix:=20guard=20read-only=20stages=20?= =?UTF-8?q?=E2=80=94=20stash=20verify=20residue,=20pin=20review=20branch?= =?UTF-8?q?=20to=20reviewed=20commit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 7 +++++++ README.md | 2 ++ SHA256SUMS | 2 +- fixbuddy.sh | 21 +++++++++++++++++++++ tests/integration.sh | 25 ++++++++++++++++++++++++- tests/stubs/agent | 21 +++++++++++++++++++++ 6 files changed, 76 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 349dcbe..bf45e33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,13 @@ test suite. Note: agy in verify/review runs sandboxed but NOT read-only — the old `--approval-mode plan` has no equivalent in the Antigravity CLI. +### Security & robustness +- The two read-only-by-contract stages are now guarded deterministically (no + agent CLI offers an enforced read-only mode): worktree files written during + **verify** are stashed before the fix branch is created, and the **review** + branch is pinned to the reviewed commit — commits a reviewer creates are + discarded, so only the reviewed commit is ever pushed. + ## [0.5.0] - 2026-06-12 Security hardening from a full audit, plus five new features. No breaking changes diff --git a/README.md b/README.md index 7ae6d7e..797a74f 100644 --- a/README.md +++ b/README.md @@ -215,8 +215,10 @@ fixbuddy creates and manages these labels: - fixbuddy refuses to start if the target checkout has a dirty working tree. - Each issue gets a fresh `fix/issue-N` branch. +- Files written during the verify stage (read-only by contract, but no agent CLI enforces that) are stashed before the fix branch is created. - The fix agent is instructed to stage only relevant files and to avoid generated artifacts. - The review agent receives the committed diff and must reject unrelated changes. +- If the reviewer creates commits, the branch is reset to the reviewed commit — only the reviewed commit is ever pushed. - Push happens only after review approval. - `fix:applied` is added only after GitHub reports that the PR is merged. - Cleanup stashes uncommitted agent output before deleting temporary branches. diff --git a/SHA256SUMS b/SHA256SUMS index 29c840d..472362d 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,2 +1,2 @@ -563bdbf7265948ad878c2cedcebb92ab734e17116eb173e1b1b58af835e8bacd fixbuddy.sh +58094ce48fa0c54cd907f26cfff763ae2602b1f4b42d20f5f7f7bb99a345f570 fixbuddy.sh ba3ba121a60ecb37fc5ca796f3cf5279a24a54566aeb2bb0dca3c3a1f4b7bd2f fixbuddy-wizard.sh diff --git a/fixbuddy.sh b/fixbuddy.sh index fd0f1f2..a37d329 100755 --- a/fixbuddy.sh +++ b/fixbuddy.sh @@ -935,6 +935,16 @@ The \`fix:needs-human\` label has been applied. This issue requires human attent return 0 fi + # Verify is contractually read-only, but no agent CLI enforces that (agy's + # --sandbox still allows workspace writes; claude/codex/opencode run with + # permission checks skipped). The tree was clean at startup, so anything + # dirty here is verify-stage residue — stash it so it can never leak into + # the fix branch or commit. Recoverable via `git stash list`. + if [ -n "$(cd "$PROJECT" && git status --porcelain 2>/dev/null)" ]; then + warn "[#$num] verify stage left worktree changes — stashing residue" + (cd "$PROJECT" && git stash push --include-untracked -m "fixbuddy-verify-residue-$num-$(ts)" --quiet) >/dev/null 2>&1 || true + fi + # ---- Stage 2+3: FIX + REVIEW (with retry) ---- local feedback="" attempt=0 approved=false while [ "$attempt" -le "$MAX_RETRIES" ]; do @@ -1053,9 +1063,20 @@ The \`fix:needs-human\` label has been applied. This issue will not be retried a fi fi + # The reviewer is contractually read-only, but no agent CLI enforces that. + # Record the commit the diff was taken from so any commits the reviewer + # creates can be discarded — only the reviewed commit may ever be pushed. + local review_head + review_head=$(cd "$PROJECT" && git rev-parse HEAD) + out=$(run_agent "$REVIEW_AGENT" "$(review_prompt "$num" "$title" "$body" "$diff")" "$issue_log" review) rc=$? + if [ -n "$review_head" ] && [ "$(cd "$PROJECT" && git rev-parse HEAD)" != "$review_head" ]; then + warn "[#$num] review stage created commits — resetting branch to the reviewed commit" + (cd "$PROJECT" && git reset --hard "$review_head") >/dev/null 2>&1 || true + fi + if [ "$did_stash" = "1" ]; then (cd "$PROJECT" && git stash pop --quiet) >/dev/null 2>&1 || warn "[#$num] 'git stash pop' failed — check 'git stash list'" fi diff --git a/tests/integration.sh b/tests/integration.sh index 4f7ac4b..ff1f51f 100755 --- a/tests/integration.sh +++ b/tests/integration.sh @@ -161,12 +161,35 @@ test_agy_internal_timeout_is_blocked() { assert_no_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:needs-human' } +test_verify_residue_is_stashed() { + # No agent CLI offers an enforced read-only mode, so files written during the + # verify stage must be stashed away before the fix branch is created — the + # fix stub emits DONE-BLOCKED if it still sees the residue file. + SCENARIO=verifydirty; make_fixture + run_fixbuddy --auto-merge + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:pr-open' + assert_no_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:needs-human' +} + +test_reviewer_commit_is_discarded() { + # A reviewer that commits to the fix branch must not get those commits + # pushed: the branch is pinned back to the commit the diff was taken from. + SCENARIO=reviewcommit; make_fixture + run_fixbuddy --auto-merge + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:pr-open' + [ "$(git -C "$TMP/origin.git" rev-list --count refs/heads/main..refs/heads/fix/issue-7)" -eq 1 ] \ + || fail "rogue reviewer commit was pushed" +} + # ---------------- Runner ---------------- TESTS=(test_happy_path test_false_positive test_review_reject test_check_gate test_dry_run_read_only test_crash_labels_blocked test_agy_full_pipeline test_gemini_rejected_with_migration_hint - test_agy_internal_timeout_is_blocked) + test_agy_internal_timeout_is_blocked + test_verify_residue_is_stashed test_reviewer_commit_is_discarded) for t in "${TESTS[@]}"; do CURRENT="$t" diff --git a/tests/stubs/agent b/tests/stubs/agent index 294a284..3fdd0c2 100755 --- a/tests/stubs/agent +++ b/tests/stubs/agent @@ -68,6 +68,27 @@ case "$scenario:$stage" in agytimeout:verify) echo "Error: timed out waiting for response" exit 0 ;; + verifydirty:verify) + ( cd "$project" && echo "scratch" > junk.txt ) + echo "DONE-PROCEED" ;; + verifydirty:fix) + if [ -f "$project/junk.txt" ]; then + echo "DONE-BLOCKED: verify residue leaked into the fix worktree" + else + do_fix_commit + echo "DONE-FIX-APPLIED" + fi ;; + verifydirty:review) + echo "DONE-APPROVED" ;; + reviewcommit:verify) + echo "DONE-PROCEED" ;; + reviewcommit:fix) + do_fix_commit + echo "DONE-FIX-APPLIED" ;; + reviewcommit:review) + ( cd "$project" && echo "rogue" >> src/app.txt && git add src/app.txt \ + && git commit -q -m "rogue reviewer commit" ) + echo "DONE-APPROVED" ;; *) echo "DONE-BLOCKED: unexpected scenario '$scenario' at stage '$stage'" exit 0 ;; From badcc36e8098051b3e905ac6b7929ca181cb1283 Mon Sep 17 00:00:00 2001 From: Codevena Date: Fri, 12 Jun 2026 13:36:53 +0100 Subject: [PATCH 11/13] fix: discard commits a verify agent creates on the base branch --- CHANGELOG.md | 7 ++++--- README.md | 2 +- SHA256SUMS | 2 +- fixbuddy.sh | 22 +++++++++++++++++++++- tests/integration.sh | 18 +++++++++++++++++- tests/stubs/agent | 9 +++++++++ 6 files changed, 53 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf45e33..0b9ef78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,9 +31,10 @@ test suite. ### Security & robustness - The two read-only-by-contract stages are now guarded deterministically (no agent CLI offers an enforced read-only mode): worktree files written during - **verify** are stashed before the fix branch is created, and the **review** - branch is pinned to the reviewed commit — commits a reviewer creates are - discarded, so only the reviewed commit is ever pushed. + **verify** are stashed and commits it creates on the base branch are + discarded before the fix branch is created, and the **review** branch is + pinned to the reviewed commit — commits a reviewer creates are discarded, so + only the reviewed commit is ever pushed. ## [0.5.0] - 2026-06-12 diff --git a/README.md b/README.md index 797a74f..e375cb6 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ fixbuddy creates and manages these labels: - fixbuddy refuses to start if the target checkout has a dirty working tree. - Each issue gets a fresh `fix/issue-N` branch. -- Files written during the verify stage (read-only by contract, but no agent CLI enforces that) are stashed before the fix branch is created. +- The verify stage is read-only by contract, but no agent CLI enforces that: files it writes are stashed and commits it creates on the base branch are discarded before the fix branch is created. - The fix agent is instructed to stage only relevant files and to avoid generated artifacts. - The review agent receives the committed diff and must reject unrelated changes. - If the reviewer creates commits, the branch is reset to the reviewed commit — only the reviewed commit is ever pushed. diff --git a/SHA256SUMS b/SHA256SUMS index 472362d..dcf83f4 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,2 +1,2 @@ -58094ce48fa0c54cd907f26cfff763ae2602b1f4b42d20f5f7f7bb99a345f570 fixbuddy.sh +098c5e3e0470db41c947a8cc98b46c1d131d7c0d6aa45540015921f3a26c8ca1 fixbuddy.sh ba3ba121a60ecb37fc5ca796f3cf5279a24a54566aeb2bb0dca3c3a1f4b7bd2f fixbuddy-wizard.sh diff --git a/fixbuddy.sh b/fixbuddy.sh index a37d329..eb25fb7 100755 --- a/fixbuddy.sh +++ b/fixbuddy.sh @@ -893,7 +893,11 @@ process_issue() { # ---- Stage 1: VERIFY ---- info "[#$num] VERIFY" - local out rc + # Capture the base ref before verify: a verify agent that COMMITS leaves a + # clean worktree (the residue stash below cannot catch it), and branch setup + # would build fix/issue-N on top of that commit and push it. + local out rc pre_verify_base + pre_verify_base=$(cd "$PROJECT" && git rev-parse "refs/heads/$BASE_BRANCH" 2>/dev/null) out=$(run_agent "$FIX_AGENT" "$(verify_prompt "$num" "$title" "$body")" "$issue_log" verify) rc=$? @@ -945,6 +949,22 @@ The \`fix:needs-human\` label has been applied. This issue requires human attent (cd "$PROJECT" && git stash push --include-untracked -m "fixbuddy-verify-residue-$num-$(ts)" --quiet) >/dev/null 2>&1 || true fi + # Pin the base ref back if the verify stage moved it (committed on the base + # branch). Runs AFTER the stash above so a reset never touches uncommitted + # files; the discarded commits stay recoverable via the reflog. + if [ -n "$pre_verify_base" ] \ + && [ "$(cd "$PROJECT" && git rev-parse "refs/heads/$BASE_BRANCH" 2>/dev/null)" != "$pre_verify_base" ]; then + warn "[#$num] verify stage created commits on $BASE_BRANCH — resetting to pre-verify state" + ( + cd "$PROJECT" || exit 0 + if [ "$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" = "$BASE_BRANCH" ]; then + git reset --hard "$pre_verify_base" >/dev/null 2>&1 + else + git branch -f "$BASE_BRANCH" "$pre_verify_base" >/dev/null 2>&1 + fi + ) || true + fi + # ---- Stage 2+3: FIX + REVIEW (with retry) ---- local feedback="" attempt=0 approved=false while [ "$attempt" -le "$MAX_RETRIES" ]; do diff --git a/tests/integration.sh b/tests/integration.sh index ff1f51f..f759861 100755 --- a/tests/integration.sh +++ b/tests/integration.sh @@ -172,6 +172,21 @@ test_verify_residue_is_stashed() { assert_no_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:needs-human' } +test_verify_commit_is_discarded() { + # A verify agent that COMMITS (the worktree stays clean, so the residue + # stash cannot catch it) must not get that commit into the fix branch: the + # base ref is pinned back to its pre-verify position. + SCENARIO=verifycommit; make_fixture + run_fixbuddy --auto-merge + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:pr-open' + [ "$(git -C "$TMP/origin.git" rev-list --count refs/heads/main..refs/heads/fix/issue-7)" -eq 1 ] \ + || fail "rogue verify commit was pushed" + if git -C "$TMP/origin.git" ls-tree -r --name-only refs/heads/fix/issue-7 | grep -q '^junk\.txt$'; then + fail "junk.txt from the verify stage reached the PR branch" + fi +} + test_reviewer_commit_is_discarded() { # A reviewer that commits to the fix branch must not get those commits # pushed: the branch is pinned back to the commit the diff was taken from. @@ -189,7 +204,8 @@ TESTS=(test_happy_path test_false_positive test_review_reject test_check_gate test_dry_run_read_only test_crash_labels_blocked test_agy_full_pipeline test_gemini_rejected_with_migration_hint test_agy_internal_timeout_is_blocked - test_verify_residue_is_stashed test_reviewer_commit_is_discarded) + test_verify_residue_is_stashed test_verify_commit_is_discarded + test_reviewer_commit_is_discarded) for t in "${TESTS[@]}"; do CURRENT="$t" diff --git a/tests/stubs/agent b/tests/stubs/agent index 3fdd0c2..6392812 100755 --- a/tests/stubs/agent +++ b/tests/stubs/agent @@ -80,6 +80,15 @@ case "$scenario:$stage" in fi ;; verifydirty:review) echo "DONE-APPROVED" ;; + verifycommit:verify) + ( cd "$project" && echo "scratch" > junk.txt && git add junk.txt \ + && git commit -q -m "rogue verify commit" ) + echo "DONE-PROCEED" ;; + verifycommit:fix) + do_fix_commit + echo "DONE-FIX-APPLIED" ;; + verifycommit:review) + echo "DONE-APPROVED" ;; reviewcommit:verify) echo "DONE-PROCEED" ;; reviewcommit:fix) From c6277c55be89f9360e686b39a492cd0d5d0432b6 Mon Sep 17 00:00:00 2001 From: Codevena Date: Fri, 12 Jun 2026 13:40:54 +0100 Subject: [PATCH 12/13] fix: run verify-stage cleanup on every outcome, not only PROCEED --- CHANGELOG.md | 10 +++---- SHA256SUMS | 2 +- fixbuddy.sh | 62 +++++++++++++++++++++++++------------------- tests/integration.sh | 15 +++++++++++ tests/stubs/agent | 5 ++++ 5 files changed, 62 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b9ef78..a83f70b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,11 +30,11 @@ test suite. ### Security & robustness - The two read-only-by-contract stages are now guarded deterministically (no - agent CLI offers an enforced read-only mode): worktree files written during - **verify** are stashed and commits it creates on the base branch are - discarded before the fix branch is created, and the **review** branch is - pinned to the reviewed commit — commits a reviewer creates are discarded, so - only the reviewed commit is ever pushed. + agent CLI offers an enforced read-only mode): after **every** verify outcome + (proceed, false positive, blocked, crash) worktree files the verify agent + wrote are stashed and commits it created on the base branch are discarded, + and the **review** branch is pinned to the reviewed commit — commits a + reviewer creates are discarded, so only the reviewed commit is ever pushed. ## [0.5.0] - 2026-06-12 diff --git a/SHA256SUMS b/SHA256SUMS index dcf83f4..b2e97c0 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,2 +1,2 @@ -098c5e3e0470db41c947a8cc98b46c1d131d7c0d6aa45540015921f3a26c8ca1 fixbuddy.sh +1924ebc408dd9e845cbfca518040f73e59c7762b9cbe06cdac62fb92d3510735 fixbuddy.sh ba3ba121a60ecb37fc5ca796f3cf5279a24a54566aeb2bb0dca3c3a1f4b7bd2f fixbuddy-wizard.sh diff --git a/fixbuddy.sh b/fixbuddy.sh index eb25fb7..0742c8a 100755 --- a/fixbuddy.sh +++ b/fixbuddy.sh @@ -554,6 +554,38 @@ run_agent() { # -------- Crash handling helpers -------- is_crash() { [ "$1" -eq 124 ] || [ "$1" -eq 125 ]; } +# Verify is contractually read-only, but no agent CLI enforces that (agy's +# --sandbox still allows workspace writes; claude/codex/opencode run with +# permission checks skipped). The tree was clean at startup, so anything dirty +# after the verify agent returns is verify residue. Called for EVERY verify +# outcome (proceed, false-positive, blocked, missing marker, crash) so no +# return path leaves residue in the operator checkout. +# +# Args: issue_num, pre-verify base-ref commit (may be empty) +cleanup_verify_residue() { + local num="$1" pre_base="$2" + if [ -n "$(cd "$PROJECT" && git status --porcelain 2>/dev/null)" ]; then + warn "[#$num] verify stage left worktree changes — stashing residue" + (cd "$PROJECT" && git stash push --include-untracked -m "fixbuddy-verify-residue-$num-$(ts)" --quiet) >/dev/null 2>&1 || true + fi + # Pin the base ref back if the verify stage committed on it (a commit leaves + # the worktree clean, so the stash above cannot catch it). Runs AFTER the + # stash so a reset never touches uncommitted files; the discarded commits + # stay recoverable via the reflog. + if [ -n "$pre_base" ] \ + && [ "$(cd "$PROJECT" && git rev-parse "refs/heads/$BASE_BRANCH" 2>/dev/null)" != "$pre_base" ]; then + warn "[#$num] verify stage created commits on $BASE_BRANCH — resetting to pre-verify state" + ( + cd "$PROJECT" || exit 0 + if [ "$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" = "$BASE_BRANCH" ]; then + git reset --hard "$pre_base" >/dev/null 2>&1 + else + git branch -f "$BASE_BRANCH" "$pre_base" >/dev/null 2>&1 + fi + ) || true + fi +} + cleanup_branch() { local num="$1" branch="$2" reason="$3" [ -n "$branch" ] || return 0 @@ -901,6 +933,10 @@ process_issue() { out=$(run_agent "$FIX_AGENT" "$(verify_prompt "$num" "$title" "$body")" "$issue_log" verify) rc=$? + # Runs before any outcome handling so every return path (crash included) + # leaves the operator checkout clean. + cleanup_verify_residue "$num" "$pre_verify_base" + if is_crash "$rc"; then handle_agent_crash "$num" "verify" "$rc" "" return 0 @@ -939,32 +975,6 @@ The \`fix:needs-human\` label has been applied. This issue requires human attent return 0 fi - # Verify is contractually read-only, but no agent CLI enforces that (agy's - # --sandbox still allows workspace writes; claude/codex/opencode run with - # permission checks skipped). The tree was clean at startup, so anything - # dirty here is verify-stage residue — stash it so it can never leak into - # the fix branch or commit. Recoverable via `git stash list`. - if [ -n "$(cd "$PROJECT" && git status --porcelain 2>/dev/null)" ]; then - warn "[#$num] verify stage left worktree changes — stashing residue" - (cd "$PROJECT" && git stash push --include-untracked -m "fixbuddy-verify-residue-$num-$(ts)" --quiet) >/dev/null 2>&1 || true - fi - - # Pin the base ref back if the verify stage moved it (committed on the base - # branch). Runs AFTER the stash above so a reset never touches uncommitted - # files; the discarded commits stay recoverable via the reflog. - if [ -n "$pre_verify_base" ] \ - && [ "$(cd "$PROJECT" && git rev-parse "refs/heads/$BASE_BRANCH" 2>/dev/null)" != "$pre_verify_base" ]; then - warn "[#$num] verify stage created commits on $BASE_BRANCH — resetting to pre-verify state" - ( - cd "$PROJECT" || exit 0 - if [ "$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" = "$BASE_BRANCH" ]; then - git reset --hard "$pre_verify_base" >/dev/null 2>&1 - else - git branch -f "$BASE_BRANCH" "$pre_verify_base" >/dev/null 2>&1 - fi - ) || true - fi - # ---- Stage 2+3: FIX + REVIEW (with retry) ---- local feedback="" attempt=0 approved=false while [ "$attempt" -le "$MAX_RETRIES" ]; do diff --git a/tests/integration.sh b/tests/integration.sh index f759861..abd9426 100755 --- a/tests/integration.sh +++ b/tests/integration.sh @@ -172,6 +172,20 @@ test_verify_residue_is_stashed() { assert_no_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:needs-human' } +test_verify_residue_cleaned_on_early_return() { + # The verify guards must run on EVERY outcome, not only PROCEED: here the + # verify agent dirties the tree AND commits on base, then reports a false + # positive. The operator checkout must come out clean regardless. + SCENARIO=fpdirty; make_fixture + run_fixbuddy --auto-merge + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_grep "$MUTLOG" '^issue close 7' + [ -z "$(git -C "$TMP/project" status --porcelain)" ] \ + || fail "verify residue left in the worktree" + [ "$(git -C "$TMP/project" rev-parse refs/heads/main)" = "$(git -C "$TMP/origin.git" rev-parse refs/heads/main)" ] \ + || fail "verify commit left on the base branch" +} + test_verify_commit_is_discarded() { # A verify agent that COMMITS (the worktree stays clean, so the residue # stash cannot catch it) must not get that commit into the fix branch: the @@ -205,6 +219,7 @@ TESTS=(test_happy_path test_false_positive test_review_reject test_check_gate test_agy_full_pipeline test_gemini_rejected_with_migration_hint test_agy_internal_timeout_is_blocked test_verify_residue_is_stashed test_verify_commit_is_discarded + test_verify_residue_cleaned_on_early_return test_reviewer_commit_is_discarded) for t in "${TESTS[@]}"; do diff --git a/tests/stubs/agent b/tests/stubs/agent index 6392812..9e830a3 100755 --- a/tests/stubs/agent +++ b/tests/stubs/agent @@ -62,6 +62,11 @@ case "$scenario:$stage" in echo "DONE-REJECTED: the fix lacks a regression test" ;; falsepos:verify) echo "DONE-FALSE-POSITIVE: the code already behaves correctly" ;; + fpdirty:verify) + ( cd "$project" && echo "scratch" > junk.txt \ + && echo "more" >> src/app.txt \ + && git add junk.txt && git commit -q -m "rogue verify commit" ) + echo "DONE-FALSE-POSITIVE: stale finding" ;; crash:verify) echo "transport error: connection reset" exit 1 ;; From 6cf8f06b89e9847dc78a94161b2fef10f3136c35 Mon Sep 17 00:00:00 2001 From: Codevena Date: Fri, 12 Jun 2026 13:45:05 +0100 Subject: [PATCH 13/13] fix: stash reviewer worktree residue on every review outcome; bash -n per file in CI --- .github/workflows/ci.yml | 6 +++++- SHA256SUMS | 2 +- fixbuddy.sh | 21 ++++++++++++++++++++- tests/integration.sh | 14 +++++++++++++- tests/stubs/agent | 8 ++++++++ 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2480b7..1fd4624 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,11 +15,15 @@ jobs: - name: Checkout uses: actions/checkout@v5 + # One file per invocation — `bash -n a b c` only checks the first file + # (the rest become positional parameters). - name: Bash syntax run: | bash -n fixbuddy.sh bash -n fixbuddy-wizard.sh - bash -n tests/integration.sh tests/stubs/agent tests/stubs/gh + bash -n tests/integration.sh + bash -n tests/stubs/agent + bash -n tests/stubs/gh - name: Install ShellCheck run: sudo apt-get update && sudo apt-get install -y shellcheck diff --git a/SHA256SUMS b/SHA256SUMS index b2e97c0..22d8e0d 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,2 +1,2 @@ -1924ebc408dd9e845cbfca518040f73e59c7762b9cbe06cdac62fb92d3510735 fixbuddy.sh +b63b3e549ddb30f154482abf585313d56edd281e2493d878b6f154693c1d1674 fixbuddy.sh ba3ba121a60ecb37fc5ca796f3cf5279a24a54566aeb2bb0dca3c3a1f4b7bd2f fixbuddy-wizard.sh diff --git a/fixbuddy.sh b/fixbuddy.sh index 0742c8a..f488d03 100755 --- a/fixbuddy.sh +++ b/fixbuddy.sh @@ -1107,8 +1107,27 @@ The \`fix:needs-human\` label has been applied. This issue will not be retried a (cd "$PROJECT" && git reset --hard "$review_head") >/dev/null 2>&1 || true fi + # Reviewer worktree residue (the tree was clean or stashed before review, + # so any dirt now is the reviewer's). Stash it on every outcome: on REJECT + # the retry would otherwise recreate the fix branch on a dirty tree (abort + # as fix:needs-human, or leak residue into the next attempt). + if [ -n "$(cd "$PROJECT" && git status --porcelain 2>/dev/null)" ]; then + warn "[#$num] review stage left worktree changes — stashing residue" + (cd "$PROJECT" && git stash push --include-untracked -m "fixbuddy-review-residue-$num-$(ts)" --quiet) >/dev/null 2>&1 || true + fi + if [ "$did_stash" = "1" ]; then - (cd "$PROJECT" && git stash pop --quiet) >/dev/null 2>&1 || warn "[#$num] 'git stash pop' failed — check 'git stash list'" + # The residue stash above may sit on top of the stack — pop the + # pre-review stash by its exact message, not blindly stash@{0}. + ( + cd "$PROJECT" || exit 1 + sid=$(git stash list 2>/dev/null | grep -m1 "fixbuddy-review-${num}\$" | cut -d: -f1) + if [ -n "$sid" ]; then + git stash pop --quiet "$sid" + else + git stash pop --quiet + fi + ) >/dev/null 2>&1 || warn "[#$num] 'git stash pop' failed — check 'git stash list'" fi if is_crash "$rc"; then diff --git a/tests/integration.sh b/tests/integration.sh index abd9426..e272d4c 100755 --- a/tests/integration.sh +++ b/tests/integration.sh @@ -212,6 +212,18 @@ test_reviewer_commit_is_discarded() { || fail "rogue reviewer commit was pushed" } +test_reviewer_residue_cleaned_before_retry() { + # A reviewer that modifies tracked files and then REJECTS must not poison + # the retry: without cleanup the fix-branch recreation fails on the dirty + # tree (fix:needs-human) or the residue leaks into the next fix attempt. + SCENARIO=reviewdirty; make_fixture + run_fixbuddy --auto-merge + [ "$RC" -eq 0 ] || fail "exit code $RC" + assert_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:rejected' + assert_no_grep "$MUTLOG" '^issue edit 7 .*--add-label fix:needs-human' + [ "$(grep -c '^claude:fix$' "$STAGELOG")" -eq 2 ] || fail "expected 2 fix attempts" +} + # ---------------- Runner ---------------- TESTS=(test_happy_path test_false_positive test_review_reject test_check_gate @@ -220,7 +232,7 @@ TESTS=(test_happy_path test_false_positive test_review_reject test_check_gate test_agy_internal_timeout_is_blocked test_verify_residue_is_stashed test_verify_commit_is_discarded test_verify_residue_cleaned_on_early_return - test_reviewer_commit_is_discarded) + test_reviewer_commit_is_discarded test_reviewer_residue_cleaned_before_retry) for t in "${TESTS[@]}"; do CURRENT="$t" diff --git a/tests/stubs/agent b/tests/stubs/agent index 9e830a3..145f667 100755 --- a/tests/stubs/agent +++ b/tests/stubs/agent @@ -103,6 +103,14 @@ case "$scenario:$stage" in ( cd "$project" && echo "rogue" >> src/app.txt && git add src/app.txt \ && git commit -q -m "rogue reviewer commit" ) echo "DONE-APPROVED" ;; + reviewdirty:verify) + echo "DONE-PROCEED" ;; + reviewdirty:fix) + do_fix_commit + echo "DONE-FIX-APPLIED" ;; + reviewdirty:review) + ( cd "$project" && echo "reviewer scratch" >> src/app.txt ) + echo "DONE-REJECTED: needs work" ;; *) echo "DONE-BLOCKED: unexpected scenario '$scenario' at stage '$stage'" exit 0 ;;