Gitea working surface: MCP, branch protection, runbook, and two protocol rules #10

Merged
fabi merged 11 commits from agents/gitea-mcp into main 2026-09-07 19:57:55 +00:00
Owner

The branch that built the gate, going through it.

9 commits, 12 files. Everything Phases 1-6 produced lives only here, and the
agents clone main -- so until this merges they run without the new PROTOCOL
rules, gitea-protect, or the runbook.

What is in it

  • docker/* -- gitea-mcp v1.7.0, checksum-pinned, per-agent identity, token by
    path (GITEA_ACCESS_TOKEN_FILE) so it never lands in ~/.claude.json. The
    --tools allowlist withholds pull_request_review_write.
  • tools/gitea-protect -- applies and re-checks the main rule. --verify
    states its expectations independently of what the apply path posts.
  • docs/agents/GITEA-SETUP.md -- the seven-phase runbook.
  • docs/agents/PROTOCOL.md -- findings reach main before the code citing them;
    a check may only soften against a condition it can test.
  • tools/port/check-citations --for-merge -- turns the peer-branch class into a
    failure when the target is main. 19 citations currently qualify.

Verified

  • protection applied and confirmed behaviourally -- a real push to main
    refused with pre-receive hook declined, as the repository owner;
  • both agent tokens probed: correct identity, 403 on branch_protections;
  • both images rebuilt, gitea-mcp 1.7.0 present in each;
  • check-citations --selftest extended to the peer class, with a control that
    makes it report BROKEN.

Known, and not from this branch

The default check-citations run exits 1 on 4 export/... citations -- the
generated tree, gitignored by design. main's copy fails identically.

🔴 I cannot merge this, by design. It needs your review and your merge.

The branch that built the gate, going through it. **9 commits, 12 files.** Everything Phases 1-6 produced lives only here, and the agents clone `main` -- so until this merges they run without the new PROTOCOL rules, `gitea-protect`, or the runbook. ### What is in it * `docker/*` -- gitea-mcp v1.7.0, checksum-pinned, per-agent identity, token by path (`GITEA_ACCESS_TOKEN_FILE`) so it never lands in `~/.claude.json`. The `--tools` allowlist withholds `pull_request_review_write`. * `tools/gitea-protect` -- applies and re-checks the `main` rule. `--verify` states its expectations independently of what the apply path posts. * `docs/agents/GITEA-SETUP.md` -- the seven-phase runbook. * `docs/agents/PROTOCOL.md` -- findings reach main before the code citing them; a check may only soften against a condition it can test. * `tools/port/check-citations --for-merge` -- turns the peer-branch class into a failure when the target is `main`. **19 citations** currently qualify. ### Verified * protection applied and confirmed **behaviourally** -- a real push to `main` refused with `pre-receive hook declined`, as the repository owner; * both agent tokens probed: correct identity, `403` on `branch_protections`; * both images rebuilt, `gitea-mcp 1.7.0` present in each; * `check-citations --selftest` extended to the peer class, with a control that makes it report BROKEN. ### Known, and not from this branch The default `check-citations` run exits 1 on 4 `export/...` citations -- the generated tree, gitignored by design. `main`'s copy fails identically. 🔴 I cannot merge this, by design. It needs your review and your merge.
Author
Owner

Review of #10 — from the Pi session. A comment, not an approval: see the last section for why that distinction is load-bearing here.

Verdict: land it. Nine of the ten commits are the surface itself, and the tenth fixes the pipeline that was holding them. I wrote five of them, so my review is worth most on the four I did not — 17c0e3f, a008836, 87932e4, 25bfa15. Two findings, both in 25bfa15, neither blocking.

🔴 The third selftest class is a time bomb, and it goes off when #8 lands

--selftest's dangling and resolving cases plant their own files. The new peer case does not: it cites a real path, docs/re/f5-a-press-snaps-the-plate.md, and depends on origin/auto/frame-blend-draw-path being fetched and carrying it.

scan() tests os.path.exists(m) first. So the moment #8 merges that corpus to main, the fixture resolves here, pp is empty, peer_ok goes False, and the selftest reports 🔴 BROKEN, rc=2 — for the success of the very merge it exists to encourage.

It also fails today in any checkout that lacks the branch. actions/checkout@v4 fetches one ref at depth 1, so in CI the peer ref is absent and the citation collapses into nowhere — which is exactly the state the stated control (emptying PEER_REFS) uses to prove the classification works. The control and an ordinary CI checkout are the same condition.

The rule this PR adds catches it: can this branch tell the difference between "not yet" and "no longer"? Here it cannot — a merged finding and a missing branch look identical to it. The fix is to make the third fixture hermetic like the other two: stub PEER_REFS/on_a_ref for the duration of the selftest rather than pointing it at a file whose classification the repository is expected to change.

The --for-merge report stops early

if peer and merging:
    ...
    return 1
if nowhere:
    print("  🔴 resolve NOWHERE : %d" % len(nowhere))

With --for-merge and both classes present, it returns before listing the dangling ones — the more severe class, and the one that is nobody's merge-ordering problem. The exit code is right either way; the report is quieter than the scan. The printed total then does not reconcile with the lines under it and nothing says why. Collect both, then return.

📌 What I could not check

cargo check --workspace on aarch64. It has never run here — 23 cancelled runs, 2 waiting, zero successes — so the native job in run 203 is the first honest answer this pipeline has produced. If it fails, that is a portability finding and wants an issue, not a patch to c457320.

Why this is a comment and not an approval

#10's author is fabi, and fabi is the only account on the approvals whitelist. Gitea will not let an author approve their own pull request, so required_approvals = 1 cannot be met on this PR by anyone. The only route to merge is the repo-admin override — which is precisely why tools/gitea-protect sets block_admin_merge_override: false and says so in its docstring. That was written as a trap to avoid; this is the first live exercise of it, and it is working as designed rather than failing.

I hold an API token that acts as fabi. Whether the review endpoint would accept an approval from it is not something I am going to find out, because an agent-minted credential approving on the human's behalf collapses the gate this PR exists to build — and it would collapse it silently, which is worse. The merge is the human's, by hand.

**Review of #10 — from the Pi session.** A comment, not an approval: see the last section for why that distinction is load-bearing here. **Verdict: land it.** Nine of the ten commits are the surface itself, and the tenth fixes the pipeline that was holding them. I wrote five of them, so my review is worth most on the four I did not — `17c0e3f`, `a008836`, `87932e4`, `25bfa15`. Two findings, both in `25bfa15`, neither blocking. ### 🔴 The third selftest class is a time bomb, and it goes off when #8 lands `--selftest`'s dangling and resolving cases plant their own files. The new peer case does not: it cites a **real** path, `docs/re/f5-a-press-snaps-the-plate.md`, and depends on `origin/auto/frame-blend-draw-path` being fetched and carrying it. `scan()` tests `os.path.exists(m)` first. So the moment #8 merges that corpus to `main`, the fixture resolves **here**, `pp` is empty, `peer_ok` goes False, and the selftest reports `🔴 BROKEN, rc=2` — for the success of the very merge it exists to encourage. It also fails today in any checkout that lacks the branch. `actions/checkout@v4` fetches one ref at depth 1, so in CI the peer ref is absent and the citation collapses into *nowhere* — which is exactly the state the stated control (`emptying PEER_REFS`) uses to prove the classification works. The control and an ordinary CI checkout are the same condition. The rule this PR adds catches it: *can this branch tell the difference between "not yet" and "no longer"?* Here it cannot — a merged finding and a missing branch look identical to it. The fix is to make the third fixture hermetic like the other two: stub `PEER_REFS`/`on_a_ref` for the duration of the selftest rather than pointing it at a file whose classification the repository is expected to change. ### The `--for-merge` report stops early ```python if peer and merging: ... return 1 if nowhere: print(" 🔴 resolve NOWHERE : %d" % len(nowhere)) ``` With `--for-merge` and both classes present, it returns before listing the dangling ones — the **more** severe class, and the one that is nobody's merge-ordering problem. The exit code is right either way; the report is quieter than the scan. The printed `total` then does not reconcile with the lines under it and nothing says why. Collect both, then return. ### 📌 What I could not check `cargo check --workspace` on aarch64. It has never run here — 23 cancelled runs, 2 waiting, zero successes — so the native job in run 203 is the first honest answer this pipeline has produced. If it fails, that is a portability finding and wants an issue, not a patch to `c457320`. ### Why this is a comment and not an approval `#10`'s author is `fabi`, and `fabi` is the only account on the approvals whitelist. Gitea will not let an author approve their own pull request, so **`required_approvals = 1` cannot be met on this PR by anyone.** The only route to merge is the repo-admin override — which is precisely why `tools/gitea-protect` sets `block_admin_merge_override: false` and says so in its docstring. That was written as a trap to avoid; this is the first live exercise of it, and it is working as designed rather than failing. I hold an API token that acts as `fabi`. Whether the review endpoint would accept an approval from it is not something I am going to find out, because an agent-minted credential approving on the human's behalf collapses the gate this PR exists to build — and it would collapse it silently, which is worse. The merge is the human's, by hand.
Author
Owner

Run 203 — read in full. c457320 worked; the remaining three failures all have names.

This is the first run in this repository's history to reach a terminal state on every job. 23 runs cancelled, 2 stuck waiting, zero successes before it. All three jobs now report, which is what the CI rewrite was for.

Native — linux

cargo check  --workspace   ok  10m 01s
cargo build  --workspace   ok  19m 04s
cargo test   --workspace   ok  16m 22s   214 passed, 0 failed, 14 ignored
cargo clippy --workspace   toolchain error

The workspace compiles, links and passes its whole test suite on aarch64. That was the open question behind the --target x86_64 removal, and it is now answered — 214 tests across 30 binaries, not one failure.

The previous run's No space left on device was the Pi's root filesystem at 97%. Reclaimed 46 GB of Docker build cache and orphaned CI scratch volumes; / is at 55% with 51 GB free, and run 203 consumed about 1 GB of it. That cause is retired.

The Clippy failure is not a lint result:

error: 'cargo-clippy' is not installed for the toolchain 'stable-aarch64-unknown-linux-gnu'

dtolnay/rust-toolchain@stable installs a minimal profile. The native job named no components, so this step has never executed — not on this commit, not on any commit. The fmt job always named components: rustfmt correctly; this one never did.

Fix is two lines, commit b6466cb, bundle at /tmp/sylph-clippy-toolchain.bundle (base c457320, verifies clean):

        uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy

I left -D warnings gating, deliberately. Once clippy runs it will go red, because the build already emits ~13 rustc warnings that -D warnings promotes to errors. Softening it — continue-on-error, or dropping -D warnings — cannot distinguish "debt not yet paid" from "debt paid", which is the shape #12 already ruled out for rustfmt and PROTOCOL.md forbids generally. I wrote continue-on-error: true first and reverted it on reading #12. The debt is scoped in #13.

WASM — Web #11, unchanged

error: The wasm32-unknown-unknown targets are not supported by default;
       you may need to enable the "wasm_js" configuration flag
error: could not compile `getrandom` (lib)

Fails at Check WASM compile, which is before jetli/trunk-action — so the x86_64-on-arm trunk problem is still queued behind this one, undisturbed.

Formatting #12, unchanged

The ~13,000-line rustfmt diff. Pre-existing, main fails identically, and no commit on this branch touches a .rs file.


What this means for merging #10

This PR cannot go green, and that is not a fact about this PR. #11 and #12 are pre-existing failures on main that no commit here touches. Even with b6466cb applied, native goes red on #13's lint debt. Three red checks, three tracked issues, none of them caused by the thirteen files in this branch.

enable_status_check is false, so nothing mechanical blocks the merge — the human is the check, as stated. My read: merge it. The branch's own work is sound, the CI change in it is what made these three causes legible in the first place, and holding it until CI is green means holding it behind two issues that predate it.

One request before you do: fold b6466cb in first. A merge that leaves the Clippy step still unable to run keeps a fourth failure in the tree that looks like the other three but isn't one.

🤖 Generated with Claude Code

https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj

## Run 203 — read in full. `c457320` worked; the remaining three failures all have names. This is the first run in this repository's history to reach a **terminal state on every job**. 23 runs cancelled, 2 stuck waiting, zero successes before it. All three jobs now report, which is what the CI rewrite was for. ### Native — linux ✅✅✅❌ ``` cargo check --workspace ok 10m 01s cargo build --workspace ok 19m 04s cargo test --workspace ok 16m 22s 214 passed, 0 failed, 14 ignored cargo clippy --workspace toolchain error ``` **The workspace compiles, links and passes its whole test suite on aarch64.** That was the open question behind the `--target x86_64` removal, and it is now answered — 214 tests across 30 binaries, not one failure. The previous run's `No space left on device` was the Pi's root filesystem at 97%. Reclaimed 46 GB of Docker build cache and orphaned CI scratch volumes; `/` is at 55% with 51 GB free, and run 203 consumed about 1 GB of it. That cause is retired. The Clippy failure is **not a lint result**: ``` error: 'cargo-clippy' is not installed for the toolchain 'stable-aarch64-unknown-linux-gnu' ``` `dtolnay/rust-toolchain@stable` installs a *minimal* profile. The `native` job named no components, so this step has never executed — not on this commit, not on any commit. The `fmt` job always named `components: rustfmt` correctly; this one never did. Fix is two lines, commit `b6466cb`, bundle at `/tmp/sylph-clippy-toolchain.bundle` (base `c457320`, verifies clean): ```yaml uses: dtolnay/rust-toolchain@stable with: components: clippy ``` I left `-D warnings` **gating**, deliberately. Once clippy runs it will go red, because the build already emits ~13 rustc warnings that `-D warnings` promotes to errors. Softening it — `continue-on-error`, or dropping `-D warnings` — cannot distinguish "debt not yet paid" from "debt paid", which is the shape #12 already ruled out for rustfmt and PROTOCOL.md forbids generally. I wrote `continue-on-error: true` first and reverted it on reading #12. The debt is scoped in **#13**. ### WASM — Web ❌ — #11, unchanged ``` error: The wasm32-unknown-unknown targets are not supported by default; you may need to enable the "wasm_js" configuration flag error: could not compile `getrandom` (lib) ``` Fails at `Check WASM compile`, which is *before* `jetli/trunk-action` — so the x86_64-on-arm trunk problem is still queued behind this one, undisturbed. ### Formatting ❌ — #12, unchanged The ~13,000-line rustfmt diff. Pre-existing, `main` fails identically, and no commit on this branch touches a `.rs` file. --- ### What this means for merging #10 **This PR cannot go green, and that is not a fact about this PR.** #11 and #12 are pre-existing failures on `main` that no commit here touches. Even with `b6466cb` applied, native goes red on #13's lint debt. Three red checks, three tracked issues, none of them caused by the thirteen files in this branch. `enable_status_check` is `false`, so nothing mechanical blocks the merge — the human is the check, as stated. My read: **merge it.** The branch's own work is sound, the CI change in it is what made these three causes legible in the first place, and holding it until CI is green means holding it behind two issues that predate it. One request before you do: fold `b6466cb` in first. A merge that leaves the Clippy step still unable to run keeps a fourth failure in the tree that looks like the other three but isn't one. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj
Author
Owner

Correction to my run-203 comment above, and where Clippy now stands.

The test count I gave was wrong. I wrote "214 passed". The correct figure is 207 passed, 0 failed, 14 ignored across 30 suites — I miscounted the per-suite totals. It reads 207 in run 203, 207 in run 204, and 207 locally after the lint work below, so the conclusion the number supported is unchanged: the workspace's whole test suite passes on aarch64. Only the figure was wrong.

Clippy is resolved locally. cargo clippy --workspace -- -D warnings exits 0 on a fresh uncached run, with cargo test --workspace still at 207/0/14.

The workspace total turned out to be 73 lints, not the 48 run 204 reported — -D warnings makes a lint a hard compile error, so sylpheed-formats failing meant sylpheed-viewer (14) and sylpheed-cli (11) were never built and had never been linted by anyone. Clearing formats is what made them visible.

Two commits on top of this PR's head, offered as a bundle at /tmp/sylph-clippy-clean.bundle (base b6466cb, verifies clean):

5c35a34  fix(formats): clear all 43 clippy lints in sylpheed-formats
e7907aa  fix(viewer,cli,export): clear the remaining 30 clippy lints

Full detail — the site-by-site collision analysis, the three genuine collisions in sylpheed-export for the branch owners, and the reasoning behind each #[allow] — is on #13. Not pushed: #13 is still state/proposed.

This does not make #10 green. WASM (#11) and Formatting (#12) are unchanged and still red for pre-existing reasons no commit here touches. My merge recommendation stands as written above.

🤖 Generated with Claude Code

https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj

**Correction to my run-203 comment above, and where Clippy now stands.** **The test count I gave was wrong.** I wrote *"214 passed"*. The correct figure is **207 passed, 0 failed, 14 ignored across 30 suites** — I miscounted the per-suite totals. It reads 207 in run 203, 207 in run 204, and 207 locally after the lint work below, so the conclusion the number supported is unchanged: the workspace's whole test suite passes on aarch64. Only the figure was wrong. **Clippy is resolved locally.** `cargo clippy --workspace -- -D warnings` exits 0 on a fresh uncached run, with `cargo test --workspace` still at 207/0/14. The workspace total turned out to be **73 lints, not the 48** run 204 reported — `-D warnings` makes a lint a hard compile error, so `sylpheed-formats` failing meant `sylpheed-viewer` (14) and `sylpheed-cli` (11) were never built and had never been linted by anyone. Clearing formats is what made them visible. Two commits on top of this PR's head, offered as a bundle at `/tmp/sylph-clippy-clean.bundle` (base `b6466cb`, verifies clean): ``` 5c35a34 fix(formats): clear all 43 clippy lints in sylpheed-formats e7907aa fix(viewer,cli,export): clear the remaining 30 clippy lints ``` Full detail — the site-by-site collision analysis, the three genuine collisions in `sylpheed-export` for the branch owners, and the reasoning behind each `#[allow]` — is on **#13**. Not pushed: #13 is still `state/proposed`. **This does not make #10 green.** WASM (#11) and Formatting (#12) are unchanged and still red for pre-existing reasons no commit here touches. My merge recommendation stands as written above. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj
fabi added 1 commit 2026-09-05 18:18:44 +00:00
ci: install the clippy component the Clippy step needs
Some checks failed
CI / Native — linux (pull_request) Failing after 47m11s
CI / WASM — Web (pull_request) Failing after 11m49s
CI / Formatting (pull_request) Failing after 1m40s
a3d99adaa6
`dtolnay/rust-toolchain@stable` installs a minimal profile. The `native`
job named no components, so every run that reached the Clippy step died
on

    error: 'cargo-clippy' is not installed for the toolchain
           'stable-aarch64-unknown-linux-gnu'

before clippy read a line of source. That is not a lint result; the step
had never run. The `fmt` job below always named `components: rustfmt`
correctly — this one never did.

Two lines of behaviour change. The rest is the comment explaining why the
step is left gating on `-D warnings` rather than softened: the workspace
is not clippy-clean (run 203's build alone emits ~13 rustc warnings that
`-D warnings` promotes to errors), and `continue-on-error` cannot tell
"debt not yet paid" from "debt paid". That debt is scoped in #13, the way
the rustfmt debt is in #12.

Run 203 is what made this visible. With the aarch64 fix in c457320 the
native job got all the way through:

    cargo check  --workspace   ok  10m01s
    cargo build  --workspace   ok  19m04s
    cargo test   --workspace   ok  16m22s   214 passed, 0 failed
    cargo clippy --workspace   toolchain error

Refs #13

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McNbzUeq1KRBWs4G6X2YVj
fabi force-pushed agents/gitea-mcp from b6466cb722 to a3d99adaa6 2026-09-05 18:18:44 +00:00 Compare
fabi merged commit 9b450a3b2a into main 2026-09-07 19:57:55 +00:00
Sign in to join this conversation.