Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 54 additions & 54 deletions apps/loopover-miner-ui/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,47 +9,47 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.

import { Route as rootRouteImport } from './routes/__root'
import { Route as RunHistoryRouteImport } from './routes/run-history'
import { Route as RankedCandidatesRouteImport } from './routes/ranked-candidates'
import { Route as PortfolioRouteImport } from './routes/portfolio'
import { Route as LedgersRouteImport } from './routes/ledgers'
import { Route as EarningsRouteImport } from './routes/earnings'
import { Route as AttemptsRouteImport } from './routes/attempts'
import { Route as IndexRouteImport } from './routes/index'
import { Route as AttemptsRouteImport } from './routes/attempts'
import { Route as EarningsRouteImport } from './routes/earnings'
import { Route as LedgersRouteImport } from './routes/ledgers'
import { Route as PortfolioRouteImport } from './routes/portfolio'
import { Route as RankedCandidatesRouteImport } from './routes/ranked-candidates'
import { Route as RunHistoryRouteImport } from './routes/run-history'

const RunHistoryRoute = RunHistoryRouteImport.update({
id: '/run-history',
path: '/run-history',
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const RankedCandidatesRoute = RankedCandidatesRouteImport.update({
id: '/ranked-candidates',
path: '/ranked-candidates',
const AttemptsRoute = AttemptsRouteImport.update({
id: '/attempts',
path: '/attempts',
getParentRoute: () => rootRouteImport,
} as any)
const PortfolioRoute = PortfolioRouteImport.update({
id: '/portfolio',
path: '/portfolio',
const EarningsRoute = EarningsRouteImport.update({
id: '/earnings',
path: '/earnings',
getParentRoute: () => rootRouteImport,
} as any)
const LedgersRoute = LedgersRouteImport.update({
id: '/ledgers',
path: '/ledgers',
getParentRoute: () => rootRouteImport,
} as any)
const EarningsRoute = EarningsRouteImport.update({
id: '/earnings',
path: '/earnings',
const PortfolioRoute = PortfolioRouteImport.update({
id: '/portfolio',
path: '/portfolio',
getParentRoute: () => rootRouteImport,
} as any)
const AttemptsRoute = AttemptsRouteImport.update({
id: '/attempts',
path: '/attempts',
const RankedCandidatesRoute = RankedCandidatesRouteImport.update({
id: '/ranked-candidates',
path: '/ranked-candidates',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
const RunHistoryRoute = RunHistoryRouteImport.update({
id: '/run-history',
path: '/run-history',
getParentRoute: () => rootRouteImport,
} as any)

Expand Down Expand Up @@ -123,25 +123,25 @@ export interface RootRouteChildren {

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/run-history': {
id: '/run-history'
path: '/run-history'
fullPath: '/run-history'
preLoaderRoute: typeof RunHistoryRouteImport
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/ranked-candidates': {
id: '/ranked-candidates'
path: '/ranked-candidates'
fullPath: '/ranked-candidates'
preLoaderRoute: typeof RankedCandidatesRouteImport
'/attempts': {
id: '/attempts'
path: '/attempts'
fullPath: '/attempts'
preLoaderRoute: typeof AttemptsRouteImport
parentRoute: typeof rootRouteImport
}
'/portfolio': {
id: '/portfolio'
path: '/portfolio'
fullPath: '/portfolio'
preLoaderRoute: typeof PortfolioRouteImport
'/earnings': {
id: '/earnings'
path: '/earnings'
fullPath: '/earnings'
preLoaderRoute: typeof EarningsRouteImport
parentRoute: typeof rootRouteImport
}
'/ledgers': {
Expand All @@ -151,25 +151,25 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LedgersRouteImport
parentRoute: typeof rootRouteImport
}
'/earnings': {
id: '/earnings'
path: '/earnings'
fullPath: '/earnings'
preLoaderRoute: typeof EarningsRouteImport
'/portfolio': {
id: '/portfolio'
path: '/portfolio'
fullPath: '/portfolio'
preLoaderRoute: typeof PortfolioRouteImport
parentRoute: typeof rootRouteImport
}
'/attempts': {
id: '/attempts'
path: '/attempts'
fullPath: '/attempts'
preLoaderRoute: typeof AttemptsRouteImport
'/ranked-candidates': {
id: '/ranked-candidates'
path: '/ranked-candidates'
fullPath: '/ranked-candidates'
preLoaderRoute: typeof RankedCandidatesRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
'/run-history': {
id: '/run-history'
path: '/run-history'
fullPath: '/run-history'
preLoaderRoute: typeof RunHistoryRouteImport
parentRoute: typeof rootRouteImport
}
}
Expand Down
69 changes: 69 additions & 0 deletions apps/loopover-ui/content/docs/label-policy.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
title: Label policy — which labels carry scoring weight, and the rules for each
description: The labels that affect a contribution's score, who may apply them, and when work on them opens. Every rule here is enforced mechanically in the pipeline, not by judgement, and every enforcement is written to the ledger.
eyebrow: Contributors
---

Three labels categorise an issue, and they are not equal: `gittensor:priority` carries the
highest scoring multiplier. That makes it the one label whose application has to be
constrained by a **rule** rather than by judgement — otherwise the highest-value label is
whatever anyone says it is.

Both rules below are enforced by the pipeline, on every label event. Neither is a
discretionary decision, neither is applied to some contributors and not others, and each
enforcement is written to the decision ledger with its rule id, so the history is
independently checkable.

## The scoring labels

| Label | Meaning | Who may apply it |
| --- | --- | --- |
| `gittensor:bug` | A fix, test, doc, chore, refactor, perf, ci, build or style change | Anyone |
| `gittensor:feature` | Genuinely new functionality | Anyone |
| `gittensor:priority` | Work the maintainer originated and triaged as most valuable | **Only valid on maintainer-authored issues** |

## Rule 1 — priority is only valid on maintainer-authored issues

`gittensor:priority` marks work **the maintainer originated**. If it is applied to an issue
somebody else authored — by anyone, including a maintainer — the pipeline removes it and
posts a single comment linking here.

**Maintainer of record** is read from the repository's own permissions (`admin` or
`maintain`), never a hardcoded list of names, so the rule means the same thing on every repo
LoopOver manages. `write` access is deliberately *not* enough: handing out push access would
otherwise widen who can mint the highest multiplier.

The rule **fails open**. If the author's permission cannot be read, nothing is stripped — a
label is never removed on the strength of a failed lookup, and the issue is re-judged next
time it is labelled.

Removing the label is not a judgement about the issue. It stays open, and contributions to
it remain welcome under its other labels.

Rule id: `priority-label-author-eligibility`.

## Rule 2 — priority issues open for work after a short window

Priority issues carry the highest payout, so assignment fairness matters most there. First-come
pickup is only fair if everyone can *see* the issue before anyone can act on it: a PR opened
moments after the label lands means the window between "issue becomes valuable" and "issue is
claimed" was effectively zero for everyone else watching the repo.

So a PR closing a priority issue becomes gate-eligible once the label has been publicly present
for a short window — **30 minutes by default**, configurable per repo via
`gate.priorityEligibilityWindow`, and `0` turns it off.

A PR opened inside that window is **not rejected**. It is held, with a comment naming the exact
moment it becomes eligible, and it proceeds normally once the window passes. There is no penalty
beyond waiting and nothing to resubmit.

The clock is anchored to the **earliest** time the label was applied, so re-applying the label
never resets the window for anyone, and "when does this issue open for work" is a single instant
that cannot move.

Rule id: `priority-eligibility-window`.

## What is not in scope

The multiplier *values* themselves are registry-side and are not set here. These two rules govern
which issues may carry the label and when work on them opens — not what the label is worth.
1 change: 1 addition & 0 deletions apps/loopover-ui/src/components/site/docs-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export const docsNav: DocsGroup[] = [
{ to: "/docs/loopover-commands", label: "@loopover commands" },
{ to: "/docs/branch-analysis", label: "Branch analysis" },
{ to: "/docs/scoreability", label: "Scoreability" },
{ to: "/docs/label-policy", label: "Label policy" },
{ to: "/docs/upstream-drift", label: "Upstream drift" },
{ to: "/docs/backtest-calibration", label: "Backtest & calibration" },
{ to: "/docs/verify-this-review", label: "Verify this review" },
Expand Down
14 changes: 14 additions & 0 deletions src/github/comments.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { withInstallationTokenRetry } from "./app";
import { githubRateLimitAdmissionKeyForInstallation, makeInstallationOctokit } from "./client";
import { PRIORITY_LABEL_COMMENT_MARKER } from "../review/priority-label-eligibility";
import type { AgentActionMode } from "../settings/agent-execution";

export const PR_PANEL_COMMENT_MARKER = "<!-- gittensory-pr-panel:v1 -->";
Expand Down Expand Up @@ -117,6 +118,19 @@ export async function createOrUpdateAgentCommandComment(
return createOrUpdateIssueCommentWithMarker(env, installationId, repoFullName, issueNumber, body, AGENT_COMMAND_COMMENT_MARKER, { mode });
}

/** #9737: the priority-label policy notice. Marked like its siblings so a RE-label updates the existing
* comment instead of posting a second one -- the rule re-runs on every label event by design. */
export async function createOrUpdatePriorityLabelPolicyComment(
env: Env,
installationId: number,
repoFullName: string,
issueNumber: number,
body: string,
mode: AgentActionMode = "live",
): Promise<{ id: number; html_url?: string; changed: boolean } | null> {
return createOrUpdateIssueCommentWithMarker(env, installationId, repoFullName, issueNumber, body, PRIORITY_LABEL_COMMENT_MARKER, { mode });
}

// #6724 (review-burst): `changed` distinguishes a genuine no-op (the rendered body was byte-identical to what's
// already posted, PATCH skipped -- see the idempotency comment below) from a real create/update, so a caller can
// avoid double-counting a republish that produced no visible change. `false` ONLY on the proven-identical path;
Expand Down
88 changes: 88 additions & 0 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ import { isSelfAuthoredCiCompletionWebhook } from "../github/self-authored";
import {
AGENT_COMMAND_COMMENT_MARKER,
createOrUpdateAgentCommandComment,
createOrUpdatePriorityLabelPolicyComment,
createOrUpdatePrIntelligenceComment,
createOrUpdateVisualFollowupComment,
PR_PANEL_COMMENT_MARKER,
Expand Down Expand Up @@ -192,6 +193,12 @@ import {
type PullRequestFreshness,
} from "../github/pr-freshness";
import { DEFAULT_TYPE_LABELS, resolvePrTypeLabel } from "../settings/pr-type-label";
import {
PRIORITY_LABEL_AUTHOR_RULE_ID,
PRIORITY_LABEL_ENFORCEMENT_EVENT,
PRIORITY_LABEL_POLICY_URL,
resolvePriorityLabelEnforcement,
} from "../review/priority-label-eligibility";
import { fetchLinkedIssueLabelsForPropagation } from "../review/linked-issue-label-propagation-fetch";
import { shouldPublishReviewCheck } from "../review/check-names";
import { fetchPublicContributorProfile } from "../github/public";
Expand Down Expand Up @@ -6801,6 +6808,82 @@ async function maybeHandleReactionWebhookEvent(
return false;
}


/**
* `issues.labeled` -> author-based priority-label eligibility (#9737).
*
* `gittensor:priority` carries the highest scoring multiplier, so the one label whose application must be
* constrained by rule rather than judgment. Priority marks work the MAINTAINER originated and triaged, so
* on a contributor-authored issue it is stripped and the reason stated once, with a link to the policy.
*
* The DECISION is `resolvePriorityLabelEnforcement` (pure, unit-tested); this function is its I/O -- read
* the author's permission, strip, upsert the marked comment, record the enforcement event. Returns `true`
* when it claimed the event, matching every sibling handler here.
*
* FAIL-SAFE end to end: an unreadable permission yields no strip (the evaluator's own rule), and every
* GitHub call is caught so a label event can never fail the webhook. Re-labelling simply re-runs it, which
* is why the comment carries a marker and is updated rather than re-posted.
*/
async function maybeHandlePriorityLabelEligibility(
env: Env,
deliveryId: string,
eventName: string,
payload: GitHubWebhookPayload,
): Promise<boolean> {
if (eventName !== "issues" || payload.action !== "labeled") return false;
const repoFullName = payload.repository?.full_name;
const issue = payload.issue;
const installationId = payload.installation?.id;
if (!repoFullName || !issue || !installationId) return false;
// A pull request arrives on the `issues` event too; the evaluator refuses it, but skipping here saves a
// permission read on every PR label.
if (issue.pull_request !== undefined && issue.pull_request !== null) return false;

const settings = await resolveRepositorySettings(env, repoFullName).catch(() => undefined);
/* v8 ignore next 2 -- noUncheckedIndexedAccess fallback: PrTypeLabelSet is a Record<string, string>, so
DEFAULT_TYPE_LABELS.priority reads as possibly-undefined to the type system though it is always set. */
const priorityLabel: string = settings?.typeLabels?.priority ?? DEFAULT_TYPE_LABELS.priority ?? "gittensor:priority";
const labels = (issue.labels ?? []).map((label) => label?.name ?? "").filter((name) => name.length > 0);
// Only the labelled event for THIS label matters; anything else is another label's business.
if (!labels.some((name) => name.toLowerCase() === priorityLabel.toLowerCase())) return false;

const authorLogin = issue.user?.login ?? null;
const authorPermission = authorLogin
? await getRepositoryCollaboratorPermission(env, installationId, repoFullName, authorLogin).catch(() => null)
: null;

const { verdict, commentBody } = resolvePriorityLabelEnforcement({
priorityLabel,
labels,
authorLogin,
authorPermission,
isPullRequest: false,
policyUrl: PRIORITY_LABEL_POLICY_URL,
});
if (!verdict.strip || commentBody === null) return false;

await removePullRequestLabel(env, installationId, repoFullName, issue.number, priorityLabel).catch(() => undefined);
await createOrUpdatePriorityLabelPolicyComment(env, installationId, repoFullName, issue.number, commentBody).catch(() => undefined);
await recordAuditEvent(env, {
eventType: PRIORITY_LABEL_ENFORCEMENT_EVENT,
actor: payload.sender?.login ?? null,
targetKey: `${repoFullName}#${issue.number}`,
outcome: "success",
detail: verdict.reason,
metadata: { ruleId: PRIORITY_LABEL_AUTHOR_RULE_ID, issueAuthor: authorLogin, authorPermission, label: priorityLabel },
}).catch(() => undefined);
await recordWebhookEvent(env, {
deliveryId,
eventName,
action: payload.action,
installationId,
repositoryFullName: repoFullName,
payloadHash: "processed",
status: "processed",
}).catch(() => undefined);
return true;
}

/**
* Handles the `issue_comment` webhook event's command/mention dispatch chain — panel retrigger, panel
* generate-tests, gate-override, resolve/explain/generate-tests/review/pause/resume/configuration/plan
Expand Down Expand Up @@ -7843,6 +7926,11 @@ export async function processGitHubWebhook(

if (await maybeHandleReactionWebhookEvent(env, deliveryId, eventName, payload)) return;

// #9737: an `issues.labeled` event carrying the priority label is judged against the issue's AUTHOR
// before anything else looks at it -- the label is the scoring input, so the sooner an ineligible one
// is removed the less it can be acted on.
if (await maybeHandlePriorityLabelEligibility(env, deliveryId, eventName, payload)) return;

if (
await maybeHandleIssueCommentCommandWebhookEvent(env, deliveryId, eventName, payload)
)
Expand Down
Loading
Loading