sync_blocked_label.py cannot take Status/Blockedoff an issue whose blockers all live in another repo. It adds correctly and it never removes, so the label sticks forever and the backlog sweep keeps reporting the issue as blocked after it is free.
The natural experiment, 2026-09-08
Two issues had every blocker closed within minutes of each other. The reconciler runs on a */15 cron in each repo and reported success on every run.
issue
blockers
where
label after 4 scheduled runs
cfbypass#1
its own sub-issues, all closed
same repo
removed automatically at ~07:44
weblib-archive#30
cfbypass#1, cfbypass#33, both closed
cross repo
still Status/Blocked at 08:30
Runs 1171/1176/1179/1182 (07:45, 08:00, 08:15, 08:30) all green. HANDS_OFF does not apply — #30 carries Kind/Feature, Priority/Low, Status/Blocked, Type/Epic, and neither Status/On Hold nor Status/Abandoned. I removed the label by hand to unstick the board, so the reproduction is gone from #30; re-create it by adding a cross-repo dependency that is already closed.
Two candidate mechanisms, both consistent, not yet distinguished
The Actions token is scoped to the repo it runs in, so it cannot read cfbypass while running in weblib-archive. Either:
Gitea omits the cross-repo entries from repos/{repo}/issues/{n}/dependencies for a token that cannot see the other repo. deps then comes back empty and the function takes its "no edges, no opinion" path and continues — never touching the label. I think this is the likelier one, because it also explains the silence: an empty list produces no log line at all.
The entries are returned without a resolvable state. Then d.get("state") != "closed" is true, every cross-repo blocker counts as open forever, and the label is re-asserted rather than removed.
They are easy to tell apart: log len(deps) and the raw entries for one known cross-repo case under the Actions token. Worth doing before fixing, since the two need different fixes — 1 needs a token that can see the other repo, 2 needs the state resolved by querying the blocker's own repo directly.
This is not a regression, it is the deliberate trade from cfbypass#19. Before that fix the rule was "only a positively open blocker blocks", and an unreadable cross-repo entry read as not-open — so the runner took the label off while cfbypass#1 was still open. The fix inverted it to "only a positively closed blocker is done", which is the right direction: a false "unblocked" sends someone to work on something that cannot be worked on, while a false "blocked" only hides available work.
But the inverted rule has no exit. Under mechanism 2 a cross-repo blocker can never be confirmed closed, so the label is permanent. #19 fixed the dangerous half and left the stuck half, and nothing measured the stuck half because the only cross-repo blockers at the time were still open.
Why it matters beyond one label
The backlog sweep in docs/UNSUPERVISED.md keys on Status/Blocked and on the dependency graph. A permanently-labelled issue is one that never comes back as workable — the exact failure bit called out on 2026-09-07: "they were unblocked/etc. and you should have started working on them". weblib-archive#30 is the largest item on the board and it would have stayed invisible.
Note the cross-repo case is not an edge case here: weblib-archive#30 → cfbypass#1, #29 → weblib-fs#17, #57 → #30. The dependency graph in this org is mostly cross-repo.
Adjacent
Whatever the fix, it wants a test, and #9 is that nothing runs this repo's tests at all. The reporting side of the same problem: a job that does nothing still reports success, which is how this survived four green runs.
`sync_blocked_label.py` cannot take `Status/Blocked` **off** an issue whose blockers all live in another repo. It adds correctly and it never removes, so the label sticks forever and the backlog sweep keeps reporting the issue as blocked after it is free.
## The natural experiment, 2026-09-08
Two issues had every blocker closed within minutes of each other. The reconciler runs on a `*/15` cron in each repo and reported **success** on every run.
| issue | blockers | where | label after 4 scheduled runs |
|---|---|---|---|
| cfbypass#1 | its own sub-issues, all closed | **same repo** | **removed automatically** at ~07:44 |
| weblib-archive#30 | cfbypass#1, cfbypass#33, both closed | **cross repo** | **still `Status/Blocked`** at 08:30 |
Runs 1171/1176/1179/1182 (07:45, 08:00, 08:15, 08:30) all green. `HANDS_OFF` does not apply — #30 carries `Kind/Feature`, `Priority/Low`, `Status/Blocked`, `Type/Epic`, and neither `Status/On Hold` nor `Status/Abandoned`. I removed the label by hand to unstick the board, so **the reproduction is gone from #30**; re-create it by adding a cross-repo dependency that is already closed.
## Two candidate mechanisms, both consistent, not yet distinguished
The Actions token is scoped to the repo it runs in, so it cannot read cfbypass while running in weblib-archive. Either:
1. **Gitea omits the cross-repo entries** from `repos/{repo}/issues/{n}/dependencies` for a token that cannot see the other repo. `deps` then comes back empty and the function takes its "no edges, no opinion" path and `continue`s — never touching the label. **I think this is the likelier one**, because it also explains the silence: an empty list produces no log line at all.
2. **The entries are returned without a resolvable `state`.** Then `d.get("state") != "closed"` is true, every cross-repo blocker counts as open forever, and the label is re-asserted rather than removed.
They are easy to tell apart: log `len(deps)` and the raw entries for one known cross-repo case under the Actions token. Worth doing before fixing, since the two need different fixes — 1 needs a token that can see the other repo, 2 needs the state resolved by querying the blocker's own repo directly.
## Why this is the fail-safe from #19 biting back
This is not a regression, it is the deliberate trade from cfbypass#19. Before that fix the rule was "only a positively *open* blocker blocks", and an unreadable cross-repo entry read as not-open — so the runner **took the label off while cfbypass#1 was still open**. The fix inverted it to "only a positively *closed* blocker is done", which is the right direction: a false "unblocked" sends someone to work on something that cannot be worked on, while a false "blocked" only hides available work.
But the inverted rule has no exit. Under mechanism 2 a cross-repo blocker can never be confirmed closed, so the label is permanent. #19 fixed the dangerous half and left the stuck half, and nothing measured the stuck half because the only cross-repo blockers at the time were still open.
## Why it matters beyond one label
The backlog sweep in `docs/UNSUPERVISED.md` keys on `Status/Blocked` and on the dependency graph. A permanently-labelled issue is one that never comes back as workable — the exact failure bit called out on 2026-09-07: *"they were unblocked/etc. and you should have started working on them"*. weblib-archive#30 is the largest item on the board and it would have stayed invisible.
Note the cross-repo case is not an edge case here: weblib-archive#30 → cfbypass#1, #29 → weblib-fs#17, #57 → #30. The dependency graph in this org is mostly cross-repo.
## Adjacent
Whatever the fix, it wants a test, and #9 is that nothing runs this repo's tests at all. The reporting side of the same problem: a job that does nothing still reports success, which is how this survived four green runs.
Nothing needed from you here beyond the decision already asked in #13
Mechanism established (not mechanism 1, and not quite the plain mechanism 2
from the original framing, but the sharper version): under a real Actions
token scoped to weblib-archive, a cross-repo dependency entry is never
omitted (len(deps) is never 0), but comes back as Gitea's Go zero-value Issue stub -- id: 0, state: "", repository: null. The old state != "closed" check read that blank state as "still open" forever,
which is why the label could never come off, and -- found live on
weblib-archive#30, 2026-09-09 -- why it started getting reasserted over a
hand-removal every 15 minutes.
Fix, tests (12, offline, proved by fault injection), and the full evidence
trail are in #13. That PR also carries a decision for bit: whether to add
a broader-scoped credential so cross-repo state can be positively confirmed,
rather than only ever reported as unknown. Probed directly: the
repo-scoped token gets 404 on GET repos/weblib/cfbypass/issues/1 and on the
repo itself, so this is a hard wall, not a permissions nuance -- and not
something to work around with a wider token without that being bit's call.
Also noticed while chasing this, not fixed in #13: main in both
weblib-archive and cfbypass is currently running each repo's own
pre-migration local copy of this script, which can no longer find Status/Blocked at all since labels moved to the org -- every scheduled run
on main prints skipped: no 'Status/Blocked' label in this repo and
touches nothing, for any issue. Filing separately per repo.
## Nothing needed from you here beyond the decision already asked in #13
Mechanism established (not mechanism 1, and not quite the plain mechanism 2
from the original framing, but the sharper version): under a real Actions
token scoped to weblib-archive, a cross-repo dependency entry is never
omitted (`len(deps)` is never 0), but comes back as Gitea's Go zero-value
`Issue` stub -- `id: 0, state: "", repository: null`. The old
`state != "closed"` check read that blank state as "still open" forever,
which is why the label could never come off, and -- found live on
weblib-archive#30, 2026-09-09 -- why it started getting *reasserted* over a
hand-removal every 15 minutes.
Fix, tests (12, offline, proved by fault injection), and the full evidence
trail are in **#13**. That PR also carries a decision for bit: whether to add
a broader-scoped credential so cross-repo state can be positively confirmed,
rather than only ever reported as `unknown`. Probed directly: the
repo-scoped token gets 404 on `GET repos/weblib/cfbypass/issues/1` and on the
repo itself, so this is a hard wall, not a permissions nuance -- and not
something to work around with a wider token without that being bit's call.
Also noticed while chasing this, not fixed in #13: `main` in both
weblib-archive and cfbypass is currently running each repo's own
pre-migration local copy of this script, which can no longer find
`Status/Blocked` at all since labels moved to the org -- every scheduled run
on `main` prints `skipped: no 'Status/Blocked' label in this repo` and
touches nothing, for any issue. Filing separately per repo.
Full evidence and the acceptance test are in #13's description now
(rewritten after a first pass got the acceptance criterion wrong -- see the
PR for the corrected version). Short version: do not close this issue when #13 merges.#13 stops the reconciler from silently re-asserting a
cross-repo Status/Blocked over a hand-removal (confirmed live on
weblib-archive#30, 2026-09-09) and makes it log the issue number instead of
guessing -- but weblib-archive's own scoped Actions token genuinely cannot
resolve a cross-repo blocker's state (GET repos/weblib/cfbypass/issues/1 ->
404 under that token), so the label still cannot be positively cleared. This
issue's title is still true after #13 merges; #13 just replaces "silently
wrong" with "loudly unknown".
The remaining decision -- whether to add a broader-scoped credential so
cross-repo state can be positively confirmed -- is posed in #13 for bit.
The two follow-up issues mentioned in my last comment are filed:
weblib-archive#70 and cfbypass#35, both assigned to bit (each repo's main
runs a pre-org-label-migration local copy of this script and has been a
complete no-op, in both directions, since labels moved to the org).
## Update: PR #13 mitigates this, does not close it
Full evidence and the acceptance test are in **#13**'s description now
(rewritten after a first pass got the acceptance criterion wrong -- see the
PR for the corrected version). Short version: **do not close this issue when
#13 merges.** #13 stops the reconciler from silently re-asserting a
cross-repo `Status/Blocked` over a hand-removal (confirmed live on
weblib-archive#30, 2026-09-09) and makes it log the issue number instead of
guessing -- but weblib-archive's own scoped Actions token genuinely cannot
resolve a cross-repo blocker's state (`GET repos/weblib/cfbypass/issues/1` ->
404 under that token), so the label still cannot be positively cleared. This
issue's title is still true after #13 merges; #13 just replaces "silently
wrong" with "loudly unknown".
The remaining decision -- whether to add a broader-scoped credential so
cross-repo state *can* be positively confirmed -- is posed in #13 for bit.
The two follow-up issues mentioned in my last comment are filed:
weblib-archive#70 and cfbypass#35, both assigned to bit (each repo's `main`
runs a pre-org-label-migration local copy of this script and has been a
complete no-op, in both directions, since labels moved to the org).
Reopening: this closed by keyword accident, not by decision
#13 is merged and that is correct, but this issue is not fixed and both the PR and its description say so.
What closed it: the PR title was "Mitigate the stuck/reasserted cross-repo Status/Blocked (does not fully fix#11)". Gitea's merge commit carries the PR title, and its keyword matcher reads fix #11 out of that string — the "does not fully" in front of it is not parsed. The close is timestamped 16:35:13, identical to the merge's commit_ref, which is the signature of an automatic close rather than a deliberate one.
So the phrasing chosen specifically to be honest about scope is what triggered the closure. Worth knowing generally: a negation does not defuse a closing keyword. Removing Closes from the body, as was done here deliberately, does not help if the title still contains fix #N, close #N, resolve #N or any of their inflections. Keep the issue number out of the title, or refer to it as "weblib-ci#11" / "issue 11" with no verb in front.
What actually remains open
The reconciler no longer guesses: an unresolvable cross-repo blocker is reported as unknown and no label is touched. That replaces silently wrong with loudly unknown, which is the improvement #13 shipped.
It does not let the label be cleared. weblib-archive's Actions token cannot read cfbypass at all (repos/weblib/cfbypass/issues/1 → 404), so weblib-archive#30's Status/Blocked is still stuck and will stay stuck. Clearing it needs a credential that can read across repos — a decision for @bit, posed in #13 and not taken unilaterally.
Reopening so that decision does not get lost. @bit, if you would rather track it elsewhere and close this, please do — I am restoring the state #13 intended, not overriding you.
## Reopening: this closed by keyword accident, not by decision
#13 is merged and that is correct, but **this issue is not fixed** and both the PR and its description say so.
What closed it: the PR title was *"Mitigate the stuck/reasserted cross-repo Status/Blocked (**does not fully fix #11**)"*. Gitea's merge commit carries the PR title, and its keyword matcher reads `fix #11` out of that string — **the "does not fully" in front of it is not parsed.** The close is timestamped 16:35:13, identical to the merge's `commit_ref`, which is the signature of an automatic close rather than a deliberate one.
So the phrasing chosen specifically to be honest about scope is what triggered the closure. Worth knowing generally: **a negation does not defuse a closing keyword.** Removing `Closes` from the body, as was done here deliberately, does not help if the *title* still contains `fix #N`, `close #N`, `resolve #N` or any of their inflections. Keep the issue number out of the title, or refer to it as "weblib-ci#11" / "issue 11" with no verb in front.
### What actually remains open
The reconciler no longer guesses: an unresolvable cross-repo blocker is reported as **unknown** and no label is touched. That replaces *silently wrong* with *loudly unknown*, which is the improvement #13 shipped.
It does **not** let the label be cleared. weblib-archive's Actions token cannot read cfbypass at all (`repos/weblib/cfbypass/issues/1` → 404), so weblib-archive#30's `Status/Blocked` is still stuck and will stay stuck. Clearing it needs a credential that can read across repos — **a decision for @bit**, posed in #13 and not taken unilaterally.
Reopening so that decision does not get lost. @bit, if you would rather track it elsewhere and close this, please do — I am restoring the state #13 intended, not overriding you.
Refinement: it is not "cross-repo", it is "invisible to this token"
Live evidence from today, which narrows this issue's real scope.
weblib-viewer#25 was created with a single dependency on fdroid-repo-ci#12, a different repo — and the reconciler correctly addedStatus/Blocked. Under the merged fix, an unresolvable blocker yields "unknown, change nothing", so an add means the entry resolved properly.
Why it resolved: fdroid-repo-ci is public. A repo-scoped Actions token can read a public repo, so the dependency comes back fully populated (id=204, state="open") and the ordinary open-blocker path applies.
So the zero-value stub is a visibility artifact, not a cross-repo one:
blocker in a public repo → resolves under any token → reconciler works normally, cross-repo or not
blocker in a private repo the token is not scoped to → zero-value stub → unknown
That explains the whole pattern without new hypotheses. weblib-archive#30 is blocked by cfbypass (private) → stuck. weblib-viewer#25 is blocked by fdroid-repo-ci (public) → works.
This issue's title is therefore too broad — "every blocker is in another repo" is not the condition; "every blocker is in a repo this token cannot see" is. The credential decision is unchanged, but it is narrower than it looked: it only has to cover the four private repos, and only for blockers among them.
No action needed from @bit beyond the credential decision already posed.
## Refinement: it is not "cross-repo", it is "invisible to this token"
Live evidence from today, which narrows this issue's real scope.
**weblib-viewer#25** was created with a single dependency on **fdroid-repo-ci#12**, a *different repo* — and the reconciler correctly **added** `Status/Blocked`. Under the merged fix, an unresolvable blocker yields "unknown, change nothing", so an add means the entry resolved properly.
Why it resolved: **fdroid-repo-ci is public.** A repo-scoped Actions token can read a public repo, so the dependency comes back fully populated (`id=204`, `state="open"`) and the ordinary open-blocker path applies.
Confirmed for all six:
| repo | private |
|---|---|
| fdroid-repo-ci, weblib-ci | **no** |
| cfbypass, weblib-fs, weblib-archive, weblib-viewer | yes |
So the zero-value stub is a **visibility** artifact, not a cross-repo one:
- blocker in a **public** repo → resolves under any token → reconciler works normally, cross-repo or not
- blocker in a **private** repo the token is not scoped to → zero-value stub → unknown
That explains the whole pattern without new hypotheses. weblib-archive#30 is blocked by **cfbypass** (private) → stuck. weblib-viewer#25 is blocked by **fdroid-repo-ci** (public) → works.
**This issue's title is therefore too broad** — "every blocker is in another repo" is not the condition; "every blocker is in a repo this token cannot see" is. The credential decision is unchanged, but it is narrower than it looked: it only has to cover the four private repos, and only for blockers among them.
No action needed from @bit beyond the credential decision already posed.
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.
sync_blocked_label.pycannot takeStatus/Blockedoff an issue whose blockers all live in another repo. It adds correctly and it never removes, so the label sticks forever and the backlog sweep keeps reporting the issue as blocked after it is free.The natural experiment, 2026-09-08
Two issues had every blocker closed within minutes of each other. The reconciler runs on a
*/15cron in each repo and reported success on every run.Status/Blockedat 08:30Runs 1171/1176/1179/1182 (07:45, 08:00, 08:15, 08:30) all green.
HANDS_OFFdoes not apply — #30 carriesKind/Feature,Priority/Low,Status/Blocked,Type/Epic, and neitherStatus/On HoldnorStatus/Abandoned. I removed the label by hand to unstick the board, so the reproduction is gone from #30; re-create it by adding a cross-repo dependency that is already closed.Two candidate mechanisms, both consistent, not yet distinguished
The Actions token is scoped to the repo it runs in, so it cannot read cfbypass while running in weblib-archive. Either:
repos/{repo}/issues/{n}/dependenciesfor a token that cannot see the other repo.depsthen comes back empty and the function takes its "no edges, no opinion" path andcontinues — never touching the label. I think this is the likelier one, because it also explains the silence: an empty list produces no log line at all.state. Thend.get("state") != "closed"is true, every cross-repo blocker counts as open forever, and the label is re-asserted rather than removed.They are easy to tell apart: log
len(deps)and the raw entries for one known cross-repo case under the Actions token. Worth doing before fixing, since the two need different fixes — 1 needs a token that can see the other repo, 2 needs the state resolved by querying the blocker's own repo directly.Why this is the fail-safe from #19 biting back
This is not a regression, it is the deliberate trade from cfbypass#19. Before that fix the rule was "only a positively open blocker blocks", and an unreadable cross-repo entry read as not-open — so the runner took the label off while cfbypass#1 was still open. The fix inverted it to "only a positively closed blocker is done", which is the right direction: a false "unblocked" sends someone to work on something that cannot be worked on, while a false "blocked" only hides available work.
But the inverted rule has no exit. Under mechanism 2 a cross-repo blocker can never be confirmed closed, so the label is permanent. #19 fixed the dangerous half and left the stuck half, and nothing measured the stuck half because the only cross-repo blockers at the time were still open.
Why it matters beyond one label
The backlog sweep in
docs/UNSUPERVISED.mdkeys onStatus/Blockedand on the dependency graph. A permanently-labelled issue is one that never comes back as workable — the exact failure bit called out on 2026-09-07: "they were unblocked/etc. and you should have started working on them". weblib-archive#30 is the largest item on the board and it would have stayed invisible.Note the cross-repo case is not an edge case here: weblib-archive#30 → cfbypass#1, #29 → weblib-fs#17, #57 → #30. The dependency graph in this org is mostly cross-repo.
Adjacent
Whatever the fix, it wants a test, and #9 is that nothing runs this repo's tests at all. The reporting side of the same problem: a job that does nothing still reports success, which is how this survived four green runs.
claude referenced this issue2026-09-09 15:49:39 +00:00
Nothing needed from you here beyond the decision already asked in #13
Mechanism established (not mechanism 1, and not quite the plain mechanism 2
from the original framing, but the sharper version): under a real Actions
token scoped to weblib-archive, a cross-repo dependency entry is never
omitted (
len(deps)is never 0), but comes back as Gitea's Go zero-valueIssuestub --id: 0, state: "", repository: null. The oldstate != "closed"check read that blank state as "still open" forever,which is why the label could never come off, and -- found live on
weblib-archive#30, 2026-09-09 -- why it started getting reasserted over a
hand-removal every 15 minutes.
Fix, tests (12, offline, proved by fault injection), and the full evidence
trail are in #13. That PR also carries a decision for bit: whether to add
a broader-scoped credential so cross-repo state can be positively confirmed,
rather than only ever reported as
unknown. Probed directly: therepo-scoped token gets 404 on
GET repos/weblib/cfbypass/issues/1and on therepo itself, so this is a hard wall, not a permissions nuance -- and not
something to work around with a wider token without that being bit's call.
Also noticed while chasing this, not fixed in #13:
mainin bothweblib-archive and cfbypass is currently running each repo's own
pre-migration local copy of this script, which can no longer find
Status/Blockedat all since labels moved to the org -- every scheduled runon
mainprintsskipped: no 'Status/Blocked' label in this repoandtouches nothing, for any issue. Filing separately per repo.
Update: PR #13 mitigates this, does not close it
Full evidence and the acceptance test are in #13's description now
(rewritten after a first pass got the acceptance criterion wrong -- see the
PR for the corrected version). Short version: do not close this issue when
#13 merges. #13 stops the reconciler from silently re-asserting a
cross-repo
Status/Blockedover a hand-removal (confirmed live onweblib-archive#30, 2026-09-09) and makes it log the issue number instead of
guessing -- but weblib-archive's own scoped Actions token genuinely cannot
resolve a cross-repo blocker's state (
GET repos/weblib/cfbypass/issues/1->404 under that token), so the label still cannot be positively cleared. This
issue's title is still true after #13 merges; #13 just replaces "silently
wrong" with "loudly unknown".
The remaining decision -- whether to add a broader-scoped credential so
cross-repo state can be positively confirmed -- is posed in #13 for bit.
The two follow-up issues mentioned in my last comment are filed:
weblib-archive#70 and cfbypass#35, both assigned to bit (each repo's
mainruns a pre-org-label-migration local copy of this script and has been a
complete no-op, in both directions, since labels moved to the org).
Reopening: this closed by keyword accident, not by decision
#13 is merged and that is correct, but this issue is not fixed and both the PR and its description say so.
What closed it: the PR title was "Mitigate the stuck/reasserted cross-repo Status/Blocked (does not fully fix #11)". Gitea's merge commit carries the PR title, and its keyword matcher reads
fix #11out of that string — the "does not fully" in front of it is not parsed. The close is timestamped 16:35:13, identical to the merge'scommit_ref, which is the signature of an automatic close rather than a deliberate one.So the phrasing chosen specifically to be honest about scope is what triggered the closure. Worth knowing generally: a negation does not defuse a closing keyword. Removing
Closesfrom the body, as was done here deliberately, does not help if the title still containsfix #N,close #N,resolve #Nor any of their inflections. Keep the issue number out of the title, or refer to it as "weblib-ci#11" / "issue 11" with no verb in front.What actually remains open
The reconciler no longer guesses: an unresolvable cross-repo blocker is reported as unknown and no label is touched. That replaces silently wrong with loudly unknown, which is the improvement #13 shipped.
It does not let the label be cleared. weblib-archive's Actions token cannot read cfbypass at all (
repos/weblib/cfbypass/issues/1→ 404), so weblib-archive#30'sStatus/Blockedis still stuck and will stay stuck. Clearing it needs a credential that can read across repos — a decision for @bit, posed in #13 and not taken unilaterally.Reopening so that decision does not get lost. @bit, if you would rather track it elsewhere and close this, please do — I am restoring the state #13 intended, not overriding you.
Refinement: it is not "cross-repo", it is "invisible to this token"
Live evidence from today, which narrows this issue's real scope.
weblib-viewer#25 was created with a single dependency on fdroid-repo-ci#12, a different repo — and the reconciler correctly added
Status/Blocked. Under the merged fix, an unresolvable blocker yields "unknown, change nothing", so an add means the entry resolved properly.Why it resolved: fdroid-repo-ci is public. A repo-scoped Actions token can read a public repo, so the dependency comes back fully populated (
id=204,state="open") and the ordinary open-blocker path applies.Confirmed for all six:
So the zero-value stub is a visibility artifact, not a cross-repo one:
That explains the whole pattern without new hypotheses. weblib-archive#30 is blocked by cfbypass (private) → stuck. weblib-viewer#25 is blocked by fdroid-repo-ci (public) → works.
This issue's title is therefore too broad — "every blocker is in another repo" is not the condition; "every blocker is in a repo this token cannot see" is. The credential decision is unchanged, but it is narrower than it looked: it only has to cover the four private repos, and only for blockers among them.
No action needed from @bit beyond the credential decision already posed.