Recording the outcome of a design discussion so it is not lost. Not scheduled — future work, no action needed now.
The shape
Support several projects (tracked, one worked on at a time) across a mix of GitHub and Gitea, including more than one Gitea instance.
The abstraction that makes all of it fall out is a named forge connection: id, kind (gitea | github), base URL, credential, operator allowlist. A project references a connection by id rather than carrying host details.
The payoff: once connections exist, N instances of the same kind cost nothing — a second Gitea is a config row using the identical client. Only a new kind needs code.
Sizing, measured rather than guessed
Consumers touch only four client methods directly (open_pull_request, create_issue, list_comments, post_comment), plus find_pull_request via the A3 reconciler. gitea.py is 193 lines; git.py is already forge-neutral.
A ForgeClient protocol is a mechanical extraction. A GitHubClient is ~150–200 lines, because Gitea's API deliberately mirrors GitHub's.
Differences that would bite: Authorization: Bearer vs token, pagination via Link headers rather than a page parameter, and rate limiting surfacing as 403 with X-RateLimit-* rather than the 429 our retry currently keys on.
Three consequences worth remembering
The operator allowlist becomes per-connection, and that is correctness. A4 already says allowlists are per channel; a Gitea login on instance A need not match instance B and certainly will not match a GitHub handle. A global list would let the wrong forge's identity satisfy a gate.
A7 (Bitwarden) starts pulling its weight. One token in .env was fine; several tokens across several hosts is precisely what the secrets client exists for.
The project registry becomes curated configuration. With a single Gitea, "which projects exist" was derivable from the forge — external truth. With several forges there is no single place to ask, so the roster becomes operational config. (Knowledge about projects still belongs in A17; the roster itself does not.)
Where the effort actually goes
Not the forge layer. A2 requires "per-project state-machine position", and agent_state is implemented as a single row (id = 1). That is sufficient while only one project is ever known, but tracking several and switching between them means keying state by project — a schema migration plus a modest orchestrator change. This is independent of GitHub and is the larger half of the work.
Non-code: a GitHub identity for the agent, an egress hole per forge host in B3's sealed allowlist, and branch protection per repo — where GitHub forbids self-approval outright, giving that property for free.
Possible directions
A config file listing connections and projects (small), or a web configuration interface (a materially larger scope).
Recording the outcome of a design discussion so it is not lost. **Not scheduled** — future work, no action needed now.
### The shape
Support several projects (tracked, one worked on at a time) across a mix of GitHub and Gitea, including more than one Gitea instance.
The abstraction that makes all of it fall out is a **named forge connection**: id, kind (`gitea` | `github`), base URL, credential, operator allowlist. A project references a connection by id rather than carrying host details.
The payoff: once connections exist, **N instances of the same kind cost nothing** — a second Gitea is a config row using the identical client. Only a new *kind* needs code.
### Sizing, measured rather than guessed
- Consumers touch only four client methods directly (`open_pull_request`, `create_issue`, `list_comments`, `post_comment`), plus `find_pull_request` via the A3 reconciler. `gitea.py` is 193 lines; `git.py` is already forge-neutral.
- A `ForgeClient` protocol is a mechanical extraction. A `GitHubClient` is ~150–200 lines, because Gitea's API deliberately mirrors GitHub's.
- Differences that would bite: `Authorization: Bearer` vs `token`, pagination via `Link` headers rather than a `page` parameter, and rate limiting surfacing as **403 with `X-RateLimit-*`** rather than the 429 our retry currently keys on.
### Three consequences worth remembering
1. **The operator allowlist becomes per-connection, and that is correctness.** A4 already says allowlists are per channel; a Gitea login on instance A need not match instance B and certainly will not match a GitHub handle. A global list would let the wrong forge's identity satisfy a gate.
2. **A7 (Bitwarden) starts pulling its weight.** One token in `.env` was fine; several tokens across several hosts is precisely what the secrets client exists for.
3. **The project registry becomes curated configuration.** With a single Gitea, "which projects exist" was derivable from the forge — external truth. With several forges there is no single place to ask, so the roster becomes operational config. (Knowledge *about* projects still belongs in A17; the roster itself does not.)
### Where the effort actually goes
Not the forge layer. **A2 requires "per-project state-machine position", and `agent_state` is implemented as a single row (`id = 1`).** That is sufficient while only one project is ever known, but tracking several and switching between them means keying state by project — a schema migration plus a modest orchestrator change. This is independent of GitHub and is the larger half of the work.
Non-code: a GitHub identity for the agent, an egress hole per forge host in B3's sealed allowlist, and branch protection per repo — where GitHub forbids self-approval outright, giving that property for free.
### Possible directions
A config file listing connections and projects (small), or a web configuration interface (a materially larger scope).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Recording the outcome of a design discussion so it is not lost. Not scheduled — future work, no action needed now.
The shape
Support several projects (tracked, one worked on at a time) across a mix of GitHub and Gitea, including more than one Gitea instance.
The abstraction that makes all of it fall out is a named forge connection: id, kind (
gitea|github), base URL, credential, operator allowlist. A project references a connection by id rather than carrying host details.The payoff: once connections exist, N instances of the same kind cost nothing — a second Gitea is a config row using the identical client. Only a new kind needs code.
Sizing, measured rather than guessed
open_pull_request,create_issue,list_comments,post_comment), plusfind_pull_requestvia the A3 reconciler.gitea.pyis 193 lines;git.pyis already forge-neutral.ForgeClientprotocol is a mechanical extraction. AGitHubClientis ~150–200 lines, because Gitea's API deliberately mirrors GitHub's.Authorization: Bearervstoken, pagination viaLinkheaders rather than apageparameter, and rate limiting surfacing as 403 withX-RateLimit-*rather than the 429 our retry currently keys on.Three consequences worth remembering
.envwas fine; several tokens across several hosts is precisely what the secrets client exists for.Where the effort actually goes
Not the forge layer. A2 requires "per-project state-machine position", and
agent_stateis implemented as a single row (id = 1). That is sufficient while only one project is ever known, but tracking several and switching between them means keying state by project — a schema migration plus a modest orchestrator change. This is independent of GitHub and is the larger half of the work.Non-code: a GitHub identity for the agent, an egress hole per forge host in B3's sealed allowlist, and branch protection per repo — where GitHub forbids self-approval outright, giving that property for free.
Possible directions
A config file listing connections and projects (small), or a web configuration interface (a materially larger scope).