Skip to content
Closed
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -545,9 +545,10 @@
# Fork PRs run without secrets -- see the identical step's comment in the old single-job validate-code
# (preserved in git history) for the full tokenless-upload rationale (branch-prefix requirement, sha
# override, etc). Condition dropped the push-or-backend check since this job's own `if:` already
# covers it.

Check notice on line 548 in .github/workflows/ci.yml

View check run for this annotation

Loopover ORB / LoopOver Context

Review queue is busy

This repo has a busy review queue in the local Gittensory cache.
- name: Upload coverage to Codecov (fork PR tokenless)
if: ${{ success() && github.event.pull_request.head.repo.fork == true }}
continue-on-error: true
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: ./coverage/lcov.info
Expand All @@ -562,6 +563,7 @@
# after the tests and hard coverage upload have already passed.
- name: Upload Vitest results to Codecov
if: ${{ !cancelled() && github.event.pull_request.head.repo.fork != true }}
continue-on-error: true
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
Expand All @@ -575,6 +577,7 @@
fail_ci_if_error: false
- name: Upload Vitest results to Codecov (fork PR tokenless)
if: ${{ !cancelled() && github.event.pull_request.head.repo.fork == true }}
continue-on-error: true
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: ./reports/junit/vitest.xml
Expand Down
2 changes: 1 addition & 1 deletion packages/loopover-mcp/bin/loopover-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,10 @@
"maintainer-triage": {
id: "maintainer-triage",
title: "Maintainer queue triage",
audience: "maintainers preparing low-noise queue and PR review context",

Check notice on line 123 in packages/loopover-mcp/bin/loopover-mcp.js

View check run for this annotation

Loopover ORB / LoopOver Context

Review queue is busy

This repo has a busy review queue in the local Gittensory cache.
purpose: "Summarize queue risk, prepare review notes, and draft public guidance for human review.",
recommendedPrompts: ["loopover_maintainer_queue_triage", "loopover_maintainer_review_prep", "loopover_maintainer_public_guidance"],
recommendedTools: ["loopover_get_repo_context", "loopover_get_burden_forecast", "loopover_preflight_pr"],
recommendedTools: ["loopover_get_repo_context", "loopover_get_burden_forecast", "loopover_get_skipped_pr_audit", "loopover_preflight_pr"],
boundaries: [
"Human-approved only: prepare summaries and draft guidance; do not post comments, label, close, merge, or edit contributor work.",
"Keep private review context, raw trust context, and authenticated-only evidence out of public snippets.",
Expand Down
70 changes: 5 additions & 65 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,10 @@
import {
buildStaticControlPanelRoleSummary,
canLoginAccessRepo,
loadControlPanelAccessScope,

Check notice on line 188 in src/api/routes.ts

View check run for this annotation

Loopover ORB / LoopOver Context

Review queue is busy

This repo has a busy review queue in the local Gittensory cache.
loadControlPanelRoleSummary,
} from "../services/control-panel-roles";
import { skippedPrAuditQuerySchema, skippedPrAuditRemediation, skippedPrAuditRepoScope, toIsoQueryDate } from "../services/skipped-pr-audit";
import { runFindOpportunities, validateFindOpportunitiesInput, type FindOpportunitiesInput } from "../mcp/find-opportunities";
import { runIssueRagRetrieval, validateIssueRagInput, type IssueRagInput } from "../mcp/issue-rag";
import {
Expand Down Expand Up @@ -278,7 +279,7 @@
import { loadPublicRepoFocusManifest, loadRepoFocusManifest, upsertRepoFocusManifest } from "../signals/focus-manifest-loader";
import { buildRepoOnboardingPackPreviewForRepo } from "../services/repo-onboarding-pack";
import { generateContributorIssueDrafts } from "../services/contributor-issue-draft";
import { buildRepoSettingsPreview, type PublicSurfaceSkipReason } from "../signals/settings-preview";
import { buildRepoSettingsPreview } from "../signals/settings-preview";
import {
buildGittensorConfigRecommendation,
buildRegistrationReadiness,
Expand Down Expand Up @@ -418,16 +419,6 @@

const MAX_LOCAL_BRANCH_REF_CHARS = 256;
const MAX_LOCAL_BRANCH_TEXT_CHARS = 4000;
const PR_VISIBILITY_SKIP_REASONS = [
"surface_off",
"missing_author",
"bot_author",
"ignored_author",
"maintainer_author",
"miner_detection_unavailable",
"not_official_gittensor_miner",
] as const satisfies readonly PublicSurfaceSkipReason[];

const preflightSchema = z.object({
repoFullName: z.string().min(3).max(PREFLIGHT_LIMITS.repoFullNameChars),
contributorLogin: z.string().min(1).max(PREFLIGHT_LIMITS.contributorLoginChars).optional(),
Expand Down Expand Up @@ -500,15 +491,6 @@
})
.strict();

const skippedPrAuditQuerySchema = z
.object({
limit: z.coerce.number().int().optional(),
repoFullName: z.string().trim().min(3).max(200).optional(),
reason: z.enum(PR_VISIBILITY_SKIP_REASONS).optional(),
since: z.string().trim().min(1).max(64).optional(),
})
.strict();

const localBranchChangedFileSchema = z
.object({
path: z.string().min(1).max(MAX_LOCAL_BRANCH_REF_CHARS),
Expand Down Expand Up @@ -1503,11 +1485,11 @@
const sinceIso = parsed.data.since ? toIsoQueryDate(parsed.data.since) : undefined;
if (parsed.data.since && !sinceIso) return c.json({ error: "invalid_since" }, 400);
const requestedRepo = parsed.data.repoFullName;
const repoFullNames = await skippedPrAuditRepoScope(c, identity, summary.roles, requestedRepo);
if (repoFullNames instanceof Response) return repoFullNames;
const scope = await skippedPrAuditRepoScope(c.env, identity, summary.roles, requestedRepo);
if (!scope.ok) return c.json({ error: scope.code }, 403);
const page = await listPrVisibilitySkipAuditEvents(c.env, {
limit: clampInteger(parsed.data.limit ?? 50, 1, 100),
repoFullNames,
repoFullNames: scope.repoFullNames,
reason: parsed.data.reason,
sinceIso,
});
Expand Down Expand Up @@ -5759,48 +5741,6 @@
return gate;
}

async function skippedPrAuditRepoScope(
c: ProtectedRouteContext,
identity: AuthIdentity,
roles: ControlPanelRoleName[],
requestedRepo: string | undefined,
): Promise<string[] | undefined | Response> {
if (identity.kind !== "session" || roles.includes("operator")) return requestedRepo ? [requestedRepo] : undefined;
const scope = await loadControlPanelAccessScope(c.env, identity.actor);
const scopedRepoNames = new Set(scope.repositoryFullNames.map((name) => name.toLowerCase()));
if (requestedRepo) {
return scopedRepoNames.has(requestedRepo.toLowerCase()) ? [requestedRepo] : c.json({ error: "forbidden_repo" }, 403);
}
return scope.repositoryFullNames;
}

function skippedPrAuditRemediation(reason: string): string {
switch (reason) {
case "surface_off":
return "Enable a PR public surface or check runs in repository settings if maintainers want LoopOver to post.";
case "missing_author":
return "Retry after GitHub provides a resolvable pull request author.";
case "bot_author":
return "No action needed; bot-authored pull requests are intentionally kept quiet.";
case "ignored_author":
return "No action needed; the repository manifest explicitly skips review output for this author.";
case "maintainer_author":
return "Enable maintainer-authored PRs in repository settings only if those PRs should receive public GitHub App output.";
case "miner_detection_unavailable":
return "Retry after official Gittensor miner detection recovers; LoopOver skips instead of guessing.";
case "not_official_gittensor_miner":
return "No public action is needed unless the author should be recognized as an official Gittensor miner.";
default:
return "Review repository settings and installation health before reprocessing the pull request.";
}
}

function toIsoQueryDate(value: string): string | undefined {
const timestamp = Date.parse(value);
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : undefined;
}


// Optional Orb-ingest auth (#1285). FAIL-OPEN by default: with no ORB_INGEST_TOKEN configured the ingress stays
// OPEN (matching today's live fleet — deploying this is non-breaking). Once the operator sets the token, the
// collector REQUIRES an exact bearer match, so the write path can be locked down after the matching
Expand Down
101 changes: 100 additions & 1 deletion src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@
isAuthorizedGitHubSessionLogin,
isMcpActuationRepoAllowed,
isMcpReadRepoAllowed,
isMcpReadUnscoped,

Check notice on line 29 in src/mcp/server.ts

View check run for this annotation

Loopover ORB / LoopOver Context

Review queue is busy

This repo has a busy review queue in the local Gittensory cache.
type AuthIdentity,
} from "../auth/security";
import { canLoginAccessRepo, canWatchRepo, loadControlPanelAccessScope, loadControlPanelRoleSummary, type ControlPanelAccessScope } from "../services/control-panel-roles";
import { buildStaticControlPanelRoleSummary, canLoginAccessRepo, canWatchRepo, loadControlPanelAccessScope, loadControlPanelRoleSummary, type ControlPanelAccessScope } from "../services/control-panel-roles";
import { PR_VISIBILITY_SKIP_REASONS, skippedPrAuditRemediation, skippedPrAuditRepoScope, toIsoQueryDate } from "../services/skipped-pr-audit";
import {
countOpenIssues,
countPendingAgentActions,
Expand Down Expand Up @@ -61,6 +62,7 @@
listOpenPullRequests,
listPullRequests,
listRecentMergedPullRequests,
listPrVisibilitySkipAuditEvents,
listRepoSyncSegments,
listRepoSyncStates,
listRepositories,
Expand Down Expand Up @@ -170,6 +172,7 @@
import { buildEnrichmentAnalyzersTaxonomyDocument, ENRICHMENT_ANALYZERS_URI } from "../review/enrichment-analyzers-taxonomy";
import { recordPredictedGateCall } from "../review/predicted-gate-calls";
import { computeContributorCalibration } from "../review/predicted-gate-calibration-ledger";
import { nowIso } from "../utils/json";

type AppContext = Context<{ Bindings: Env }>;
type ToolPayload = {
Expand All @@ -195,6 +198,13 @@
windowDays: z.number().int().positive().optional(),
};

const skippedPrAuditShape = {
repoFullName: z.string().trim().min(3).max(200).optional(),
reason: z.enum(PR_VISIBILITY_SKIP_REASONS).optional(),
since: z.string().trim().min(1).max(64).optional(),
limit: z.number().int().optional(),
};

const windowOnlyShape = {
windowDays: z.number().int().positive().optional(),
};
Expand Down Expand Up @@ -815,6 +825,30 @@
signals: z.array(z.string()).optional(),
};

const skippedPrAuditOutputSchema = {
generatedAt: z.string().optional(),
limit: z.number().optional(),
hasMore: z.boolean().optional(),
filters: z
.object({
repoFullName: z.string().nullable().optional(),
reason: z.string().nullable().optional(),
since: z.string().nullable().optional(),
})
.optional(),
items: z
.array(
z.object({
repoFullName: z.string(),
pullNumber: z.number(),
reason: z.string(),
timestamp: z.string(),
remediation: z.string(),
}),
)
.optional(),
};

const contributorProfileOutputSchema = {
login: z.string().optional(),
github: z.unknown().optional(),
Expand Down Expand Up @@ -1679,6 +1713,17 @@
async (input) => this.toolResult(await this.getGatePrecision(input)),
);

server.registerTool(
"loopover_get_skipped_pr_audit",
{
description:
"Return the read-only skipped-PR audit trail: PRs the automated reviewer skipped publicly, with reason codes and optional repo/reason/since filters. Maintainer-authenticated; measurement only.",
inputSchema: skippedPrAuditShape,
outputSchema: skippedPrAuditOutputSchema,
},
async (input) => this.toolResult(await this.getSkippedPrAudit(input)),
);

server.registerTool(
"loopover_get_fleet_analytics",
{
Expand Down Expand Up @@ -2975,6 +3020,55 @@
};
}

private async getSkippedPrAudit(input: {
repoFullName?: string | undefined;
reason?: (typeof PR_VISIBILITY_SKIP_REASONS)[number] | undefined;
since?: string | undefined;
limit?: number | undefined;
}): Promise<ToolPayload> {
const roleSummary =
this.identity.kind === "session"
? await loadControlPanelRoleSummary(this.env, this.identity.actor)
: buildStaticControlPanelRoleSummary(this.identity.actor);
if ((this.identity.kind === "static" && this.identity.actor === "mcp") || !roleSummary.roles.some((role) => ["maintainer", "owner", "operator"].includes(role))) {
throw new Error("Forbidden: maintainer access is required for the skipped PR audit.");
}
const sinceIso = input.since ? toIsoQueryDate(input.since) : undefined;
if (input.since && !sinceIso) {
throw new Error("Invalid request: since must be a valid date string.");
}
const scope = await skippedPrAuditRepoScope(this.env, this.identity, roleSummary.roles, input.repoFullName);
if (!scope.ok) {
throw new Error("Forbidden: maintainer access is required for this repository.");
}
const page = await listPrVisibilitySkipAuditEvents(this.env, {
limit: clampInteger(input.limit ?? 50, 1, 100),
repoFullNames: scope.repoFullNames,
reason: input.reason,
sinceIso,
});
return {
summary: `LoopOver skipped-PR audit returned ${page.items.length} entr${page.items.length === 1 ? "y" : "ies"}.`,
data: {
generatedAt: nowIso(),
limit: page.limit,
hasMore: page.hasMore,
filters: {
repoFullName: input.repoFullName ?? null,
reason: input.reason ?? null,
since: sinceIso ?? null,
},
items: page.items.map((item) => ({
repoFullName: item.repoFullName,
pullNumber: item.pullNumber,
reason: item.reason,
timestamp: item.createdAt,
remediation: skippedPrAuditRemediation(item.reason),
})),
},
};
}

// #2224 - surface the deterministic open-PR pressure simulator over MCP. Pure and read-only: the caller
// supplies all queue/role context, so nothing beyond a computation on that input is revealed and no repo
// access is required (mirrors loopover_run_local_scorer). Output is already public-safe - every scenario
Expand Down Expand Up @@ -4080,6 +4174,11 @@
);
}

function clampInteger(value: number, min: number, max: number): number {
const floored = Number.isFinite(value) ? Math.trunc(value) : min;
return Math.min(max, Math.max(min, floored));
}

async function authenticateMcpRequest(c: AppContext): Promise<AuthIdentity | null> {
const identity = await authenticatePrivateToken(c.env, extractBearerToken(c.req.header("authorization")));
if (!identity || identity.kind !== "session") return identity;
Expand Down
73 changes: 73 additions & 0 deletions src/services/skipped-pr-audit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { z } from "zod";

Check notice on line 1 in src/services/skipped-pr-audit.ts

View check run for this annotation

Loopover ORB / LoopOver Context

Review queue is busy

This repo has a busy review queue in the local Gittensory cache.
import { loadControlPanelAccessScope } from "./control-panel-roles";
import type { AuthIdentity } from "../auth/security";
import type { ControlPanelRoleName } from "../types";
import type { PublicSurfaceSkipReason } from "../signals/settings-preview";

export const PR_VISIBILITY_SKIP_REASONS = [
"surface_off",
"missing_author",
"bot_author",
"ignored_author",
"maintainer_author",
"miner_detection_unavailable",
"not_official_gittensor_miner",
] as const satisfies readonly PublicSurfaceSkipReason[];

export const skippedPrAuditQuerySchema = z
.object({
limit: z.coerce.number().int().optional(),
repoFullName: z.string().trim().min(3).max(200).optional(),
reason: z.enum(PR_VISIBILITY_SKIP_REASONS).optional(),
since: z.string().trim().min(1).max(64).optional(),
})
.strict();

export type SkippedPrAuditScopeResult =
| { ok: true; repoFullNames: string[] | undefined }
| { ok: false; code: "forbidden_repo" };

export async function skippedPrAuditRepoScope(
env: Env,
identity: AuthIdentity,
roles: ControlPanelRoleName[],
requestedRepo: string | undefined,
): Promise<SkippedPrAuditScopeResult> {
if (identity.kind !== "session" || roles.includes("operator")) {
return { ok: true, repoFullNames: requestedRepo ? [requestedRepo] : undefined };
}
const scope = await loadControlPanelAccessScope(env, identity.actor);
const scopedRepoNames = new Set(scope.repositoryFullNames.map((name) => name.toLowerCase()));
if (requestedRepo) {
return scopedRepoNames.has(requestedRepo.toLowerCase())
? { ok: true, repoFullNames: [requestedRepo] }
: { ok: false, code: "forbidden_repo" };
}
return { ok: true, repoFullNames: scope.repositoryFullNames };
}

export function skippedPrAuditRemediation(reason: string): string {
switch (reason) {
case "surface_off":
return "Enable a PR public surface or check runs in repository settings if maintainers want LoopOver to post.";
case "missing_author":
return "Retry after GitHub provides a resolvable pull request author.";
case "bot_author":
return "No action needed; bot-authored pull requests are intentionally kept quiet.";
case "ignored_author":
return "No action needed; the repository manifest explicitly skips review output for this author.";
case "maintainer_author":
return "Enable maintainer-authored PRs in repository settings only if those PRs should receive public GitHub App output.";
case "miner_detection_unavailable":
return "Retry after official Gittensor miner detection recovers; LoopOver skips instead of guessing.";
case "not_official_gittensor_miner":
return "No public action is needed unless the author should be recognized as an official Gittensor miner.";
default:
return "Review repository settings and installation health before reprocessing the pull request.";
}
}

export function toIsoQueryDate(value: string): string | undefined {
const timestamp = Date.parse(value);
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : undefined;
}
1 change: 1 addition & 0 deletions test/integration/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5308,9 +5308,10 @@
expect(toolNames).toContain("loopover_get_maintainer_noise");
expect(toolNames).toContain("loopover_get_label_audit");
expect(toolNames).toContain("loopover_get_maintainer_lane");
expect(toolNames).toContain("loopover_get_repo_onboarding_pack");

Check notice on line 5311 in test/integration/api.test.ts

View check run for this annotation

Loopover ORB / LoopOver Context

Review queue is busy

This repo has a busy review queue in the local Gittensory cache.
expect(toolNames).toContain("loopover_get_issue_quality");
expect(toolNames).toContain("loopover_get_burden_forecast");
expect(toolNames).toContain("loopover_get_skipped_pr_audit");
expect(toolNames).toContain("loopover_get_contributor_profile");
expect(toolNames).toContain("loopover_get_decision_pack");
expect(toolNames).toContain("loopover_explain_repo_decision");
Expand Down
Loading
Loading