Files
gitea-review-bot/AGENTS.md
T
Keysat 907b83388b Adjudicate 2026-07-29: drop the wedged-loop heartbeat, add a hang-bounding fold-in
Heartbeat dropped: a Docker healthcheck badge restarts nothing under this
container's setup, spark-control's tile would need its own change to read it,
and serial per-room polling (a review may run 10 min) makes any staleness
threshold either too loose to be timely or noisy mid-review. No wedge has ever
been observed. Reopens on an observed wedge plus confirmation the tile already
renders Docker health.

Replaced by a fold-in, since the realistic failure is a hang rather than a
crash: bound the two unbounded awaits in src/bot.py and stop the process if the
poll task ever dies, letting restart: unless-stopped and the existing
spark-control tile cover it.

AGENTS.md Current state reconciled to match. Phase 3's onboard-3-repos verify
item stands (inside the 14-day skip window).
2026-07-29 20:09:42 -05:00

9.0 KiB
Raw Blame History

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.exampleMATRIX_* + 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). That left two items: onboard 3 repos, and a wedged-loop heartbeat.

Backlog adjudicated 2026-07-29: the heartbeat is dropped — a healthcheck badge doesn't restart anything here, spark-control's tile would need its own change to read it, and serial per-room polling (a review may run 10 min) makes any staleness threshold either too loose to be timely or noisy mid-review; no wedge has ever been observed. Reopens only on an observed wedge plus confirmation the tile already renders Docker health. In its place ROADMAP carries a fold-in: bound the two unbounded awaits in src/bot.py (await proc.wait() after the timeout kill in ssh_run; client.room_send in say()) and stop the process if the poll task ever dies, so restart: unless-stopped + the tile cover it. Phase 3's onboard-3-repos verify item stands.