From 75cf7ef10c5ea7708f2acdae5d285d479647bf14 Mon Sep 17 00:00:00 2001 From: oktofeesh1 <287075021+oktofeesh1@users.noreply.github.com> Date: Fri, 26 Jun 2026 03:33:58 -0700 Subject: [PATCH] feat(mcp): expose repo label-policy audit via gittensory_get_label_audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The deterministic label audit (`buildLabelAudit`, src/signals/engine.ts) reports whether a repo's configured (.gittensory.yml / dashboard) label set matches the live GitHub labels and is trustworthy for label-multiplier scoring — surfacing missing configured labels, suspicious status/source-style labels, and the overall trusted-label-pipeline readiness. It already powers the repo-intelligence response but had no MCP surface, so agents/CLI couldn't pull it (it is also absent from gittensory_get_repo_context, which exposes a different subset). Add `gittensory_get_label_audit` (maintainer-authenticated via the existing requireRepoAccess, advisory only), mirroring the merged maintainer-noise (#1414) and outcome-calibration (#1174) serving: a thin load-or-compute service (src/services/label-audit.ts) that loads the repo's labels + cached signals and runs the existing builder, plus the MCP tool registration + output schema + method. Output flows through the existing redactSensitiveForMcp wrapper. No new REST route, no migration, no auth-policy change — purely additive to the MCP surface. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/mcp/server.ts | 34 ++++++++++++++++++++++ src/services/label-audit.ts | 21 ++++++++++++++ test/integration/api.test.ts | 1 + test/unit/label-audit.test.ts | 43 ++++++++++++++++++++++++++++ test/unit/mcp-output-schemas.test.ts | 14 +++++++++ 5 files changed, 113 insertions(+) create mode 100644 src/services/label-audit.ts create mode 100644 test/unit/label-audit.test.ts diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 8b046e8013..7ae3816977 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -72,6 +72,7 @@ import { loadOrComputeRepoOutcomePatternsResponse } from "../services/repo-outco import { buildRepoOutcomeCalibration, outcomeCalibrationSummary } from "../services/outcome-calibration"; import { computeFleetAnalytics } from "../orb/analytics"; import { loadMaintainerNoiseReport, maintainerNoiseSummary } from "../services/maintainer-noise"; +import { loadLabelAudit, labelAuditSummary } from "../services/label-audit"; import { buildUnavailableQueueTrendReport } from "../services/queue-trends"; import { applyMcpPlanningChoices, @@ -611,6 +612,19 @@ const maintainerNoiseOutputSchema = { summary: z.string().optional(), }; +const labelAuditOutputSchema = { + repoFullName: z.string().optional(), + generatedAt: z.string().optional(), + configuredLabels: z.array(z.string()).optional(), + liveLabels: z.array(z.string()).optional(), + observedLabels: z.array(z.unknown()).optional(), + missingConfiguredLabels: z.array(z.string()).optional(), + suspiciousConfiguredLabels: z.array(z.string()).optional(), + trustedPipelineReady: z.boolean().optional(), + findings: z.array(z.unknown()).optional(), + summary: z.string().optional(), +}; + const freshnessResponseOutputSchema = { status: z.string().optional(), repoFullName: z.string().optional(), @@ -1062,6 +1076,16 @@ export class GittensoryMcp { async (input) => this.toolResult(await this.getMaintainerNoise(input)), ); + server.registerTool( + "gittensory_get_label_audit", + { + description: "Return the repo's label-policy audit: configured-vs-live labels, missing configured labels, suspicious status/source-style labels, and trusted-label-pipeline readiness for label-multiplier scoring. Maintainer-authenticated; advisory only.", + inputSchema: ownerRepoShape, + outputSchema: labelAuditOutputSchema, + }, + async (input) => this.toolResult(await this.getLabelAudit(input)), + ); + server.registerTool( "gittensory_get_burden_forecast", { @@ -1829,6 +1853,16 @@ export class GittensoryMcp { }; } + private async getLabelAudit(input: { owner: string; repo: string }): Promise { + const fullName = `${input.owner}/${input.repo}`; + await this.requireRepoAccess(fullName); + const report = await loadLabelAudit(this.env, fullName); + return { + summary: labelAuditSummary(report), + data: report as unknown as Record, + }; + } + private async getBurdenForecast(input: { owner: string; repo: string }): Promise { const fullName = `${input.owner}/${input.repo}`; await this.requireRepoAccess(fullName); diff --git a/src/services/label-audit.ts b/src/services/label-audit.ts new file mode 100644 index 0000000000..cd2225f972 --- /dev/null +++ b/src/services/label-audit.ts @@ -0,0 +1,21 @@ +import { getRepository, listIssueSignalSample, listOpenPullRequests, listRepoLabels } from "../db/repositories"; +import { buildLabelAudit, type LabelAudit } from "../signals/engine"; + +// Maintainer label-policy health: whether the repo's configured (.gittensory.yml / dashboard) label set matches +// the live GitHub labels and is trustworthy for label-multiplier scoring — surfacing missing configured labels, +// suspicious status/source-style labels, and the overall trusted-label-pipeline readiness. The deterministic +// builder already powers the repo-intelligence response; this load-or-compute wrapper makes the same audit +// available to the MCP tool surface (agent / CLI), mirroring the maintainer-noise / maintainer-lane serving. +export async function loadLabelAudit(env: Env, fullName: string): Promise { + const [repo, labels, issues, pullRequests] = await Promise.all([ + getRepository(env, fullName), + listRepoLabels(env, fullName), + listIssueSignalSample(env, fullName), + listOpenPullRequests(env, fullName), + ]); + return buildLabelAudit(repo, labels, issues, pullRequests, fullName); +} + +export function labelAuditSummary(report: LabelAudit): string { + return `Gittensory label audit for ${report.repoFullName}: trusted-label pipeline ${report.trustedPipelineReady ? "ready" : "not ready"}; ${report.missingConfiguredLabels.length} missing, ${report.suspiciousConfiguredLabels.length} suspicious configured label(s).`; +} diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts index 27be323250..c753a46bc5 100644 --- a/test/integration/api.test.ts +++ b/test/integration/api.test.ts @@ -4876,6 +4876,7 @@ describe("api routes", () => { 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_label_audit"); expect(toolNames).toContain("gittensory_get_issue_quality"); expect(toolNames).toContain("gittensory_get_burden_forecast"); expect(toolNames).toContain("gittensory_get_contributor_profile"); diff --git a/test/unit/label-audit.test.ts b/test/unit/label-audit.test.ts new file mode 100644 index 0000000000..f89e1aa286 --- /dev/null +++ b/test/unit/label-audit.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest"; +import { upsertRepositoryFromGitHub } from "../../src/db/repositories"; +import type { LabelAudit } from "../../src/signals/engine"; +import { labelAuditSummary, loadLabelAudit } from "../../src/services/label-audit"; +import { createTestEnv } from "../helpers/d1"; + +describe("label audit serving", () => { + it("loads repo labels and computes the audit on demand", async () => { + const env = createTestEnv(); + await upsertRepositoryFromGitHub(env, { name: "demo", full_name: "octo/demo", private: false, owner: { login: "octo" }, default_branch: "main" }); + const report = await loadLabelAudit(env, "octo/demo"); + expect(report.repoFullName).toBe("octo/demo"); + expect(Array.isArray(report.configuredLabels)).toBe(true); + expect(Array.isArray(report.suspiciousConfiguredLabels)).toBe(true); + expect(Array.isArray(report.observedLabels)).toBe(true); + expect(typeof report.trustedPipelineReady).toBe("boolean"); + // Public-safe: no private economic/identity terms leak through. + expect(JSON.stringify(report)).not.toMatch(/wallet|hotkey|coldkey|payout|reward/i); + }); + + it("renders a public-safe summary for both trusted-pipeline-readiness states", () => { + const base: LabelAudit = { + repoFullName: "octo/demo", + generatedAt: "2026-06-01T00:00:00.000Z", + configuredLabels: ["bug", "status:ready"], + liveLabels: ["status:ready"], + observedLabels: [], + missingConfiguredLabels: ["bug"], + suspiciousConfiguredLabels: ["status:ready"], + trustedPipelineReady: false, + findings: [], + }; + const notReady = labelAuditSummary(base); + expect(notReady).toContain("octo/demo"); + expect(notReady).toContain("not ready"); + expect(notReady).toContain("1 missing"); + expect(notReady).toContain("1 suspicious"); + + const ready = labelAuditSummary({ ...base, missingConfiguredLabels: [], suspiciousConfiguredLabels: [], trustedPipelineReady: true }); + expect(ready).toContain("pipeline ready"); + expect(ready).not.toContain("not ready"); + }); +}); diff --git a/test/unit/mcp-output-schemas.test.ts b/test/unit/mcp-output-schemas.test.ts index 5a92a925bf..72659b9141 100644 --- a/test/unit/mcp-output-schemas.test.ts +++ b/test/unit/mcp-output-schemas.test.ts @@ -13,6 +13,7 @@ import { createTestEnv } from "../helpers/d1"; const TOOLS_WITH_OUTPUT_SCHEMA = [ "gittensory_get_repo_context", "gittensory_get_maintainer_noise", + "gittensory_get_label_audit", "gittensory_get_burden_forecast", "gittensory_get_repo_outcome_patterns", "gittensory_get_outcome_calibration", @@ -233,6 +234,19 @@ describe("MCP tool calls return schema-valid structured content", () => { expect(result.structuredContent).toBeUndefined(); }); + it("gittensory_get_label_audit returns a structured label-policy audit for a repo", async () => { + const env = createTestEnv(); + await upsertRepositoryFromGitHub(env, { name: "demo", full_name: "octo/demo", private: false, owner: { login: "octo" }, default_branch: "main" }); + const { client } = await connectTestClient(env); + const result = await client.callTool({ name: "gittensory_get_label_audit", arguments: { owner: "octo", repo: "demo" } }); + expect(result.isError).toBeFalsy(); + const data = result.structuredContent as Record; + expect(data.repoFullName).toBe("octo/demo"); + expect(typeof data.trustedPipelineReady).toBe("boolean"); + expect(Array.isArray(data.suspiciousConfiguredLabels)).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 } });