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
34 changes: 34 additions & 0 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { loadMaintainerLaneReport, maintainerLaneSummary } from "../services/maintainer-lane";
import { buildUnavailableQueueTrendReport } from "../services/queue-trends";
import {
applyMcpPlanningChoices,
Expand Down Expand Up @@ -611,6 +612,19 @@ const maintainerNoiseOutputSchema = {
summary: z.string().optional(),
};

const maintainerLaneOutputSchema = {
repoFullName: z.string().optional(),
generatedAt: z.string().optional(),
lane: z.unknown().optional(),
maintainerCut: z.number().optional(),
maintainerCutConfigured: z.boolean().optional(),
queueHealth: z.unknown().optional(),
configQuality: z.unknown().optional(),
contributorIntakeHealth: z.unknown().optional(),
findings: z.array(z.unknown()).optional(),
summary: z.string().optional(),
};

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

server.registerTool(
"gittensory_get_maintainer_lane",
{
description: "Return the maintainer-lane triage report for a repo: the lane recommendation alongside the configured maintainer cut, queue health, config quality, and contributor-intake health. Maintainer-authenticated; advisory only.",
inputSchema: ownerRepoShape,
outputSchema: maintainerLaneOutputSchema,
},
async (input) => this.toolResult(await this.getMaintainerLane(input)),
);

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

private async getMaintainerLane(input: { owner: string; repo: string }): Promise<ToolPayload> {
const fullName = `${input.owner}/${input.repo}`;
await this.requireRepoAccess(fullName);
const report = await loadMaintainerLaneReport(this.env, fullName);
return {
summary: maintainerLaneSummary(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
22 changes: 22 additions & 0 deletions src/services/maintainer-lane.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { getRepository, listIssueSignalSample, listOpenPullRequests, listRecentMergedPullRequests } from "../db/repositories";
import { buildCollisionReport, buildMaintainerLaneReport, type MaintainerLaneReport } from "../signals/engine";

// Maintainer-lane triage synthesis: the lane recommendation in the context of the configured maintainer cut,
// queue health, config quality, and contributor-intake health — i.e. "how should this repo's maintainer treat
// their own lane right now". The deterministic builder already powers the repo-intelligence response; this
// load-or-compute wrapper makes the same report available to the MCP tool surface (agent / CLI), mirroring the
// outcome-calibration / maintainer-noise serving.
export async function loadMaintainerLaneReport(env: Env, fullName: string): Promise<MaintainerLaneReport> {
const [repo, issues, pullRequests, recentMergedPullRequests] = await Promise.all([
getRepository(env, fullName),
listIssueSignalSample(env, fullName),
listOpenPullRequests(env, fullName),
listRecentMergedPullRequests(env, fullName),
]);
const collisions = buildCollisionReport(fullName, issues, pullRequests, recentMergedPullRequests);
return buildMaintainerLaneReport(repo, issues, pullRequests, fullName, collisions);
}

export function maintainerLaneSummary(report: MaintainerLaneReport): string {
return `Gittensory maintainer lane for ${report.repoFullName}: maintainer_cut ${report.maintainerCutConfigured ? "configured" : "not configured"}; contributor intake ${report.contributorIntakeHealth.level}.`;
}
1 change: 1 addition & 0 deletions test/integration/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4900,6 +4900,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_maintainer_lane");
expect(toolNames).toContain("gittensory_get_issue_quality");
expect(toolNames).toContain("gittensory_get_burden_forecast");
expect(toolNames).toContain("gittensory_get_contributor_profile");
Expand Down
57 changes: 57 additions & 0 deletions test/unit/maintainer-lane.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, it } from "vitest";
import { upsertPullRequestFromGitHub, upsertRepositoryFromGitHub } from "../../src/db/repositories";
import { loadMaintainerLaneReport, maintainerLaneSummary } from "../../src/services/maintainer-lane";
import { createTestEnv } from "../helpers/d1";

describe("maintainer lane report serving", () => {
it("loads repo signals and computes the lane report on demand", 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: "Fix retry backoff", state: "open", user: { login: "alice" }, body: "" });
const report = await loadMaintainerLaneReport(env, "octo/demo");
expect(report.repoFullName).toBe("octo/demo");
// No maintainer_cut in the registry config → not configured, and a finding flags it.
expect(report.maintainerCutConfigured).toBe(false);
expect(report.findings.some((finding) => finding.code === "maintainer_cut_not_configured")).toBe(true);
expect(report.lane).toBeTruthy();
expect(report.queueHealth).toBeTruthy();
expect(report.configQuality).toBeTruthy();
expect(typeof report.contributorIntakeHealth.level).toBe("string");
// Public-safe: no private economic/identity terms leak through.
expect(JSON.stringify(report)).not.toMatch(/wallet|hotkey|coldkey|payout|reward|trust score/i);
});

it("renders a public-safe one-line summary", () => {
const summary = maintainerLaneSummary({
repoFullName: "octo/demo",
generatedAt: "2026-06-01T00:00:00.000Z",
lane: {} as never,
maintainerCut: 0,
maintainerCutConfigured: false,
queueHealth: {} as never,
configQuality: {} as never,
contributorIntakeHealth: { level: "healthy" } as never,
summary: "",
findings: [],
});
expect(summary).toContain("octo/demo");
expect(summary).toContain("not configured");
expect(summary).toContain("healthy");

// Cover the configured-cut side of the summary ternary.
const configured = maintainerLaneSummary({
repoFullName: "octo/demo",
generatedAt: "2026-06-01T00:00:00.000Z",
lane: {} as never,
maintainerCut: 0.1,
maintainerCutConfigured: true,
queueHealth: {} as never,
configQuality: {} as never,
contributorIntakeHealth: { level: "developing" } as never,
summary: "",
findings: [],
});
expect(configured).toContain("maintainer_cut configured");
expect(configured).not.toContain("not configured");
});
});
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 @@ -13,6 +13,7 @@ import { createTestEnv } from "../helpers/d1";
const TOOLS_WITH_OUTPUT_SCHEMA = [
"gittensory_get_repo_context",
"gittensory_get_maintainer_noise",
"gittensory_get_maintainer_lane",
"gittensory_get_burden_forecast",
"gittensory_get_repo_outcome_patterns",
"gittensory_get_outcome_calibration",
Expand Down Expand Up @@ -233,6 +234,21 @@ describe("MCP tool calls return schema-valid structured content", () => {
expect(result.structuredContent).toBeUndefined();
});

it("gittensory_get_maintainer_lane returns a structured lane 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: "Fix retry backoff", state: "open", user: { login: "alice" }, body: "" });
const { client } = await connectTestClient(env);
const result = await client.callTool({ name: "gittensory_get_maintainer_lane", 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.maintainerCutConfigured).toBe("boolean");
expect(data.lane).toBeTruthy();
expect(data.contributorIntakeHealth).toBeTruthy();
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