Current state had grown into a running log of two adjudication cycles. The 2026-07-29 heartbeat drop is a decision, so it lives in Decisions with its reopen condition; the per-repo deploy-key requirement is a durable gotcha, so it sits with the Deploy command. What remains is present-tense status plus three ordered next steps: the orphaned-thread prune fix, triaging this repo's 3 inbox items, and the 2nd/3rd-repo poll-cycle check.
124 lines
8.6 KiB
Markdown
124 lines
8.6 KiB
Markdown
# 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 one click on
|
||
this bot's tile in Spark Control's Bots tab** (shipped since spark-control v0.28.0, confirmed live:
|
||
status badge, Update, Start/Restart/Stop, View logs; the tile is hidden unless `GITEA_REVIEW_USER`
|
||
is set in its Settings). `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.
|
||
**Deploy-key gotcha:** each repo the Spark clones needs its *own* Gitea deploy key plus its own
|
||
`Host` alias — Gitea refuses to reuse one SSH key across two repos' deploy keys, and the default
|
||
`immense-voyage.local` block already carries matrix-bridge's key.
|
||
- **Onboard a repo:** create a Matrix room named like the repo, invite this bot → it auto-joins,
|
||
maps the room to `<owner>/<roomname>` (+ `~/Projects/<roomname>` 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 `<cmd> <n>` 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 that walks **all** mapped repos in **one async task, serially** (so rooms never interleave,
|
||
but a long review head-of-line-blocks the next room's poll: the scaling limit to watch at 3+ repos,
|
||
and what the deferred poll→webhook item addresses); 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.<name>]` 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 `<x>` → Gitea `<owner>/<x>` + `~/Projects/<x>`; mapping persists to
|
||
`state/` (mirrors matrix-bridge D14). One room per repo.
|
||
- **No wedge-detection heartbeat** (adjudicated 2026-07-29). A healthcheck badge restarts nothing
|
||
under this compose setup, the tile that would show it lives in spark-control (so it's a two-repo
|
||
change), and serial per-room polling with a 10-min review budget makes any staleness threshold
|
||
either too loose to be timely or noisy mid-review. Reopens only on an *observed* wedge **plus**
|
||
confirmation the spark-control tile already renders Docker health. The cheaper half of the same
|
||
goal is the ROADMAP fold-in: bound the unbounded awaits and die if the poll task dies, so
|
||
`restart: unless-stopped` handles it.
|
||
|
||
## 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 in steady use.** The bot runs on the Spark, maps each review room to its repo, and is proven
|
||
end-to-end on **ten31-site PR #4** (2026-06-28): threaded review → `merge` + `yes` → force_merge →
|
||
auto-publish → thread redacted, with headless `claude -p` genuinely spawning the reviewer / adjudicator /
|
||
security subagents. ten31-site is the only repo onboarded so far.
|
||
|
||
Running on the Spark at `063e694` (a merge always redacts its thread; a failed post-merge deploy warns at
|
||
top level rather than orphaning one; `clear`/`resolve` clears an orphan without touching the PR). `master`
|
||
is only docs ahead of that, so **no redeploy is pending**. No test suite: verification means exercising
|
||
the bot in a real review room.
|
||
|
||
**Next steps, in priority order:**
|
||
1. **Fix the orphaned-thread prune** (`src/bot.py` ~L594): the poll loop drops a PR's thread root from
|
||
state when the PR leaves the open list *without redacting*, so a PR merged or closed in the Gitea web
|
||
UI strands its thread and `clear` can no longer reach it. Redact before dropping the root.
|
||
2. **`/triage` the 3 untriaged inbox items** tagged for this repo — adjudication never reads the inbox, so
|
||
their content is still unknown here.
|
||
3. Onboard a 2nd/3rd repo, then watch one poll cycle (`docker compose logs -f`) for a long review
|
||
head-of-line-blocking the next room — ROADMAP Phase 3's verify item, and the real scaling limit.
|