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
49 changes: 49 additions & 0 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@
import { buildRemediationPlan } from "../services/remediation-plan";
import { explainScoreBreakdown } from "../services/score-breakdown";
import { loadOrComputeIssueQualityResponse } from "../services/issue-quality";
import { loadOrComputeBurdenForecastResponse } from "../services/burden-forecast";

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

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.

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

View check run for this annotation

Deleted GitHub App / Gittensory Context

Open PR queue is busy

This repo has a busy open PR queue in the local Gittensory cache.
import { buildMcpClientTelemetry } from "../services/client-telemetry";
import { loadOrComputeRepoOutcomePatternsResponse } from "../services/repo-outcome-patterns";
import { buildRepoOutcomeCalibration } from "../services/outcome-calibration";
import { buildUnavailableQueueTrendReport } from "../services/queue-trends";
import {
applyMcpPlanningChoices,
Expand Down Expand Up @@ -137,6 +138,12 @@
repo: z.string().min(1),
};

const ownerRepoWindowShape = {
owner: z.string().min(1),
repo: z.string().min(1),
windowDays: z.number().int().positive().optional(),
};

const loginShape = {
login: z.string().min(1),
};
Expand Down Expand Up @@ -587,6 +594,16 @@
report: z.unknown().optional(),
};

const maintainerMeasurementReportOutputSchema = {
repoFullName: z.string().optional(),
generatedAt: z.string().optional(),
windowDays: z.number().nullable().optional(),
slop: z.unknown().optional(),
recommendations: z.unknown().optional(),
signals: z.array(z.string()).optional(),
status: z.string().optional(),
};

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

server.registerTool(
"gittensory_get_outcome_calibration",
{
description:
"Return slop-band and recommendation outcome calibration for a repo: whether higher-slop bands merge less often and how agent recommendations are panning out. Maintainer-authenticated; measurement only.",
inputSchema: ownerRepoWindowShape,
outputSchema: maintainerMeasurementReportOutputSchema,
},
async (input) => this.toolResult(await this.getOutcomeCalibration(input)),
);

server.registerTool(
"gittensory_get_contributor_profile",
{
Expand Down Expand Up @@ -1884,6 +1912,27 @@
};
}

private async getOutcomeCalibration(input: { owner: string; repo: string; windowDays?: number | undefined }): Promise<ToolPayload> {
const fullName = `${input.owner}/${input.repo}`;
await this.requireRepoAccess(fullName);
const report = await buildRepoOutcomeCalibration(
this.env,
fullName,
input.windowDays !== undefined ? input.windowDays : undefined,
);
const slop = report.slop;
const summary =
slop.discriminates === true
? `Outcome calibration for ${fullName}: slop bands are predictive across ${slop.totalResolved} resolved PRs.`
: slop.discriminates === false
? `Outcome calibration for ${fullName}: slop bands are NOT discriminating on current data (${slop.totalResolved} resolved PRs).`
: `Outcome calibration for ${fullName}: not enough resolved PR data to judge slop calibration yet.`;
return {
summary,
data: report as unknown as Record<string, unknown>,
};
}

private async loadOpenQueueCounts(fullName: string): Promise<{ openIssues: number; openPullRequests: number }> {
const [totals, openIssues, openPullRequests] = await Promise.all([
getLatestRepoGithubTotalsSnapshot(this.env, fullName),
Expand Down
1 change: 1 addition & 0 deletions test/integration/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4869,9 +4869,10 @@
expect(toolNames).toContain("gittensory_get_decision_pack");
expect(toolNames).toContain("gittensory_explain_repo_decision");
expect(toolNames).toContain("gittensory_preflight_pr");
expect(toolNames).toContain("gittensory_preflight_local_diff");

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

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.

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

View check run for this annotation

Deleted GitHub App / Gittensory Context

Open PR queue is busy

This repo has a busy open PR queue in the local Gittensory cache.
expect(toolNames).toContain("gittensory_preview_local_pr_score");
expect(toolNames).toContain("gittensory_explain_score_breakdown");
expect(toolNames).toContain("gittensory_get_outcome_calibration");
expect(toolNames).toContain("gittensory_get_registry_changes");
expect(toolNames).toContain("gittensory_get_upstream_drift");
expect(toolNames).toContain("gittensory_explain_review_risk");
Expand Down
28 changes: 27 additions & 1 deletion test/unit/mcp-output-schemas.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";

Check notice on line 1 in test/unit/mcp-output-schemas.test.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.

Check notice on line 1 in test/unit/mcp-output-schemas.test.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Open PR queue is busy

This repo has a busy open PR queue in the local Gittensory cache.
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { describe, expect, it } from "vitest";
import { persistSignalSnapshot, upsertBounty, upsertIssueFromGitHub, upsertRepositoryFromGitHub } from "../../src/db/repositories";
import { persistSignalSnapshot, upsertBounty, upsertIssueFromGitHub, upsertPullRequestFromGitHub, upsertRepositoryFromGitHub, updatePullRequestSlopAssessment } from "../../src/db/repositories";
import { GittensoryMcp } from "../../src/mcp/server";
import { normalizeRegistryPayload } from "../../src/registry/normalize";
import { persistRegistrySnapshot } from "../../src/registry/sync";
Expand All @@ -13,6 +13,7 @@
"gittensory_get_repo_context",
"gittensory_get_burden_forecast",
"gittensory_get_repo_outcome_patterns",
"gittensory_get_outcome_calibration",
"gittensory_get_contributor_profile",
"gittensory_get_decision_pack",
"gittensory_monitor_open_prs",
Expand Down Expand Up @@ -380,6 +381,31 @@
expect(cached.isError).toBeFalsy();
expect(cached.structuredContent).toMatchObject({ status: "ready", source: "snapshot", freshness: "fresh", repoFullName: "owner/cached" });
});

it("gittensory_get_outcome_calibration returns structured slop calibration 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: "merged clean",
state: "closed",
user: { login: "alice" },
merged_at: "2026-06-01T00:00:00.000Z",
});
await updatePullRequestSlopAssessment(env, "octo/demo", 1, { slopRisk: 0, slopBand: "clean" });
const { client } = await connectTestClient(env);
const result = await client.callTool({
name: "gittensory_get_outcome_calibration",
arguments: { owner: "octo", repo: "demo", windowDays: 30 },
});
expect(result.isError).toBeFalsy();
const data = result.structuredContent as Record<string, unknown>;
expect(data.repoFullName).toBe("octo/demo");
expect(data.windowDays).toBe(30);
expect(data.slop).toBeTruthy();
expect(data.recommendations).toBeTruthy();
expect(Array.isArray(data.signals)).toBe(true);
});
});

// ── Public/private safety ─────────────────────────────────────────────────────
Expand Down
Loading