### Fixed - The release doors run on a Forgejo consumer. `lib/facts.sh` and `release.yml` gathered and published through `gh`, which the runner image does not ship, so the merge door read `labeled=no` for a correctly labeled ceremony PR and the tag door died at the publish (#191). - A release fact that could not be read is no longer reported as a definite `no`. A completed read finding no label is still `no` and still fail-closed; a read that did not complete refuses and emits no fact (#191). ### Added - `forge_release_exists`, `forge_commit_pulls`, `forge_tag_create`, `forge_release_create` and `forge_pr_create` on both backends, so the release path names no client (#191). - The forgejo backend serves one PR object at `/commits/{sha}/pull` where GitHub serves an array at `/pulls`; both verbs emit the array shape, so the call site carries one expression (#191). - Forgejo creates tags at `POST /tags` — it serves `/git/refs` GET-only, so GitHub's ref-POST would have 404'd there forever (#191). - `forgejo_api_base` refuses when `REPO` is empty. Every verb interpolates it and every call reaches the network through there, so `repos//…` — whose 404 reads as "no release" and "no PRs" — is now impossible (#191). - Release asset names are percent-encoded. The hook contract permits any filename, and the name travels as a query value: a space made curl reject the URL and `&`/`#`/`+`/`%` silently renamed the asset (#191). ### Changed - `docs/CONSUMERS.md`'s artifact-hook recovery no longer tells operators to run `gh release create` by hand — on a Forgejo runner there is no `gh`. It names the forge-neutral tag-door path first, with both clients shown (#191).