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
2 changes: 1 addition & 1 deletion apps/gittensory-ui/src/lib/mcp-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const MCP_PACKAGE_NAME = "@jsonbored/gittensory-mcp";
export const MCP_PACKAGE_ENCODED_NAME = "@jsonbored%2fgittensory-mcp";
export const MCP_PACKAGE_REGISTRY_URL = `https://registry.npmjs.org/${MCP_PACKAGE_ENCODED_NAME}`;
export const MCP_PACKAGE_NPM_URL = `https://www.npmjs.com/package/${MCP_PACKAGE_NAME}`;
export const MCP_PACKAGE_KNOWN_LATEST_VERSION = "0.5.0";
export const MCP_PACKAGE_KNOWN_LATEST_VERSION = "0.6.0";
export const MCP_MINIMUM_SUPPORTED_VERSION = "0.5.0";

export type NpmPackageMetadata = {
Expand Down
36 changes: 36 additions & 0 deletions packages/gittensory-mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,41 @@
# Changelog

## mcp-v0.6.0 - 2026-06-14

### Features
- Add missing test evidence slop signal (#616)
- Gittensory_validate_linked_issue linked-issue multiplier validator (#622)
- Gittensory_check_before_start pre-start duplicate/solvability check (#621)
- Authoritative .gittensory.yml gate config (config-as-code) (#647)
- Dual-AI maintainer review + BYOK (Phase C) (#652)
- Config-as-code provider/model + maintainer self-serve BYOK routes (#664)
- Event→subscription→delivery service + MCP badge feed (closes #536, advances #535) (#707)
- Policy-pack-pluggable gate — gittensor + oss-anti-slop packs (#692) (#710)
- Agent-native pre-submission self-check as a general product (#693) (#712)
- Surface the earn-on-Gittensor path for oss-anti-slop adopters (#694) (#713)
- Miner-facing post-merge reward & outcome attribution (#702) (#714)
- Wire + modernize the deterministic slop score into the gate, context & MCP (#530/#531/#532) (#716)
- AI-assisted advisory slop layer (advisory-only, never blocks) (#724)
- Issue-side slop triage (#533) (#729)
- Model upstream time-decay, applied behind a default-off flag (#703) (#731)
- Gittensory_lint_pr_text commit/PR-body rubric linter (#634)
- Per-repo time-decay hyperparameters + go live (#703) (#733)
- Active issue-watch monitor — gittensory_watch_issues (#699 path B) (#735)
- Slop-risk + issue-slop self-check tools in the npm package

### Fixes
- Bound local branch scenario inputs (#614)
- Harden manifest public-safe filter (#659)
- Enforce repo scope for gate prediction (#717)
- Bound mark-read ids (#718)
- Visibility-aware access gate for subscriptions + fan-out (#742)

### Refactors
- Shared public-safe redaction module (#542) (#743)

### Chores
- Add outputSchema to every tool that lacks it (#637)

## mcp-v0.5.0 - 2026-06-12

### Features
Expand Down
35 changes: 35 additions & 0 deletions packages/gittensory-mcp/bin/gittensory-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,21 @@ const lintPrTextShape = {
linkedIssue: z.number().int().positive().optional(),
};

const checkSlopRiskShape = {
changedFiles: z
.array(z.object({ path: z.string().min(1).max(400), additions: z.number().int().min(0).optional(), deletions: z.number().int().min(0).optional() }))
.max(2000)
.optional(),
description: z.string().max(20000).optional(),
tests: z.array(z.string().max(400)).max(2000).optional(),
testFiles: z.array(z.string().max(400)).max(2000).optional(),
};

const checkIssueSlopShape = {
title: z.string().max(500).optional(),
body: z.string().max(40000).optional(),
};

const preflightShape = {
repoFullName: z.string().min(3),
contributorLogin: z.string().min(1).optional(),
Expand Down Expand Up @@ -330,6 +345,26 @@ server.registerTool(
async (input) => toolResult("Gittensory PR-text lint.", await apiPost("/v1/lint/pr-text", input)),
);

server.registerTool(
"gittensory_check_slop_risk",
{
description:
"Assess the deterministic slop risk of a planned change from local diff metadata (paths + line counts) + the PR description — an agent-native, source-free quality self-check. Returns slopRisk (0-100), band, findings, and the rubric. No repo data needed.",
inputSchema: checkSlopRiskShape,
},
async (input) => toolResult("Gittensory slop-risk self-check.", await apiPost("/v1/lint/slop-risk", input)),
);

server.registerTool(
"gittensory_check_issue_slop",
{
description:
"Assess the deterministic slop risk of an issue from its title + body alone (no repo data) — flags clearly low-effort issues (empty body, an unfilled template) for triage. Returns slopRisk (0-100), band, findings, and the rubric. Advisory-only.",
inputSchema: checkIssueSlopShape,
},
async (input) => toolResult("Gittensory issue-slop self-check.", await apiPost("/v1/lint/issue-slop", input)),
);

server.registerTool(
"gittensory_preflight_local_diff",
{
Expand Down
2 changes: 1 addition & 1 deletion packages/gittensory-mcp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@jsonbored/gittensory-mcp",
"version": "0.5.0",
"version": "0.6.0",
"license": "AGPL-3.0-only",
"type": "module",
"description": "Local stdio MCP wrapper for the Gittensory Gittensor base-agent.",
Expand Down
36 changes: 35 additions & 1 deletion src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ import { attachDataQuality, buildCoreSignalFidelity, buildFreshnessSloReport, bu
import { buildContributorOpenPrMonitor } from "../signals/contributor-open-pr-monitor";
import { buildPullRequestReviewability, type PullRequestReviewability } from "../signals/reward-risk";
import { buildLocalBranchAnalysis, findCurrentBranchPullRequest } from "../signals/local-branch";
import { buildSlopAssessment, buildIssueSlopAssessment, SLOP_RUBRIC_MARKDOWN, ISSUE_SLOP_RUBRIC_MARKDOWN } from "../signals/slop";
import { buildPredictedGateVerdict } from "../rules/predicted-gate";
import { buildMaintainerActivationPreview, recommendedAdvisoryActivationSettings } from "../services/maintainer-activation";
import { buildRepoOutcomeCalibration } from "../services/outcome-calibration";
Expand Down Expand Up @@ -361,6 +362,22 @@ const lintPrTextSchema = z.object({
linkedIssue: z.number().int().positive().optional(),
});

// Pure local-metadata slop self-checks (no repo data, no secrets) — mirror the gittensory_check_slop_risk /
// gittensory_check_issue_slop MCP tools so the npm package can offer the same agent-native self-check.
const slopRiskSchema = z.object({
changedFiles: z
.array(z.object({ path: z.string().min(1).max(400), additions: z.number().int().min(0).optional(), deletions: z.number().int().min(0).optional() }))
.max(2000)
.optional(),
description: z.string().max(20000).optional(),
tests: z.array(z.string().max(400)).max(2000).optional(),
testFiles: z.array(z.string().max(400)).max(2000).optional(),
});
const issueSlopSchema = z.object({
title: z.string().max(500).optional(),
body: z.string().max(40000).optional(),
});

const skippedPrAuditQuerySchema = z
.object({
limit: z.coerce.number().int().optional(),
Expand Down Expand Up @@ -2113,6 +2130,21 @@ export function createApp() {
return c.json(buildPrTextLint(parsed.data));
});

// Agent-native slop self-checks (#530/#533): pure local-metadata, mirroring the MCP tools of the same name.
app.post("/v1/lint/slop-risk", async (c) => {
const body = await c.req.json().catch(() => null);
const parsed = slopRiskSchema.safeParse(body);
if (!parsed.success) return c.json({ error: "invalid_slop_risk_request", issues: parsed.error.issues }, 400);
return c.json({ ...buildSlopAssessment(parsed.data), rubric: SLOP_RUBRIC_MARKDOWN });
});

app.post("/v1/lint/issue-slop", async (c) => {
const body = await c.req.json().catch(() => null);
const parsed = issueSlopSchema.safeParse(body);
if (!parsed.success) return c.json({ error: "invalid_issue_slop_request", issues: parsed.error.issues }, 400);
return c.json({ ...buildIssueSlopAssessment(parsed.data), rubric: ISSUE_SLOP_RUBRIC_MARKDOWN });
});

app.post("/v1/preflight/pr", async (c) => {
const body = await c.req.json().catch(() => null);
const parsed = preflightSchema.safeParse(body);
Expand Down Expand Up @@ -3973,6 +4005,8 @@ function contributorEvidenceFromProfile(profile: {
const EXTENSION_PULL_CONTEXT_PATH = "/v1/extension/pull-context";
const EXTENSION_PULL_CONTEXT_SCOPE = "extension:pull_context";
const LINT_PR_TEXT_PATH = "/v1/lint/pr-text";
const LINT_SLOP_RISK_PATH = "/v1/lint/slop-risk";
const LINT_ISSUE_SLOP_PATH = "/v1/lint/issue-slop";

type ProtectedRouteContext = {
env: Env;
Expand Down Expand Up @@ -4013,7 +4047,7 @@ function canSessionAccessPath(env: Env, identity: Extract<AuthIdentity, { kind:
if (isRepoAiConfigPath(path)) return true;
if (isRepoCheckBeforeStartPath(path)) return true;
if (isRepoContributorIssueDraftGeneratePath(path)) return true;
if (path === LINT_PR_TEXT_PATH) return true;
if (path === LINT_PR_TEXT_PATH || path === LINT_SLOP_RISK_PATH || path === LINT_ISSUE_SLOP_PATH) return true;
if (path === EXTENSION_PULL_CONTEXT_PATH && isExtensionScopedSession(identity)) return true;
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion src/services/mcp-compatibility.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const GITTENSORY_API_VERSION = "0.1.0";
export const GITTENSORY_MCP_PACKAGE_NAME = "@jsonbored/gittensory-mcp";
export const MINIMUM_SUPPORTED_MCP_VERSION = "0.5.0";
export const LATEST_RECOMMENDED_MCP_VERSION = "0.5.0";
export const LATEST_RECOMMENDED_MCP_VERSION = "0.6.0";

export type McpCompatibilityStatus = "current" | "stale" | "incompatible" | "unknown";

Expand Down
50 changes: 45 additions & 5 deletions test/integration/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ describe("api routes", () => {
status: "ok",
service: "gittensory-api",
minMcpVersion: "0.5.0",
latestRecommendedMcpVersion: "0.5.0",
latestRecommendedMcpVersion: "0.6.0",
});

const compatibility = await app.request("/v1/mcp/compatibility", {}, env);
Expand All @@ -101,8 +101,8 @@ describe("api routes", () => {
mcp: {
packageName: "@jsonbored/gittensory-mcp",
minimumSupportedVersion: "0.5.0",
latestRecommendedVersion: "0.5.0",
latestPackageVersion: "0.5.0",
latestRecommendedVersion: "0.6.0",
latestPackageVersion: "0.6.0",
},
compatibilityWarnings: [],
breakingChanges: [],
Expand Down Expand Up @@ -974,6 +974,46 @@ describe("api routes", () => {
const invalidLintPrText = await app.request("/v1/lint/pr-text", { method: "POST", headers: apiHeaders(env), body: JSON.stringify({ linkedIssue: -1 }) }, env);
expect(invalidLintPrText.status).toBe(400);

// Agent-native slop self-checks (mirror the gittensory_check_slop_risk / gittensory_check_issue_slop MCP tools).
const slopRisk = await app.request(
"/v1/lint/slop-risk",
{ method: "POST", headers: apiHeaders(env), body: JSON.stringify({ changedFiles: [{ path: "src/widget.ts", additions: 80, deletions: 2 }], description: "" }) },
env,
);
expect(slopRisk.status).toBe(200);
const slopRiskBody = await slopRisk.json();
expect(slopRiskBody).toMatchObject({ slopRisk: expect.any(Number), band: expect.stringMatching(/clean|low|elevated|high/), findings: expect.any(Array), rubric: expect.any(String) });
expect(JSON.stringify(slopRiskBody)).not.toMatch(/hotkey|coldkey|wallet|payout|reward/i);

// Session identities (not just the API token) can reach it — it is allowlisted like the other local self-checks.
const { token: slopSessionToken } = await createSessionForGitHubUser(env, { login: "slop-mcp-user", id: 4343 });
const sessionSlopRisk = await app.request(
"/v1/lint/slop-risk",
{ method: "POST", headers: { authorization: `Bearer ${slopSessionToken}`, "content-type": "application/json" }, body: JSON.stringify({ changedFiles: [] }) },
env,
);
expect(sessionSlopRisk.status).toBe(200);

const invalidSlopRisk = await app.request("/v1/lint/slop-risk", { method: "POST", headers: apiHeaders(env), body: JSON.stringify({ changedFiles: [{ path: "", additions: -1 }] }) }, env);
expect(invalidSlopRisk.status).toBe(400);

const issueSlop = await app.request(
"/v1/lint/issue-slop",
{ method: "POST", headers: apiHeaders(env), body: JSON.stringify({ title: "Add retries", body: "" }) },
env,
);
expect(issueSlop.status).toBe(200);
await expect(issueSlop.json()).resolves.toMatchObject({ slopRisk: expect.any(Number), band: expect.stringMatching(/clean|low|elevated|high/), rubric: expect.any(String) });

const invalidIssueSlop = await app.request("/v1/lint/issue-slop", { method: "POST", headers: apiHeaders(env), body: JSON.stringify({ title: 123 }) }, env);
expect(invalidIssueSlop.status).toBe(400);

// Malformed (unparseable) JSON bodies fall through the catch to a 400, not a 500.
const malformedSlopRisk = await app.request("/v1/lint/slop-risk", { method: "POST", headers: apiHeaders(env), body: "{not json" }, env);
expect(malformedSlopRisk.status).toBe(400);
const malformedIssueSlop = await app.request("/v1/lint/issue-slop", { method: "POST", headers: apiHeaders(env), body: "{not json" }, env);
expect(malformedIssueSlop.status).toBe(400);

const queueIntelligence = await app.request(
"/v1/internal/queue-intelligence",
{
Expand Down Expand Up @@ -3398,7 +3438,7 @@ describe("api routes", () => {
expect(mcpCompatibilityBody).toMatchObject({
adoption: expect.objectContaining({
minimumSupportedVersion: "0.5.0",
latestRecommendedVersion: "0.5.0",
latestRecommendedVersion: "0.6.0",
staleEvents: 1,
incompatibleEvents: 3,
totalEvents: 4,
Expand Down Expand Up @@ -5107,7 +5147,7 @@ describe("api routes", () => {
protocolVersion: "2025-03-26",
compatibilityStatus: "incompatible",
minimumSupportedVersion: "0.5.0",
latestRecommendedVersion: "0.5.0",
latestRecommendedVersion: "0.6.0",
}),
}),
]),
Expand Down
Loading