diff --git a/apps/gittensory-ui/src/lib/mcp-package.ts b/apps/gittensory-ui/src/lib/mcp-package.ts index 9315c51341..e09d6e228a 100644 --- a/apps/gittensory-ui/src/lib/mcp-package.ts +++ b/apps/gittensory-ui/src/lib/mcp-package.ts @@ -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 = { diff --git a/packages/gittensory-mcp/CHANGELOG.md b/packages/gittensory-mcp/CHANGELOG.md index 3b94d6da37..c095c4d8c0 100644 --- a/packages/gittensory-mcp/CHANGELOG.md +++ b/packages/gittensory-mcp/CHANGELOG.md @@ -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 diff --git a/packages/gittensory-mcp/bin/gittensory-mcp.js b/packages/gittensory-mcp/bin/gittensory-mcp.js index 37308b3da1..d79ad689bb 100755 --- a/packages/gittensory-mcp/bin/gittensory-mcp.js +++ b/packages/gittensory-mcp/bin/gittensory-mcp.js @@ -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(), @@ -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", { diff --git a/packages/gittensory-mcp/package.json b/packages/gittensory-mcp/package.json index 5afbd18a76..7169e2872b 100644 --- a/packages/gittensory-mcp/package.json +++ b/packages/gittensory-mcp/package.json @@ -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.", diff --git a/src/api/routes.ts b/src/api/routes.ts index b2ec5e23f7..d89041e58b 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -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"; @@ -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(), @@ -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); @@ -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; @@ -4013,7 +4047,7 @@ function canSessionAccessPath(env: Env, identity: Extract { 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); @@ -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: [], @@ -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", { @@ -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, @@ -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", }), }), ]),