# AGENTS.md ## Project Overview teabot is a Go daemon (systemd **user** service) that watches Gitea repositories and dispatches **one-shot Claude Code sessions in Docker containers** to work issues and review pull requests. It polls the Gitea API for new issues, pull requests, and comments; for each event it runs a throwaway container that clones the repo and runs `claude --print` with a task-specific prompt, acting as a configurable bot **personality**. ## Structure ``` main.go # package main; wires version into internal/cli internal/cli/ # cobra command tree: run, config init/show internal/config/ # config.yaml + tea-config parsing, defaults, validation internal/gitea/ # read-only Gitea REST client (issues/pulls/comments/diff) internal/state/ # processed-event persistence (JSON, atomic writes) internal/prompt/ # per-event prompt construction (issue/pull/follow-up) internal/dispatch/ # poll loop, event filtering, loop prevention, job orchestration internal/docker/ # containerised Claude execution behind a Runner interface docs/ # architecture + configuration + per-subcommand docs packaging/nfpm.yaml # RPM spec (binary + completions + systemd unit + example config) scripts/build-rpm.sh # generates completions + packages the RPM with nfpm systemd/teabot.service # systemd user unit config.example.yaml # example config (also embedded for `config init`) .woodpecker/ # CI: build, test, pre-commit (PR) + release (tag) ``` ## Build / test ```bash make build # -> dist/teabot (CGO disabled, static) make test # go test -race ./... make rpm # build + package RPM (needs nfpm) ``` Requires Go 1.25+. Deps: `github.com/spf13/cobra`, `gopkg.in/yaml.v3`. ## Design notes for contributors - **Loop prevention is load-bearing.** Any event authored by a personality's Gitea username is skipped (but recorded). Never remove this — it is what stops the bot reacting to its own PRs/comments forever. - **Author allowlist is a security control.** Jobs run `--dangerously-skip-permissions` with a prompt built from event text, so teabot only dispatches for authors in `allowed_authors` (optional per-repo override). Fail-closed: empty list dispatches nothing. Comment follow-ups gate on the *new comment's* author, not just the acted-on thread. Skipped events are marked processed-only (never acted-on). - **Dedup before dispatch.** Issues/PRs/comments are marked processed in the state store *before* their job starts, so a re-poll or mid-job restart cannot double-launch. State lives at `~/.local/state/teabot/state.json`. - **First-contact seeding.** The first poll of a repo records existing open items as processed without dispatching, so a fresh install doesn't stampede. - **Follow-ups only on acted threads.** Comment follow-ups fire only when teabot previously opened a PR for / reviewed the parent issue/PR. - **Docker is behind `docker.Runner`.** All dispatch logic is tested with a fake runner; no Docker daemon is needed for `go test`. `DockerRunner.buildArgs` is pure given a job dir so the `docker run` argument list is unit-tested directly. - **Personalities use tea configs.** tea persists to `$XDG_CONFIG_HOME/tea` and has no `--config` flag, so each personality's config is mounted into the container and `XDG_CONFIG_HOME` is set there. - **Image reuse.** The default `job_image` is `agent-dev`, which already ships the claude CLI + dev toolchain; teabot does not build its own image. ## Testing conventions Every package has meaningful unit tests (no rubber-stamps): config precedence & validation & tea parsing, state persistence/dedup roundtrips, gitea client via `httptest`, prompt content assertions, docker arg/mount/env construction, and dispatch filtering/loop-prevention/seeding/follow-up routing via a fake client + fake runner. `go build ./...` and `go test -race ./...` must pass. ## Conventions (house rules) - Branches `benvin/`. PR bodies: short "why" paragraph + present-tense "how" bullets. HTTPS remotes for git.unkin.net (SSH blocked). Use `tea`, not `gh`. Do not merge PRs yourself. - Releases are Woodpecker pipelines triggered on `v*` tags; bump with `make patch|minor|major`. Every CI step sets k8s resource requests+limits.