Files
amethyst/tools/buzz-agent/README.md
T
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

134 lines
6.7 KiB
Markdown

# 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`](../../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) jobs** — `agent-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:
```bash
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:
```bash
amy buzz agent doctor --repo /path/to/your/checkout # gh token scope + branch protection + clean tree
```
## Usage (manual / direct path)
```bash
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
```bash
./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
```bash
./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.