# gitea-review-bot: AGENTS.md gitea-review-bot is a **thin Matrix bot that runs an AI PR-review-and-merge workflow for your Gitea repos**, one review room per repo. It is a *separate* bot from `matrix-bridge` (which turns Matrix messages into interactive Claude Code sessions): this one's single responsibility is **reviewing pull requests** — so every room it's in is a review room and there is no manual "ignore this room" wiring. Invite it to a room, it auto-joins and maps the room to a repo by name, then for that repo it polls Gitea PRs, posts a headless `claude -p` review as a Matrix thread, and lets you merge/reject from the thread. > **Inbox check:** At session start, if `~/Projects/standards/INBOX.md` exists, scan it for items > tagged `(gitea-review-bot)` and surface them before proposing next steps; triage with `/triage`. ## Stack Python + **matrix-nio**, one thin Docker container on the Spark (same shape as `matrix-bridge` and the `ten31-database` intake bot). No framework (Maubot rejected — see Decisions). The heavy work — the `claude -p` review and any deploy — runs **on the Mac over SSH**, reusing matrix-bridge's proven wrappers (`scripts/ask-claude.sh` for the review, `scripts/deploy-site.sh` for publish). State is flat JSON in a writable `state/` mount. ## Placement | Dimension | Call | |---|---| | Host | **Spark**, plain Docker container (NOT Start9/s9pk) | | Runtime | Long-running service: matrix-nio sync + a Gitea poll loop over mapped repos | | Model routing | `claude -p` on the Mac via the Spark→Mac SSH seam (subscription); the review session spawns subagents (reviewer / adjudicator / security-auditor) | | Data layer | Flat JSON in `state/` (room→repo map + enabled agents + reviewed-PR heads + thread roots) | | Interface | Matrix — one review room per repo (+ phone) | | Repo home | Local + Gitea (`ssh://git@immense-voyage.local:59916/grant/gitea-review-bot.git`) | | Sensitivity | Sends PR diffs to `claude -p` (subscription). Fine for code review; flag the boundary before pointing it at a sensitive repo. | ## Commands - **Run (container, on the Spark):** from `~/gitea-review-bot`, `docker compose up -d --build` (host networking, `restart: unless-stopped`; read-only mounts of `.env`/`config.toml`/SSH key, read-write `state/`). Logs: `docker compose logs -f`. - **Deploy:** the Spark's `~/gitea-review-bot` is a Gitea clone tracking `master`; deploy = `git fetch && git reset --hard origin/master && docker compose up -d --build` (or a Spark Control Update tile, once added — captured in the inbox). `config.toml` is gitignored — refresh it on the Spark separately (scp) like matrix-bridge. Reach the Spark from the Mac as **`ssh modelo32`** (the **NVIDIA Sync** alias — host `spark-32d0`, user `modelo`, key-based; injected into `~/.ssh/config` via an `Include`. `modelo27`/`spark-27ea` is a second Spark on another LAN). `reset --hard` is safe: `.env`/`config.toml`/`state/` are gitignored. - **Onboard a repo:** create a Matrix room named like the repo, invite this bot → it auto-joins, maps the room to `/` (+ `~/Projects/` on the Mac), and posts an onboarding message. Pick review agents in-chat: `agents +reviewer +security -adjudicator`. - **In a review room:** `merge` / `reject` / `clear` (inside a PR's thread, or ` ` by number), `yes`/`no` to confirm a merge, `agents …` to toggle the subagent panel. `clear` (alias `resolve`) redacts an orphaned thread without touching the PR. ## Layout - `src/bot.py` — the bot: matrix-nio sync; auto-join + in-room auto-map (room→repo by name); a Gitea poll loop per mapped repo; threaded `claude -p` review (subagent panel); merge/reject/deploy in-thread; whole-thread redaction on resolve (server-enumerated, restart-proof) — a merge always redacts (a failed post-merge deploy leaves a top-level warning, not an orphaned thread); `clear` redacts without touching the PR. - `config.example.toml` — homeserver, `[mac]` (ssh alias + the reused matrix-bridge wrapper paths), `[gitea]` (api_base/owner/verify_tls), `[defaults]`, optional `[repo.]` deploy overrides. - `.env.example` — `MATRIX_*` + `GITEA_TOKEN` (real `.env` gitignored). - `Dockerfile` · `docker-compose.yml` · `docker-entrypoint.sh` — the Spark container (generic image; secrets/config via read-only mounts; entrypoint writes `~/.ssh/config` for the Mac alias). - `state/` — gitignored runtime JSON (`rooms.json`: room map + agents + heads + thread roots). ## Decisions - **Separate bot from matrix-bridge** (single responsibility = PR review). Beat: extending matrix-bridge with review-room special-casing. Reopens if the two bots' logic heavily overlaps. - **Thin matrix-nio container, NOT Maubot.** Reevaluated 2026-06-28: Maubot helps with the Matrix plumbing we've already solved, not the SSH/`claude -p`/poll logic that's the actual weight, and it reintroduces a web-UI/management layer (Spark Control is the dashboard). Reopens at ~6+ bots or a non-developer web-management need; the lighter step first is a shared "bot kit" library. - **Subagent panel (Option B):** the lead `claude -p` session spawns subagents and presents each output + its own overall recommendation. Beat: bot-orchestrated separate `claude -p` runs (more deterministic but 3× the sessions + more bot code). Reopens if headless subagent spawning is flaky. - **Panel composition is per-room, set in chat** (onboarding message + `agents +/-`), not config. - **Reuse matrix-bridge's Mac wrappers + Spark→Mac SSH key** (don't duplicate the seam). - **Auto-map by name:** room `` → Gitea `/` + `~/Projects/`; mapping persists to `state/` (mirrors matrix-bridge D14). One room per repo. ## Sovereignty Reviews send PR diffs to `claude -p` (the subscription), not a frontier API on payload data; that's acceptable for code review of these repos. Before pointing the bot at a repo with sensitive content, revisit this — local inference via Spark Control would be the path. ## Current state **LIVE and proven.** Bot runs on the Spark (`docker compose up -d`), `@reviewer` maps each review room to its repo, and the subagent panel is configured in-chat. Proven end-to-end on **ten31-site PR #4** (2026-06-28): threaded review → `merge` + `yes` → force_merge → auto-publish → thread redacted — and the previously-unproven bit, **headless `claude -p` spawning the reviewer/adjudicator/security subagents, works**. The ten31-site colleague is onboarded; the pipeline is in steady use. **This session's hardening (DEPLOYED to the Spark at `063e694`):** a merge now **always redacts** the thread on merge success; a failed post-merge deploy posts its "deploy manually" warning at **top level** so it survives the redaction instead of orphaning the thread (the bug PR #4 hit when the Mac was left off `master`). New **`clear`/`resolve`** command redacts an orphaned thread without touching the PR. **Deploy gotcha (still true):** the Spark needs a *dedicated* per-repo Gitea deploy key + a `Host` alias (the default `immense-voyage.local` block uses matrix-bridge's key); Gitea won't reuse one SSH key across repos' deploy keys. A Spark Control tile is captured in the inbox. PR #4's orphaned thread was cleared this session by direct Matrix redaction (the bot couldn't — see the known issue below); future redeploys: `ssh modelo32`. **Known issue / next fix:** the poll loop prunes a PR's thread root from state when it leaves the open list (`src/bot.py` ~L594) *without redacting* — so a PR merged/closed **outside** the bot (e.g. the Gitea web UI) still orphans its thread, un-redactable by the bot afterward. The merge-redact fix covers bot-driven merges; the prune should redact the thread before dropping the root.