# forgebot K8s operator + API for AI agent dispatch from git forges. ## Architecture - **API server** (`cmd/api`) — REST API backed by PostgreSQL. Receives webhooks from Gitea, manages task lifecycle, and orchestrates the review workflow. - **Operator** (`cmd/operator`) — Kubernetes controller that watches for pending tasks, creates Jobs via AgentPool/AgentTask CRDs, and reports completion back to the API. - **TUI** (`cmd/tui`) — Terminal kanban board for viewing and managing tasks. ## Task Lifecycle Tasks follow a kanban workflow with automated review: ``` +-----------+ | Todo | +-----+-----+ | agent picks up | +-----v-------+ | In Progress | +-----+-------+ | agent completes | +-----------+-----------+ | | auto-create error? review task back to Todo | +-----v------+ | In Review |<-----+ +-----+------+ | | | human decision reviewer | suggests +----+----+ changes | | | +----v--+ +---v----+ | | Done | | Wontdo | | +-------+ +--------+ | | (new fix task in Todo) ``` Only humans can move tasks from In Review to Done/Wontdo. ## Quick Start ### API Server ```bash export DBHOST=localhost DBUSER=forgebot DBPASS=secret DBNAME=forgebot export GITEA_URL=https://git.unkin.net GITEA_TOKEN= make build ./bin/forgebot-api ``` ### Operator ```bash ./bin/forgebot-operator --api-url http://forgebot-api:8000 # or FORGEBOT_API_URL=http://forgebot-api:8000 ./bin/forgebot-operator ``` ### TUI ```bash ./bin/forgebot-tui -api http://localhost:8000 # or FORGEBOT_API_URL=http://forgebot-api:8000 ./bin/forgebot-tui ``` #### TUI Key Bindings | Key | Action | |-----|--------| | `h`/`l` or arrows | Move between columns | | `j`/`k` or arrows | Move within column | | `Enter` | Task detail view | | `e` | Edit task in $EDITOR | | `n` | Create new task | | `d` | Mark done (in review only) | | `w` | Mark wontdo (in review only) | | `r` | Refresh | | `/` | Filter by repository | | `?` | Toggle help | | `q` | Quit | ## API | Method | Path | Description | |--------|------|-------------| | `GET` | `/health` | Health check | | `GET` | `/api/v1/tasks` | List tasks (`?status=`, `?repository=`) | | `POST` | `/api/v1/tasks` | Create task | | `GET` | `/api/v1/tasks/{id}` | Get task | | `PATCH` | `/api/v1/tasks/{id}` | Update task status | | `POST` | `/api/v1/tasks/{id}/complete` | Agent completion callback (triggers review workflow) | | `POST` | `/api/v1/tasks/{id}/comment` | Post comment to forge | | `POST` | `/api/v1/webhook/gitea` | Gitea webhook receiver | ## Schema The SQL lives in `migrations/`, is embedded in the API binary and applied at startup before the server listens, so there is no mirrored copy in the deployment to drift out of sync. The runner is [`golib/pg`](https://git.unkin.net/unkin/golib)'s `pg.NewMigrated` — forgebot owns the SQL, the shared library owns the mechanics. Each start takes `pg_advisory_lock` on a fixed key (FNV-1a/64 of the lock name `forgebot-migrations`), creates `schema_migrations` (`version`, `applied_at`) if missing, and applies every embedded file whose filename is not yet recorded — in lexical (version) order, each file's SQL and its tracking row in one transaction — then unlocks. Replicas starting together queue on the lock and then find nothing to do. A file absent from `schema_migrations` is re-run even where the live database already has the schema, which is how a database created by the pre-`golib` runner is adopted. Migrations are therefore `IF NOT EXISTS`-guarded and their data fixups re-runnable. ## CRDs - **AgentPool** — Configuration for a pool of AI agents (model, concurrency, image, resources) - **AgentTask** — A task dispatched to a pool for execution - **ProviderQueue** — Polls the API for pending tasks and creates AgentTask CRs - **RepositoryBinding** — Links a repository to a queue and pool with access controls ## Building ```bash make build # all binaries to bin/ make test # run tests make lint # go vet make fmt # gofmt make generate # regenerate CRDs and RBAC make docker-api # build API container image make docker-operator # build operator container image ``` ### `GOPRIVATE` forgebot depends on `git.unkin.net/unkin/golib`, which is served by Gitea and is unknown to `proxy.golang.org` / `sum.golang.org`. Module resolution therefore needs: ``` export GOPRIVATE=git.unkin.net ``` The `Makefile` exports it for every target, and both Dockerfiles and the woodpecker Go steps set it themselves, so `make build|test|lint` and CI work on a clean checkout. Only bare `go` commands run outside `make` need it in your shell — set it there rather than with `go env -w`, which is machine state this repo cannot carry.