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
32 changes: 32 additions & 0 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import { buildMcpClientTelemetry } from "../services/client-telemetry";
import { loadOrComputeRepoOutcomePatternsResponse } from "../services/repo-outcome-patterns";
import { buildRepoOutcomeCalibration, outcomeCalibrationSummary } from "../services/outcome-calibration";
import { computeFleetAnalytics } from "../orb/analytics";
import { loadMaintainerNoiseReport, maintainerNoiseSummary } from "../services/maintainer-noise";
import { buildUnavailableQueueTrendReport } from "../services/queue-trends";
import {
applyMcpPlanningChoices,
Expand Down Expand Up @@ -599,6 +600,17 @@ const repoContextOutputSchema = {
dataQuality: z.unknown().optional(),
};

const maintainerNoiseOutputSchema = {
repoFullName: z.string().optional(),
generatedAt: z.string().optional(),
score: z.number().optional(),
level: z.string().optional(),
noiseSources: z.array(z.string()).optional(),
maintainerActions: z.array(z.string()).optional(),
queueHealth: z.unknown().optional(),
summary: z.string().optional(),
};

const freshnessResponseOutputSchema = {
status: z.string().optional(),
repoFullName: z.string().optional(),
Expand Down Expand Up @@ -1040,6 +1052,16 @@ export class GittensoryMcp {
async (input) => this.toolResult(await this.getRepoContext(input)),
);

server.registerTool(
"gittensory_get_maintainer_noise",
{
description: "Return the maintainer queue-noise triage report for a repo: a noise score/level, the specific noise sources to clear first, and recommended maintainer actions. Maintainer-authenticated; advisory only.",
inputSchema: ownerRepoShape,
outputSchema: maintainerNoiseOutputSchema,
},
async (input) => this.toolResult(await this.getMaintainerNoise(input)),
);

server.registerTool(
"gittensory_get_burden_forecast",
{
Expand Down Expand Up @@ -1797,6 +1819,16 @@ export class GittensoryMcp {
};
}

private async getMaintainerNoise(input: { owner: string; repo: string }): Promise<ToolPayload> {
const fullName = `${input.owner}/${input.repo}`;
await this.requireRepoAccess(fullName);
const report = await loadMaintainerNoiseReport(this.env, fullName);
return {
summary: maintainerNoiseSummary(report),
data: report as unknown as Record<string, unknown>,
};
}

private async getBurdenForecast(input: { owner: string; repo: string }): Promise<ToolPayload> {
const fullName = `${input.owner}/${input.repo}`;
await this.requireRepoAccess(fullName);
Expand Down
20 changes: 20 additions & 0 deletions src/services/maintainer-noise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getRepository, listIssueSignalSample, listOpenPullRequests, listRecentMergedPullRequests } from "../db/repositories";
import { buildMaintainerNoiseReport, type MaintainerNoiseReport } from "../signals/reward-risk";

// Maintainer triage signal: which queue-noise sources to clear FIRST (open PRs without linked-issue context,
// broad/churn-style diffs, duplicate clusters, contributor-intake pressure). The deterministic builder already
// exists and powers the `@gittensory noise-report` PR command; this load-or-compute wrapper makes the same
// report available to the MCP tool surface (agent / CLI), mirroring the outcome-calibration serving (#1174).
export async function loadMaintainerNoiseReport(env: Env, fullName: string): Promise<MaintainerNoiseReport> {
const [repo, issues, pullRequests, recentMergedPullRequests] = await Promise.all([
getRepository(env, fullName),
listIssueSignalSample(env, fullName),
listOpenPullRequests(env, fullName),
listRecentMergedPullRequests(env, fullName),
]);
return buildMaintainerNoiseReport(repo, issues, pullRequests, recentMergedPullRequests, fullName);
}

export function maintainerNoiseSummary(report: MaintainerNoiseReport): string {
return `Gittensory maintainer noise report for ${report.repoFullName}: ${report.level} noise (score ${report.score}); ${report.noiseSources.length} source(s) to triage.`;
}
1 change: 1 addition & 0 deletions test/integration/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4875,6 +4875,7 @@ describe("api routes", () => {
const toolsPayload = (await mcpJson(toolsList)) as { result: { tools: Array<{ name: string }> } };
const toolNames = toolsPayload.result.tools.map((tool) => tool.name);
expect(toolNames).toContain("gittensory_get_repo_context");
expect(toolNames).toContain("gittensory_get_maintainer_noise");
expect(toolNames).toContain("gittensory_get_issue_quality");
expect(toolNames).toContain("gittensory_get_burden_forecast");
expect(toolNames).toContain("gittensory_get_contributor_profile");
Expand Down
46 changes: 46 additions & 0 deletions test/unit/maintainer-noise.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, expect, it } from "vitest";
import { upsertPullRequestFromGitHub, upsertRepositoryFromGitHub } from "../../src/db/repositories";
import { loadMaintainerNoiseReport, maintainerNoiseSummary } from "../../src/services/maintainer-noise";
import { createTestEnv } from "../helpers/d1";

describe("maintainer noise report serving", () => {
it("loads repo signals and computes the noise report on demand", async () => {
const env = createTestEnv();
await upsertRepositoryFromGitHub(env, { name: "demo", full_name: "octo/demo", private: false, owner: { login: "octo" }, default_branch: "main" });
// An open PR with no linked issue and a broad/churn-style title → queue-noise sources.
await upsertPullRequestFromGitHub(env, "octo/demo", { number: 1, title: "misc cleanup and various refactors across modules", state: "open", user: { login: "alice" }, body: "" });
const report = await loadMaintainerNoiseReport(env, "octo/demo");
expect(report.repoFullName).toBe("octo/demo");
expect(report.score).toBeGreaterThan(0);
expect(["low", "medium", "high", "critical"]).toContain(report.level);
expect(report.noiseSources.length).toBeGreaterThan(0);
// Public-safe: no private economic/identity terms leak through.
expect(JSON.stringify(report)).not.toMatch(/wallet|hotkey|coldkey|payout|reward|trust score/i);
});

it("returns a clean-queue report (no noise sources) for a repo with no open PRs", async () => {
const env = createTestEnv();
await upsertRepositoryFromGitHub(env, { name: "quiet", full_name: "octo/quiet", private: false, owner: { login: "octo" }, default_branch: "main" });
const report = await loadMaintainerNoiseReport(env, "octo/quiet");
expect(report.repoFullName).toBe("octo/quiet");
expect(report.level).toBe("low");
expect(report.noiseSources).toEqual([expect.stringMatching(/No major maintainer-noise source/i)]);
});

it("renders a public-safe one-line summary", () => {
const summary = maintainerNoiseSummary({
repoFullName: "octo/demo",
generatedAt: "2026-06-01T00:00:00.000Z",
score: 42,
level: "medium",
noiseSources: ["a", "b"],
maintainerActions: ["review_now"],
queueHealth: {} as never,
summary: "",
});
expect(summary).toContain("octo/demo");
expect(summary).toContain("medium");
expect(summary).toContain("42");
expect(summary).toContain("2 source(s)");
});
});
16 changes: 16 additions & 0 deletions test/unit/mcp-output-schemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { createTestEnv } from "../helpers/d1";
// Tools that ship an MCP-native output schema so modern clients can validate/render responses.
const TOOLS_WITH_OUTPUT_SCHEMA = [
"gittensory_get_repo_context",
"gittensory_get_maintainer_noise",
"gittensory_get_burden_forecast",
"gittensory_get_repo_outcome_patterns",
"gittensory_get_outcome_calibration",
Expand Down Expand Up @@ -184,6 +185,21 @@ describe("MCP tool calls return schema-valid structured content", () => {
expect(data.repoFullName).toBe("octo/demo");
});

it("gittensory_get_maintainer_noise returns a structured noise triage report for a repo", async () => {
const env = createTestEnv();
await upsertRepositoryFromGitHub(env, { name: "demo", full_name: "octo/demo", private: false, owner: { login: "octo" }, default_branch: "main" });
await upsertPullRequestFromGitHub(env, "octo/demo", { number: 1, title: "misc cleanup and various refactors", state: "open", user: { login: "alice" }, body: "" });
const { client } = await connectTestClient(env);
const result = await client.callTool({ name: "gittensory_get_maintainer_noise", arguments: { owner: "octo", repo: "demo" } });
expect(result.isError).toBeFalsy();
const data = result.structuredContent as Record<string, unknown>;
expect(data.repoFullName).toBe("octo/demo");
expect(typeof data.score).toBe("number");
expect(typeof data.level).toBe("string");
expect(Array.isArray(data.noiseSources)).toBe(true);
expect(JSON.stringify(data)).not.toMatch(/hotkey|coldkey|wallet|payout|reward/i);
});

it("gittensory_validate_linked_issue reports multiplier eligibility for an uncached issue", async () => {
const { client } = await connectTestClient();
const result = await client.callTool({ name: "gittensory_validate_linked_issue", arguments: { owner: "octo", repo: "demo", issueNumber: 1 } });
Expand Down
Loading