Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d6c0049
feat(mcp): add gittensory_remediation_plan tool
kiannidev Jun 13, 2026
7e8bcfd
test(mcp): raise remediation plan branch coverage
kiannidev Jun 13, 2026
0c915b2
fix(mcp): drop sanitized-only remediation steps
kiannidev Jun 13, 2026
91bb063
test(mcp): cover remediation plan fallback branches
kiannidev Jun 13, 2026
db9259e
test(mcp): cover remediation plan API route
kiannidev Jun 13, 2026
5b44d9e
Merge branch 'main' into feat/issue-548-remediation-plan
kiannidev Jun 13, 2026
48d6310
merge upstream main into feat/issue-548-remediation-plan
kiannidev Jun 15, 2026
f4a2c1d
fix(deps): override ws and refresh lockfile for npm audit
kiannidev Jun 15, 2026
03633ff
merge origin feat/issue-548-remediation-plan
kiannidev Jun 15, 2026
e8bbccf
merge upstream main into feat/issue-548-remediation-plan
kiannidev Jun 16, 2026
47b1b77
test: raise coverage after merging score-breakdown
kiannidev Jun 16, 2026
2373abb
fix(deps): bump hono lockfile for npm audit
kiannidev Jun 16, 2026
b12cb50
Merge branch 'main' into feat/issue-548-remediation-plan
JSONbored Jun 16, 2026
53e3c9b
test: raise remediation-plan patch coverage for codecov
kiannidev Jun 16, 2026
80c3f87
test: fix remediation-plan route and redaction expectations
kiannidev Jun 16, 2026
599359e
test: cover repo manifest fallback and generic rerun branches
kiannidev Jun 16, 2026
267c7bb
Merge branch 'main' into feat/issue-548-remediation-plan
JSONbored Jun 16, 2026
0319b99
Merge branch 'main' into feat/issue-548-remediation-plan
JSONbored Jun 17, 2026
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
14 changes: 14 additions & 0 deletions packages/gittensory-mcp/bin/gittensory-mcp.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env node

Check warning on line 1 in packages/gittensory-mcp/bin/gittensory-mcp.js

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #548.

Check notice on line 1 in packages/gittensory-mcp/bin/gittensory-mcp.js

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #548.

Check notice on line 1 in packages/gittensory-mcp/bin/gittensory-mcp.js

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Titles/paths share 6 meaningful terms.

Check notice on line 1 in packages/gittensory-mcp/bin/gittensory-mcp.js

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 packages/gittensory-mcp/bin/gittensory-mcp.js

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 { createHash } from "node:crypto";
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
Expand Down Expand Up @@ -600,6 +600,20 @@
},
);

server.registerTool(
"gittensory_remediation_plan",
{
description: "Analyze the current git branch and return an ordered public-safe remediation checklist with rerun conditions.",
inputSchema: currentBranchShape,
},
async (input) => {
const workspaceInput = await withClientWorkspaceRoots(input);
const payload = buildBranchAnalysisPayload({ ...workspaceInput, cwd: resolveWorkspaceCwd(workspaceInput).cwd });
const { localScorerStatus: _localScorerStatus, ...body } = payload;
return toolResult("Gittensory remediation plan.", await apiPost("/v1/local/remediation-plan", body));
},
);

server.registerTool(
"gittensory_prepare_pr_packet",
{
Expand Down
54 changes: 54 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Hono, type Context } from "hono";

Check warning on line 1 in src/api/routes.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #548.

Check notice on line 1 in src/api/routes.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #548.

Check notice on line 1 in src/api/routes.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Titles/paths share 6 meaningful terms.

Check notice on line 1 in src/api/routes.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/api/routes.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 { z } from "zod";
import { analyzePRQueue, type AuthorRole, type ChecksStatus } from "../queue-intelligence";
import { completeGitHubWebOAuth, createSessionFromGitHubToken, pollGitHubDeviceFlow, startGitHubDeviceFlow, startGitHubWebOAuth } from "../auth/github-oauth";
Expand Down Expand Up @@ -131,6 +131,7 @@
preflightBranchWithAgent,
startAgentRun,
} from "../services/agent-orchestrator";
import { buildRemediationPlan } from "../services/remediation-plan";
import { explainScoreBreakdown } from "../services/score-breakdown";
import { buildMcpClientTelemetry } from "../services/client-telemetry";
import {
Expand Down Expand Up @@ -2265,6 +2266,59 @@
return c.json(response);
});

app.post("/v1/local/remediation-plan", async (c) => {
const body = await c.req.json().catch(() => null);
const parsed = localBranchAnalysisSchema.safeParse(body);
if (!parsed.success) return c.json({ error: "invalid_local_branch_analysis_request", issues: parsed.error.issues }, 400);
const unauthorized = await requireContributorAccess(c, parsed.data.login);
if (unauthorized) return unauthorized;
const [context, repo, issues, pullRequests, recentMergedPullRequests, bounties, snapshot, issueQuality, repoManifest] = await Promise.all([
loadContributorFastContext(c.env, parsed.data.login),
getRepository(c.env, parsed.data.repoFullName),
listIssues(c.env, parsed.data.repoFullName),
listPullRequests(c.env, parsed.data.repoFullName),
listRecentMergedPullRequests(c.env, parsed.data.repoFullName),
listBountiesByRepo(c.env, parsed.data.repoFullName),
getOrCreateScoringModelSnapshot(c.env),
loadOrComputeIssueQualityResponse(c.env, parsed.data.repoFullName),
loadRepoFocusManifest(c.env, parsed.data.repoFullName),
]);
const fit = buildContributorFit(context.profile, context.repositories, [], [], context.syncStates, context.repoStats);
const scoringProfile = buildContributorScoringProfile({ login: parsed.data.login, fit, scoringSnapshot: snapshot });
const checkSummaries = await loadCheckSummariesForPullRequests(c.env, parsed.data.repoFullName, parsed.data, pullRequests);
const analysisInput = parsed.data.focusManifest !== undefined || !repoManifest.present
? parsed.data
: { ...parsed.data, focusManifest: repoManifest as unknown };
const analysis = buildLocalBranchAnalysis({
input: analysisInput,
repo,
issues,
pullRequests,
contributorPullRequests: context.contributorPullRequests,
recentMergedPullRequests,
bounties,
repositories: context.repositories,
checkSummaries,
profile: context.profile,
outcomeHistory: context.outcomeHistory,
scoringSnapshot: snapshot,
scoringProfile,
issueQuality: issueQuality?.report,
gittensorSnapshot: context.gittensorSnapshot,
});
return c.json(
buildRemediationPlan({
login: analysis.login,
repoFullName: analysis.repoFullName,
branchQualityBlockers: analysis.branchQualityBlockers,
accountStateBlockers: analysis.accountStateBlockers,
scoreBlockers: analysis.scoreBlockers,
recommendedRerunCondition: analysis.recommendedRerunCondition,
localFindings: analysis.localFindings,
}),
);
});

app.post("/v1/agent/runs", async (c) => {
const body = await c.req.json().catch(() => null);
const parsed = agentRunSchema.safeParse(body);
Expand Down
37 changes: 37 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 warning on line 1 in src/mcp/server.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #548.

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

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #548.

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 6 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 @@ -52,6 +52,7 @@
} from "../services/agent-orchestrator";
import { loadContributorDecisionPackForServing, repoDecisionFromPack } from "../services/decision-pack";
import { buildPublicPrBodyDraft } from "../services/pr-body-draft";
import { buildRemediationPlan } from "../services/remediation-plan";
import { explainScoreBreakdown } from "../services/score-breakdown";
import { loadOrComputeIssueQualityResponse } from "../services/issue-quality";
import { loadOrComputeBurdenForecastResponse } from "../services/burden-forecast";
Expand Down Expand Up @@ -533,6 +534,14 @@
report: z.unknown().optional(),
};

const remediationPlanOutputSchema = {
repoFullName: z.string().optional(),
login: z.string().optional(),
summary: z.string().optional(),
recommendedRerunCondition: z.string().optional(),
items: z.unknown().optional(),
};

const scoreBreakdownOutputSchema = {
repoFullName: z.string().optional(),
scoreabilityStatus: z.string().optional(),
Expand Down Expand Up @@ -1087,6 +1096,17 @@
async (input) => this.toolResult(await this.localBranchSlice(input, "scoreBlockers")),
);

server.registerTool(
"gittensory_remediation_plan",
{
description:
"Turn local branch blocker lists into an ordered, deduplicated public-safe remediation checklist with rerun conditions. Metadata only.",
inputSchema: localBranchAnalysisShape,
outputSchema: remediationPlanOutputSchema,
},
async (input) => this.toolResult(await this.remediationPlan(input)),
);

server.registerTool(
"gittensory_prepare_pr_packet",
{
Expand Down Expand Up @@ -1906,6 +1926,23 @@
};
}

private async remediationPlan(input: z.infer<z.ZodObject<typeof localBranchAnalysisShape>>): Promise<ToolPayload> {
const analysis = await this.analyzeLocalBranch(input);
const plan = buildRemediationPlan({
login: analysis.login,
repoFullName: analysis.repoFullName,
branchQualityBlockers: analysis.branchQualityBlockers,
accountStateBlockers: analysis.accountStateBlockers,
scoreBlockers: analysis.scoreBlockers,
recommendedRerunCondition: analysis.recommendedRerunCondition,
localFindings: analysis.localFindings,
});
return {
summary: `Gittensory remediation plan for ${analysis.login} in ${analysis.repoFullName}.`,
data: plan as unknown as Record<string, unknown>,
};
}

private async draftPrBody(input: z.infer<z.ZodObject<typeof localBranchAnalysisShape>>): Promise<ToolPayload> {
const analysis = await this.analyzeLocalBranch(input);
const draft = buildPublicPrBodyDraft(analysis);
Expand Down
163 changes: 163 additions & 0 deletions src/services/remediation-plan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { sanitizePublicComment } from "../github/commands";

Check warning on line 1 in src/services/remediation-plan.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #548.

Check notice on line 1 in src/services/remediation-plan.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #548.

Check notice on line 1 in src/services/remediation-plan.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Titles/paths share 6 meaningful terms.

Check notice on line 1 in src/services/remediation-plan.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/services/remediation-plan.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.

export type RemediationPlanSource = "account_state" | "branch_quality" | "score";

export type RemediationPlanItem = {
rank: number;
source: RemediationPlanSource;
step: string;
rerunCondition: string;
impact: "high" | "medium";
};

export type RemediationPlan = {
repoFullName: string;
login: string;
summary: string;
recommendedRerunCondition: string;
items: RemediationPlanItem[];
};

export type RemediationPlanInput = {
login: string;
repoFullName: string;
branchQualityBlockers: string[];
accountStateBlockers: string[];
scoreBlockers: string[];
recommendedRerunCondition: string;
localFindings?: Array<{
code: string;
severity: "info" | "warning" | "critical";
title: string;
detail: string;
action?: string | undefined;
}>;
};

const FORBIDDEN_PATTERN =
/\b(reward\w*|wallet|hotkey|coldkey|mnemonic|farming|payout|ranking|raw[-_\s]?trust|trust[-_\s]?score|private[-_\s]?reviewability|reviewability)\b|\/Users\/|\/home\/|\/tmp\/|[A-Z]:[\\/]Users[\\/]/i;

const SOURCE_PRIORITY: Record<RemediationPlanSource, number> = {
account_state: 0,
branch_quality: 1,
score: 2,
};

function publicSafeText(value: string): string {
const sanitized = sanitizePublicComment(value).trim();
if (!sanitized || FORBIDDEN_PATTERN.test(sanitized) || /^(?:private context\s*)+$/i.test(sanitized)) return "";
return sanitized;
}

function publicSafeRerunCondition(condition: string): string {
const sanitized = publicSafeText(condition);
if (!sanitized) return "Rerun after branch, base, or PR state changes before opening or submitting.";
return /eligibility|multiplier|scoreability|score/i.test(sanitized)
? "Refresh linked issue and base branch metadata before submission."
: sanitized;
}

function normalizeKey(value: string): string {
return value.trim().toLowerCase().replace(/\s+/g, " ");
}

function actionForFinding(findings: RemediationPlanInput["localFindings"], title: string): string | undefined {
const match = findings?.find((finding) => normalizeKey(finding.title) === normalizeKey(title));
return match?.action ? publicSafeText(match.action) : undefined;
}

function rerunForAccountBlocker(blocker: string, fallback: string): string {
if (/open PR|concurrent|threshold/i.test(blocker)) {
return publicSafeRerunCondition("Rerun after pending PRs merge/close or open PR count is within the allowance.");
}
if (/credibility|history|maturity/i.test(blocker)) {
return publicSafeRerunCondition("Rerun after account/queue maturity blockers clear.");
}
return publicSafeRerunCondition(fallback);
}

function rerunForBranchBlocker(blocker: string, fallback: string): string {
if (/stale|fetch origin|base/i.test(blocker)) {
return publicSafeRerunCondition("Run `git fetch origin` and rerun branch analysis against the refreshed base.");
}
if (/validation|test|check/i.test(blocker)) {
return publicSafeRerunCondition("Rerun after fixing branch-quality blockers or adding explicit validation evidence.");
}
if (/linked issue|duplicate|eligibility/i.test(blocker)) {
return publicSafeRerunCondition("Refresh linked issue and base branch metadata before submission.");
}
return publicSafeRerunCondition(fallback);
}

function stepFromBlocker(source: RemediationPlanSource, blocker: string, findings: RemediationPlanInput["localFindings"]): string {
const findingAction = actionForFinding(findings, blocker);
if (findingAction) return findingAction;
const sanitized = publicSafeText(blocker);
if (sanitized) return sanitized;
if (source === "account_state") return "Clear account or queue maturity blockers before opening more work.";
if (source === "branch_quality") return "Resolve branch-quality findings before submission.";
return "Resolve scoreability blockers before relying on this preview.";
}

function impactFor(source: RemediationPlanSource, blocker: string): "high" | "medium" {
if (source === "account_state") return "high";
if (/GitHub checks|validation failed|maintainer-blocked|duplicate|ineligible/i.test(blocker)) return "high";
return source === "branch_quality" ? "high" : "medium";
}

function collectItems(input: RemediationPlanInput): Array<Omit<RemediationPlanItem, "rank">> {
const seen = new Set<string>();
const items: Array<Omit<RemediationPlanItem, "rank">> = [];
const push = (source: RemediationPlanSource, blocker: string) => {
const key = normalizeKey(blocker);
if (!key || seen.has(key)) return;
const step = stepFromBlocker(source, blocker, input.localFindings);
if (!step) return;
seen.add(key);
const rerunCondition =
source === "account_state"
? rerunForAccountBlocker(blocker, input.recommendedRerunCondition)
: source === "branch_quality"
? rerunForBranchBlocker(blocker, input.recommendedRerunCondition)
: publicSafeRerunCondition(input.recommendedRerunCondition);
items.push({
source,
step,
rerunCondition,
impact: impactFor(source, blocker),
});
};

for (const blocker of input.accountStateBlockers) push("account_state", blocker);
for (const blocker of input.branchQualityBlockers) push("branch_quality", blocker);
for (const blocker of input.scoreBlockers) push("score", blocker);

items.sort(
(left, right) =>
SOURCE_PRIORITY[left.source] - SOURCE_PRIORITY[right.source] ||
Number(right.impact === "high") - Number(left.impact === "high") ||
left.step.localeCompare(right.step),
);
return items;
}

/**
* Turn local branch blocker lists into an ordered, deduplicated remediation checklist.
* Steps and rerun conditions are public-safe for PR-body reuse.
*/
export function buildRemediationPlan(input: RemediationPlanInput): RemediationPlan {
const ordered = collectItems(input);
const items = ordered.map((item, index) => ({ ...item, rank: index + 1 }));
const summary =
items.length === 0
? "No blockers detected; rerun after any branch, base, or PR state changes before opening or submitting."
: `${items.length} remediation step(s) ordered by impact; start with ${items[0]?.step ?? "the first listed item"}.`;

return {
repoFullName: input.repoFullName,
login: input.login,
summary: publicSafeText(summary),
recommendedRerunCondition: publicSafeRerunCondition(input.recommendedRerunCondition),
items,
};
}
Loading
Loading