Skip to content

feat(gate): let maintainers be exempt from the missing-linked-issue penalty - #10160

Merged
JSONbored merged 1 commit into
mainfrom
feat/linked-issue-maintainer-exemption
Jul 31, 2026
Merged

feat(gate): let maintainers be exempt from the missing-linked-issue penalty#10160
JSONbored merged 1 commit into
mainfrom
feat/linked-issue-maintainer-exemption

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Closes #10158

gate.linkedIssue: block exists to stop unlinked contributor work. Applied to a maintainer's own PR it asks them to file an issue against their own repo before touching it, then holds the PR when they don't — and the bot's own PRs (release-please, dependency bumps, generated-doc refreshes) hit it constantly for the same reason.

Measured on the Orb since the beta.7 deploy, with the hold | success bug (#10116) now fixed:

 action | reason_code          | count
 hold   | missing_linked_issue |    26   <- now the single largest bucket
 merge  | success              |     9
 hold   | ai_review_inconclusive |   4

One clamp, because the two halves are already separate

  • the missing_linked_issue finding is produced on requireLinkedIssue — true for any mode but off
  • it blocks only when resolveConfiguredGateMode resolves block

So clamping blockadvisory for maintainer-authored PRs keeps "No linked issue detected" visible in the review comment and removes exactly its power to fail the gate, hold, or close. Nothing is suppressed; nothing goes silent.

gate:
  linkedIssue: block                  # unchanged — full force for contributors
  linkedIssueMaintainerExempt: true   # new

Config-as-code only, following hardGuardrailGlobsno DB column, so no dashboard toggle can silently disagree with the file, and no migration.

Scope

Only the MISSING case. Everything that inspects an issue that is linked — linkedIssueSatisfaction, linkedIssueHardRules, linkedIssueLabelPropagation — keys on a cited issue and is untouched. A maintainer who links one is scrutinised exactly like anyone else. That is also why this is a clamp on one mode rather than a bypass flag: a bypass would have had to be excluded from each linked-issue path by hand, and the next such path would have been added without the exclusion.

"Maintainer" is the existing protected-author set (authorIsOwner || authorIsAdmin || authorIsAutomationBotprocessors.ts's protectedAuthor), reused rather than redefined so it cannot mean one thing here and another on the close path. Bots are included deliberately: they already have auto-close protection for the same reason, and in practice they are a large share of the unlinked PRs.

Wiring

Applied to settings before it reaches gateCheckPolicy, not passed as an argument — both things that must agree (requireLinkedIssue and linkedIssueGateMode) read that same object, so clamping upstream makes them unable to disagree. gateCheckPolicy also already takes seven positional parameters and does not need an eighth.

Wired at all four call sites (webhook, sweep, two re-gate paths). Wiring only the webhook path would mean a maintainer PR passing live and then being held when the sweep re-gates it — the same drift class as #10116.

Verification

  • Full suite green: 26,283 passed, 0 failed. typecheck, dead-exports, dead-source-files, import-specifiers clean; openapi.json regenerated.
  • 9 root tests + 5 engine-side tests (engine lines need the engine flag's own coverage, per CLAUDE.md).

Notable: four separate exhaustiveness guards caught this change being incomplete, and two of the failures were silent-in-production:

guard what it caught
gate known-keys allowlist the key parsed and warned unknown key … ignoring it — inert in every real config while all unit tests passed
gateConfigToJson round-trip the key was dropped on serialization
openapi parity type schema drift vs RepositorySettings
config-templates mirror documented in .loopover.yml.example but not config/examples/loopover.full.yml

Behaviour with the knob absent/false/null is byte-identical to before. The clamp is one-directional — off is never raised to advisory, which would start producing a finding on a repo that had switched the gate off entirely.

…enalty

`gate.linkedIssue: block` exists to stop unlinked CONTRIBUTOR work. Applied to
a maintainer's own PR it asks them to file an issue against their own repo
before touching it, and holds the PR when they don't; the bot's own PRs
(release-please, dependency bumps, generated-doc refreshes) hit it constantly
for the same reason. On the production Orb `hold | missing_linked_issue` is the
second-largest hold bucket -- 503 in a week.

New `gate.linkedIssueMaintainerExempt`, config-as-code only (global or
per-repo), following hardGuardrailGlobs: no DB column, so no dashboard toggle
can silently disagree with the file, and no migration.

It is ONE clamp -- block to advisory, maintainer-authored PRs only -- because
the two halves are already separate concerns. The finding is PRODUCED on
`requireLinkedIssue` (true for any mode but off) and only BLOCKS at `block`, so
clamping to advisory keeps "No linked issue detected" visible in the review
comment while removing exactly its power to fail the gate, hold, or close.
Nothing is suppressed and nothing goes silent.

Scoped to the MISSING case. Everything that inspects an issue that IS linked --
linkedIssueSatisfaction, linkedIssueHardRules, linkedIssueLabelPropagation --
keys on a cited issue and is untouched, so a maintainer who links one is
scrutinised exactly like anyone else.

"Maintainer" is the existing PROTECTED AUTHOR set (owner, per-repo admin,
protected automation author) reused rather than redefined, so it cannot come to
mean one thing here and another on the close path. Bots are included
deliberately: they already have auto-close protection for the same reason and
are a large share of the unlinked PRs in practice.

Applied to `settings` before it reaches gateCheckPolicy rather than passed as
an argument -- both things that must agree read that object, and the function
already takes seven positional parameters. Wired at ALL FOUR call sites
(webhook, sweep, two re-gate paths): wiring only the webhook path would mean a
maintainer PR passing live and then being held when the sweep re-gates it.

Closes #10158
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
loopover-ui ceef67e Jul 31 2026, 10:07 AM

@loopover-orb

loopover-orb Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Important

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏳ LoopOver is waiting…

LoopOver has seen this pull request and is waiting on CI checks to finish before reviewing it. This comment will update once the review runs.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed · 🟨 Waiting

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 214 bytes (0.0%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
loopover-ui 7.95MB 214 bytes (0.0%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-DXeWLwHX.js (New) 2.16MB 2.16MB 100.0% 🚀
assets/tanstack-vendor-CkdM7WDR.js (New) 957.76kB 957.76kB 100.0% 🚀
openapi.json 116 bytes 762.67kB 0.02%
assets/docs.fumadocs-spike-api-reference-Dr89Jjnj.js (New) 443.45kB 443.45kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-DsfCq4ju.js (New) 201.7kB 201.7kB 100.0% 🚀
assets/modal-0eycMRJ7.js (New) 184.5kB 184.5kB 100.0% 🚀
assets/client-TiXhWsow.js (New) 151.47kB 151.47kB 100.0% 🚀
assets/maintainer-panel-B7eldNLh.js (New) 78.97kB 78.97kB 100.0% 🚀
assets/routes-OP4Ot1A2.js (New) 35.88kB 35.88kB 100.0% 🚀
assets/owner-panel-D65JD-LG.js (New) 28.42kB 28.42kB 100.0% 🚀
assets/app-BkgYSCfW.js (New) 25.82kB 25.82kB 100.0% 🚀
assets/ui-vendor-DErOZ61t.js (New) 24.57kB 24.57kB 100.0% 🚀
assets/fairness-BDtagdHm.js (New) 23.44kB 23.44kB 100.0% 🚀
assets/miner-panel-C0mu4xKF.js (New) 20.49kB 20.49kB 100.0% 🚀
assets/api._op-CklzX8Fk.js (New) 17.51kB 17.51kB 100.0% 🚀
assets/self-hosting-docs-audit-ChDddLsg.js (New) 16.6kB 16.6kB 100.0% 🚀
assets/docs-client-loader-CYl2yKE6.js (New) 15.68kB 15.68kB 100.0% 🚀
assets/playground-panel-BFdBiVxC.js (New) 14.35kB 14.35kB 100.0% 🚀
assets/app.audit-3xYT77je.js (New) 10.22kB 10.22kB 100.0% 🚀
assets/app.config-generator-DL2WGU9t.js (New) 10.06kB 10.06kB 100.0% 🚀
assets/maintainers-gbURvt0f.js (New) 8.06kB 8.06kB 100.0% 🚀
assets/miners-lWmwCkZq.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-DXQiH2vS.js (New) 7.74kB 7.74kB 100.0% 🚀
assets/proof._owner._repo-CqLe_e7f.js (New) 7.11kB 7.11kB 100.0% 🚀
assets/commands-panel-CPXsaP5N.js (New) 6.74kB 6.74kB 100.0% 🚀
assets/maintainer-workflow-BD5FIFJI.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/docs-nav-DjIkfprG.js (New) 6.19kB 6.19kB 100.0% 🚀
assets/digest-panel-BvCjdFmi.js (New) 6.15kB 6.15kB 100.0% 🚀
assets/repos._owner._repo.quality-CRaiBCgd.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs.index-BTH3Q6OS.js (New) 6.01kB 6.01kB 100.0% 🚀
assets/api.index-C20ZllyI.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-DdyyYYhz.js (New) 3.01kB 3.01kB 100.0% 🚀
assets/api-DiJ4nNri.js (New) 2.66kB 2.66kB 100.0% 🚀
assets/docs-page-DynA5qVd.js (New) 2.14kB 2.14kB 100.0% 🚀
assets/table-ChN25e8H.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-DZfD1W3H.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/tabs-BMAQcGvK.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-NbZeXmI0.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-NI2tybw_.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog-C5bOY-hH.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/docs._slug-DAlESYrv.js (New) 554 bytes 554 bytes 100.0% 🚀
assets/app.maintainer-CYoPlU8J.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/app.owner-DpSqcU-8.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-C9wfY9RG.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-DdhhkfFq.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-B0OUZV-B.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-Uaj5mrv0.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-DtjNfFlK.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner-CvcX-8ji.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-CzJ3eqjS.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-BzFDnQkP.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/git-pull-request-arrow-D8WSzkxp.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/list-checks-CGQlLAH1.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/compass-C4DQvECw.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-4ZoaErrd.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/message-square-ZoHmlFYj.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-C-8HA7Wv.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/play-B6HHR-Ov.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check-D6039O7k.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/add-scalar-classes-B4GaHSM2.js (Deleted) -2.16MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-DVBG_3ef.js (Deleted) -957.7kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-Ce3R0RMU.js (Deleted) -443.45kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-tu5lzlo1.js (Deleted) -201.7kB 0 bytes -100.0% 🗑️
assets/modal-CiY9wcBA.js (Deleted) -184.5kB 0 bytes -100.0% 🗑️
assets/client-CDaN0n6B.js (Deleted) -151.47kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-SFlZKExh.js (Deleted) -78.97kB 0 bytes -100.0% 🗑️
assets/routes-BEqwM3mB.js (Deleted) -35.88kB 0 bytes -100.0% 🗑️
assets/owner-panel-Cv6SY8SR.js (Deleted) -28.39kB 0 bytes -100.0% 🗑️
assets/app-BleATQH9.js (Deleted) -25.82kB 0 bytes -100.0% 🗑️
assets/ui-vendor-BGL7ts-a.js (Deleted) -24.57kB 0 bytes -100.0% 🗑️
assets/fairness-BznGUonO.js (Deleted) -23.44kB 0 bytes -100.0% 🗑️
assets/miner-panel-BHNWNTcu.js (Deleted) -20.49kB 0 bytes -100.0% 🗑️
assets/api._op-COmkUFwO.js (Deleted) -17.51kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-DD6Hganx.js (Deleted) -16.6kB 0 bytes -100.0% 🗑️
assets/docs-client-loader-Bk-F5pJC.js (Deleted) -15.68kB 0 bytes -100.0% 🗑️
assets/playground-panel-CHb3FtzU.js (Deleted) -14.35kB 0 bytes -100.0% 🗑️
assets/app.audit-Dj5UqJXo.js (Deleted) -10.22kB 0 bytes -100.0% 🗑️
assets/app.config-generator-CkZM6uOB.js (Deleted) -10.06kB 0 bytes -100.0% 🗑️
assets/maintainers-DWUfQvmr.js (Deleted) -8.06kB 0 bytes -100.0% 🗑️
assets/miners-BX9PXQuf.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-C0VEeUuY.js (Deleted) -7.74kB 0 bytes -100.0% 🗑️
assets/proof._owner._repo-D_3Xr--B.js (Deleted) -7.11kB 0 bytes -100.0% 🗑️
assets/commands-panel--0ytnQ3-.js (Deleted) -6.74kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-Dz-S0su7.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/docs-nav-D0Yk8jgD.js (Deleted) -6.19kB 0 bytes -100.0% 🗑️
assets/digest-panel-DQ-b5ket.js (Deleted) -6.15kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-BnqeSpl7.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs.index-zC2lV3Kx.js (Deleted) -6.01kB 0 bytes -100.0% 🗑️
assets/api.index-BMOu7jkj.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs-DV194PO0.js (Deleted) -3.01kB 0 bytes -100.0% 🗑️
assets/api-gkS9xLkC.js (Deleted) -2.66kB 0 bytes -100.0% 🗑️
assets/docs-page-D6nqTsBQ.js (Deleted) -2.14kB 0 bytes -100.0% 🗑️
assets/table-iwPyP75Z.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-h6Rhz1pV.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/tabs-TtwH79rx.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-CpNGUFGG.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-BlOi-GGy.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog-CvGyl1IT.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/docs._slug-B-Bwq2sS.js (Deleted) -554 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-D41VPsI3.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/app.owner-CIZBV5Pb.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-6YcvB4ZZ.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-BYS6qlTj.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-C4Jse2Fd.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-v-pbx42E.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-DoV5ks7u.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner-DXKQuwjS.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-JvyoPABt.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-BZnv5Ygr.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-CgYd81mY.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/list-checks-5F91iPYZ.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/compass-B0OddBrY.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history-Dw4JqrNz.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/message-square-DdHVXwgw.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-BIDEQkoI.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/play-DqQJ-BZH.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-CecGTUIP.js (Deleted) -178 bytes 0 bytes -100.0% 🗑️

@JSONbored
JSONbored merged commit 8e4dccb into main Jul 31, 2026
7 of 8 checks passed
@JSONbored
JSONbored deleted the feat/linked-issue-maintainer-exemption branch July 31, 2026 10:34
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.88889% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.20%. Comparing base (4736302) to head (ceef67e).
⚠️ Report is 4 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/queue/processors.ts 66.66% 1 Missing and 2 partials ⚠️
src/signals/focus-manifest.ts 0.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #10160      +/-   ##
==========================================
- Coverage   92.21%   92.20%   -0.01%     
==========================================
  Files         931      932       +1     
  Lines      114066   114099      +33     
  Branches    27547    27558      +11     
==========================================
+ Hits       105183   105209      +26     
  Misses       7584     7584              
- Partials     1299     1306       +7     
Flag Coverage Δ
backend 95.67% <70.58%> (-0.02%) ⬇️
engine 73.80% <100.00%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/loopover-engine/src/focus-manifest.ts 82.43% <100.00%> (+0.09%) ⬆️
src/openapi/schemas.ts 100.00% <ø> (ø)
src/settings/linked-issue-exemption.ts 100.00% <100.00%> (ø)
src/types.ts 100.00% <ø> (ø)
src/signals/focus-manifest.ts 99.23% <0.00%> (-0.39%) ⬇️
src/queue/processors.ts 94.53% <66.66%> (-0.08%) ⬇️

... and 1 file with indirect coverage changes

JSONbored added a commit that referenced this pull request Jul 31, 2026
…icating zero token counts (#10233)

* feat(observability): capture every miner AI generation, and stop fabricating zero token counts

Closes #10200
Closes #10207

* chore(contract): regenerate api-schemas for the linkedIssueMaintainerExempt field

#10160 added linkedIssueMaintainerExempt to the repository-settings surface without
regenerating packages/loopover-contract/src/api-schemas.ts, so contract:api-schemas:check
has been failing on main since. The check is part of the local test:ci aggregate but is
not wired into ci.yml, which is why nothing surfaced it.

Pure regeneration: the one added line is the generator's own output for a field that
already exists everywhere else.

---------

Co-authored-by: JSONbored <aetherealdev@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gate: let maintainers be exempt from the missing-linked-issue penalty without weakening it for contributors

1 participant