The Deploy section claimed a Spark Control tile was "captured in the inbox" pending build. It isn't: spark-control has shipped this bot's tile since v0.28.0, verified live against the running dashboard. Deploy is one click. Record in Layout that the poll loop walks all mapped repos serially in one async task. That's why rooms can't interleave, and why a long review head-of-line-blocks the next room's poll at 3+ repos. Current state: prune the finished merge-redact narrative to a line, keep the stale-prune orphan as the concrete next step, and record the 2026-07-18 adjudication that cut ROADMAP to one phase.
121 lines
8.4 KiB
Markdown
121 lines
8.4 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.
|
||
- **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.
|
||
|
||
## 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.
|
||
|
||
Deployed to the Spark at `063e694`: a merge always redacts its thread on success, and a failed
|
||
post-merge deploy posts its "deploy manually" warning at top level so it survives the redaction
|
||
instead of orphaning the thread. `clear`/`resolve` 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. Redeploy via the Spark Control tile, or `ssh modelo32`.
|
||
|
||
**Known issue / next fix (the concrete next step):** 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 or closed
|
||
**outside** the bot (the Gitea web UI) still orphans its thread, un-redactable by the bot afterward.
|
||
The merge-redact fix covers bot-driven merges only; the prune should redact before dropping the root.
|
||
|
||
**Backlog adjudicated 2026-07-18** (`9adcdc8`): ROADMAP is down to one phase. Phase 2 (subagent panel,
|
||
`agents +/-`) deleted as finished; Phase 4 deleted because spark-control has shipped this bot's tile
|
||
since v0.28.0; deploy-on-a-non-static-repo dropped (no repo needs it, and the deploy step is hardcoded
|
||
to the static-site publish script, so a per-repo deploy-command field would have to come first).
|
||
Remaining: onboard 3 repos, and a heartbeat so a **wedged** poll loop is distinguishable from healthy
|
||
(the tile reads container state, so it already catches a crash but not a stuck loop).
|