Files
davotoulaandClaude Opus 5 592fd8f542 test: pin the amy argument surface the buzz-agent path depends on
Flag names and error strings are the contract between amy and the wrapper
scripts (and their operators), but nothing type-checks them: renaming --base-ref
or rewording an error breaks callers while compiling clean. This harness pins the
ones the buzz-agent path drives.

14 cases, all offline against an isolated ~/.amy via the `amy.home` seam the JVM
tests use: the shared `repo-naddr-or-coordinates` positional on git
browse/cat/log, `pass --channel GID` on the workflow verbs, --base-ref accepted
while --base-reff is rejected on both agent up and agent serve, and the
no_relays detail.

Confirmed to discriminate: 14/14 on this branch, 10/14 when built against
main-upstream's BuzzCommands, with the four failures printing the old misleading
"no relays: pass --relays ws://…" for input the user did pass.

Worth recording from writing it: a bare word like `garbage` is *accepted* as a
relay (the normalizer prepends wss://), so the realistic trigger for the empty
set is a pasted http:// url — which is what the cases use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WopdqNoZ9tYoMNppJG17BL
2026-07-29 15:42:48 +02:00
..
2026-07-29 14:14:28 +02:00

Buzz agent --exec wrapper

agent-exec.sh is the reference command you point amy buzz agent serve --exec at to turn Buzz job requests into pull requests. It runs a coding agent (Claude Code by default) on the task inside the job's isolated git worktree, then commits, pushes the job branch, and opens a PR — printing the PR URL as the job result. It never touches the default branch and never force-pushes; the merge is a human action on GitHub.

See the design in cli/plans/2026-07-25-buzz-agent-support-channel.md.

The contract (how the scheduler calls it)

Channel Meaning
stdin the task text (the kind-43001 request)
cwd the job's git worktree — a fresh branch off --base-ref
env BUZZ_JOB_ID BUZZ_REQUESTER BUZZ_CHANNEL BUZZ_RELAY BUZZ_AGENT BUZZ_UPVOTES BUZZ_BRANCH BUZZ_WORKTREE BUZZ_BASE_REF
stdout becomes the job result (kind-43004) — the wrapper prints the PR URL
non-zero exit becomes the job error (kind-43006); stderr is the detail

Its steps: read task → run agent → verify a diff exists → commit → push the branch → open (or reuse) the PR → print the URL.

Two paths: gated vs direct

  • Direct (ungated) jobsagent-exec.sh (this file's subject) does agent → commit → push → PR in one shot. Point amy buzz agent serve at it.
  • Gated workflow runs — the work pauses on a human-approval gate before anything ships, so it's split into two steps around the gate: workflow-agent.sh (the --exec step: agent → commit, no push) and workflow-ship.sh (the --on-approve step: push → PR, runs only after a human grants). Point amy buzz workflow run at them.

The easy button: amy buzz agent up

For the gated path you don't need to wire any of this by hand. amy bundles workflow-agent.sh + workflow-ship.sh and runs them for you:

amy buzz agent up wss://your-buzz-relay --repo /path/to/your/checkout --approver npub1you…

It resolves the channel (the relay's only one, or pass --channel), defaults the worktree to --repo, scopes intake to the channel roster (--accept-from-channel), and extracts the wrappers to ~/.amy/buzz-agent/ (edit them there to customize, or pass your own --exec/--on-approve). Before first run, check the host is safe:

amy buzz agent doctor --repo /path/to/your/checkout   # gh token scope + branch protection + clean tree

Usage (manual / direct path)

amy buzz agent serve wss://your-buzz-relay <channel-uuid> \
  --exec /path/to/tools/buzz-agent/agent-exec.sh \
  --worktree /path/to/amethyst \
  --accept-from-channel \
  --parallel 2

The bundled copies agent up extracts live in cli/src/main/resources/buzz-agent/; the copies here in tools/buzz-agent/ are the readable, customizable reference (same content).

--worktree is required (the wrapper needs BUZZ_BRANCH/BUZZ_WORKTREE). --parallel N runs N jobs at once, each in its own worktree+branch.

Config knobs (env)

Var Default Purpose
AGENT_CMD (unset) Override the whole agent invocation. Receives the prompt on stdin and as $AGENT_PROMPT. Use this for Goose/Codex or a custom runner.
AGENT_ALLOWED_TOOLS Edit,Write,Read,Bash,Glob,Grep Claude Code --allowedTools — the agent's capability boundary.
COMMIT_PREFIX feat Prefix for the auto-commit subject when the agent didn't commit.

Adjust the default claude -p … --permission-mode acceptEdits --allowedTools … line to match your Claude Code version, or bypass it entirely with AGENT_CMD.

Security — the guardrails that make this safe

Buzz authorizes by identity, not capability flags, so none of the "can't merge or destroy main, can't code other things" guarantees come from Buzz. They come from three things you configure here:

  1. A PR-only git credential. Authenticate gh on the agent host with a fine-grained PAT scoped to the amethyst repo only, granting exactly Contents: Read and write + Pull requests: Read and write — and nothing else. No Administration, no org scope. The token can open PRs on feature branches; it cannot merge, bypass checks, or reach other repos.
  2. Branch protection on the default branch. Protect main: require a pull request, require review approval, require status checks (CI) to pass, and block force-pushes and branch deletion. Do not grant the bot an exception. This is what actually stops a bad merge — the wrapper only ever pushes claude/job-* feature branches.
  3. Scoped intake + agent tools. Run the scheduler with --accept-from / --accept-from-channel so only your team's keys can file jobs, keep AGENT_ALLOWED_TOOLS tight, and run on a host whose checkout is amethyst only — so the agent "can't code other things."

The merge is deliberately outside this loop: a completed job's result is its PR, and a human merges it on GitHub.

Testing

./test-workflow-ship.sh — the ship step, end to end

./tools/buzz-agent/test-workflow-ship.sh                    # the sibling workflow-ship.sh
./tools/buzz-agent/test-workflow-ship.sh /path/to/other.sh  # or any other copy

Runs the real script against a throwaway repo with a local bare origin (so git push genuinely pushes) and a stub gh each case configures. No network, no GitHub, no credentials; exits non-zero on the first failing case. It covers the happy path, PR reuse, all four default-branch refusals, the empty-worktree failure mode, and base_branch resolution when gh answers oddly (null, empty, or non-zero) — the paths that otherwise only fail on someone else's machine, unattended, via a kind-46007 whose stderr is the only clue.

./test-amy-args.sh — the amy argument surface

./gradlew :cli:installDist
./tools/buzz-agent/test-amy-args.sh
AMY=/path/to/amy ./tools/buzz-agent/test-amy-args.sh   # or point at another build

Pins the flag names and error strings the wrappers and their operators key on — --base-ref, the repo-naddr-or-coordinates positional, pass --channel GID, and the no_relays detail. A renamed flag or reworded message breaks callers without breaking any compile, and none of it is covered by a type. Runs offline against an isolated ~/.amy (every case is rejected before a relay is dialled).

agent-exec.sh

Verified end-to-end against a throwaway repo with a stubbed gh + agent (reads task → runs agent → commits the diff → pushes the feature branch → prints the PR URL; and errors cleanly when the agent makes no changes). Point AGENT_CMD at a stub to dry-run the git/PR plumbing without invoking a real agent.