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
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@
"@lovable.dev/vite-plugin-dev-server-bridge": "1.0.1",
"@lovable.dev/vite-plugin-hmr-gate": "1.0.1",
"esbuild": "^0.28.1",
"ws": "^8.21.0",
"tar": "^7.5.16",
"js-yaml": "^4.2.0",
"lovable-tagger@1.2.0": {
"esbuild": "^0.28.1"
},
Expand Down
8 changes: 8 additions & 0 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createMcpHandler } from "agents/mcp";

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

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Titles/paths share 7 meaningful terms.

Check notice on line 1 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 1 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 type { Context } from "hono";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
Expand Down Expand Up @@ -1550,6 +1550,13 @@
loadOrComputeIssueQualityResponse(this.env, repoFullName),
loadRepoFocusManifest(this.env, repoFullName),
]);
// Parity with the maintainer gate: only CONFIRMED Gittensor contributors are ever hard-blocked, so the
// prediction must know the caller's own confirmed status — otherwise it over-reports `failure` for a
// non-confirmed contributor whose synthetic PR trips a blocker. Resolve it the same way the pipeline
// does (official Gittensor API → confirmed). The oss-anti-slop pack drops the contributor gate entirely,
// so skip the lookup there (keeps the prediction account-free for non-Gittensor adopters).
const pack = manifest.gate.pack ?? "gittensor";
const confirmedContributor = pack === "oss-anti-slop" ? undefined : (await fetchGittensorContributorSnapshot(input.login)) !== null;
const verdict = buildPredictedGateVerdict({
input: {
repoFullName,
Expand All @@ -1565,6 +1572,7 @@
pullRequests,
bounties,
issueQuality: issueQuality?.report,
confirmedContributor,
});
return {
summary: `Predicted Gittensory gate for ${repoFullName} under the ${verdict.pack} pack: ${verdict.conclusion}.`,
Expand Down
82 changes: 81 additions & 1 deletion test/unit/mcp-predict-gate.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";

Check notice on line 1 in test/unit/mcp-predict-gate.test.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Titles/paths share 7 meaningful terms.

Check notice on line 1 in test/unit/mcp-predict-gate.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-predict-gate.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 { afterEach, describe, expect, it, vi } from "vitest";
import { GittensoryMcp } from "../../src/mcp/server";
import { createSessionForGitHubUser, type AuthIdentity } from "../../src/auth/security";
import { upsertRepoFocusManifest } from "../../src/signals/focus-manifest-loader";
Expand Down Expand Up @@ -46,6 +46,86 @@
expect((minimal.structuredContent as { pack: string }).pack).toBe("oss-anti-slop");
});

// Parity regression (#627-class): under the default `gittensor` pack, only CONFIRMED Gittensor
// contributors are ever hard-blocked. The prediction must resolve the caller's confirmed status — if it
// doesn't (the bug), a non-confirmed contributor whose synthetic PR trips a blocker is wrongly told
// `failure` when the real maintainer gate would return `neutral`.
describe("contributor-confirmation parity under the gittensor pack", () => {
afterEach(() => vi.unstubAllGlobals());

function stubGittensorMiners(confirmedLogins: Array<{ login: string; id: number }>) {
vi.stubGlobal("fetch", async (input: RequestInfo | URL) => {
const url = input.toString();
// The miners LIST endpoint decides confirmation; follow-up detail/prs/issues calls are best-effort.
if (/\/miners$/.test(url)) {
return Response.json(confirmedLogins.map((m) => ({ githubId: m.id, githubUsername: m.login })));
}
return Response.json([]);
});
}

it("stays NEUTRAL for a non-confirmed contributor even when a blocker fires", async () => {
const env = createTestEnv();
await upsertRepositoryFromGitHub(env, { name: "widgets", full_name: "acme/widgets" });
// gittensor pack, linked-issue blocks; the contributor supplies no linked issue → blocker fires.
await upsertRepoFocusManifest(env, "acme/widgets", { gate: { pack: "gittensor", linkedIssue: "block" } });
stubGittensorMiners([]); // miner1 is NOT a confirmed Gittensor contributor
const client = await connect(env);

const result = await client.callTool({
name: "gittensory_predict_gate",
arguments: { login: "miner1", owner: "acme", repo: "widgets", title: "Add retry to upload client", linkedIssues: [] },
});
expect(result.isError).toBeFalsy();
const data = result.structuredContent as { pack: string; conclusion: string; confirmedContributor: boolean | undefined };
expect(data.pack).toBe("gittensor");
expect(data.conclusion).toBe("neutral");
expect(data.confirmedContributor).toBe(false);
});

it("predicts FAILURE for a confirmed contributor when the same blocker fires", async () => {
const env = createTestEnv();
await upsertRepositoryFromGitHub(env, { name: "widgets", full_name: "acme/widgets" });
await upsertRepoFocusManifest(env, "acme/widgets", { gate: { pack: "gittensor", linkedIssue: "block" } });
stubGittensorMiners([{ login: "miner1", id: 4242 }]); // miner1 IS confirmed
const client = await connect(env);

const result = await client.callTool({
name: "gittensory_predict_gate",
arguments: { login: "miner1", owner: "acme", repo: "widgets", title: "Add retry to upload client", linkedIssues: [] },
});
expect(result.isError).toBeFalsy();
const data = result.structuredContent as { conclusion: string; confirmedContributor: boolean | undefined; blockers: Array<{ code: string }> };
expect(data.confirmedContributor).toBe(true);
expect(data.conclusion).toBe("failure");
expect(data.blockers.some((b) => b.code === "missing_linked_issue")).toBe(true);
});

it("treats a Gittensor API failure as non-confirmed (fail-safe NEUTRAL, never a false FAILURE)", async () => {
const env = createTestEnv();
await upsertRepositoryFromGitHub(env, { name: "widgets", full_name: "acme/widgets" });
// No explicit pack → defaults to the gittensor pack, which still resolves confirmed status via the API.
await upsertRepoFocusManifest(env, "acme/widgets", { gate: { linkedIssue: "block" } });
// The confirmation lookup is the only network call on the prediction path (the URL is a fixed constant
// base; the login is never interpolated into it — it is filtered client-side — so there is no SSRF
// surface). When that call fails/times out, fetchGittensorContributorSnapshot resolves to null, so the
// contributor is treated as non-confirmed → the gate stays neutral rather than wrongly blocking them.
vi.stubGlobal("fetch", async () => {
throw new Error("network down");
});
const client = await connect(env);

const result = await client.callTool({
name: "gittensory_predict_gate",
arguments: { login: "miner1", owner: "acme", repo: "widgets", title: "Add retry to upload client", linkedIssues: [] },
});
expect(result.isError).toBeFalsy();
const data = result.structuredContent as { conclusion: string; confirmedContributor: boolean | undefined };
expect(data.confirmedContributor).toBe(false);
expect(data.conclusion).toBe("neutral");
});
});

it("is repo-scoped: a session cannot predict against an inaccessible repo", async () => {
const env = createTestEnv();
await upsertRepositoryFromGitHub(env, { name: "private-roadmap", full_name: "victimco/private-roadmap", private: true, owner: { login: "victimco" } });
Expand Down
Loading