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 package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions packages/gittensory-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ gittensory-mcp preflight --login jsonbored --json
gittensory-mcp --stdio
```

For near-term what-if scoreability, pass the situational assumptions explicitly:

```sh
gittensory-mcp analyze-branch --login jsonbored \
--pending-merged-prs 3 \
--expected-open-prs 0 \
--projected-credibility 0.8 \
--scenario-note "approved PRs expected to merge" \
--json
```

## Auth

`login` uses GitHub Device Flow by default. For non-interactive bootstrap:
Expand Down
74 changes: 65 additions & 9 deletions packages/gittensory-mcp/bin/gittensory-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ const localScoreShape = {
openPrCount: z.number().int().min(0).optional(),
credibility: z.number().min(0).max(1).optional(),
changesRequestedCount: z.number().int().min(0).optional(),
pendingMergedPrCount: z.number().int().min(0).optional(),
pendingClosedPrCount: z.number().int().min(0).optional(),
approvedPrCount: z.number().int().min(0).optional(),
expectedOpenPrCountAfterMerge: z.number().int().min(0).optional(),
projectedCredibility: z.number().min(0).max(1).optional(),
scenarioNotes: z.array(z.string()).optional(),
scorePreviewCommand: z.string().optional(),
};

Expand All @@ -85,6 +91,12 @@ const currentBranchShape = {
body: z.string().optional(),
labels: z.array(z.string()).optional(),
linkedIssues: z.array(z.number().int().positive()).optional(),
pendingMergedPrCount: z.number().int().min(0).optional(),
pendingClosedPrCount: z.number().int().min(0).optional(),
approvedPrCount: z.number().int().min(0).optional(),
expectedOpenPrCountAfterMerge: z.number().int().min(0).optional(),
projectedCredibility: z.number().min(0).max(1).optional(),
scenarioNotes: z.array(z.string()).optional(),
validation: z
.array(
z.object({
Expand All @@ -109,7 +121,7 @@ if (cliArgs[0] && cliArgs[0] !== "--stdio") {

const server = new McpServer({
name: "gittensory-local",
version: "0.1.0",
version: "0.1.3",
});

server.registerTool(
Expand Down Expand Up @@ -208,7 +220,7 @@ server.registerTool(
async ({ variants }) => {
const previews = [];
for (const variant of variants) previews.push(await previewLocalScore({ ...variant, targetKey: variant.targetKey ?? `variant:${previews.length + 1}` }));
previews.sort((left, right) => Number(right?.remotePreview?.result?.scoreEstimate?.estimatedMergedScore ?? 0) - Number(left?.remotePreview?.result?.scoreEstimate?.estimatedMergedScore ?? 0));
previews.sort((left, right) => Number(right?.remotePreview?.result?.effectiveEstimatedScore ?? right?.remotePreview?.result?.scoreEstimate?.estimatedMergedScore ?? 0) - Number(left?.remotePreview?.result?.effectiveEstimatedScore ?? left?.remotePreview?.result?.scoreEstimate?.estimatedMergedScore ?? 0));
return toolResult("Gittensory PR variant comparison.", { variants: previews });
},
);
Expand Down Expand Up @@ -262,7 +274,13 @@ server.registerTool(
},
async (input) => {
const result = await analyzeCurrentBranch(input);
return toolResult("Gittensory current-branch private score preview.", { local: result.local, scorePreview: result.analysis.scorePreview, scoreBlockers: result.analysis.scoreBlockers });
return toolResult("Gittensory current-branch private score preview.", {
local: result.local,
scorePreview: result.analysis.scorePreview,
scenarioScorePreview: result.analysis.scenarioScorePreview,
scoreBlockers: result.analysis.scoreBlockers,
recommendedRerunCondition: result.analysis.recommendedRerunCondition,
});
},
);

Expand All @@ -274,7 +292,7 @@ server.registerTool(
},
async (input) => {
const result = await analyzeCurrentBranch(input);
return toolResult("Gittensory local next-action ranking.", { local: result.local, nextActions: result.analysis.nextActions, rewardRisk: result.analysis.rewardRisk });
return toolResult("Gittensory local next-action ranking.", { local: result.local, nextActions: result.analysis.nextActions, rewardRisk: result.analysis.rewardRisk, recommendedRerunCondition: result.analysis.recommendedRerunCondition });
},
);

Expand All @@ -286,7 +304,15 @@ server.registerTool(
},
async (input) => {
const result = await analyzeCurrentBranch(input);
return toolResult("Gittensory local blocker explanation.", { local: result.local, scoreBlockers: result.analysis.scoreBlockers, localFindings: result.analysis.localFindings });
return toolResult("Gittensory local blocker explanation.", {
local: result.local,
scoreBlockers: result.analysis.scoreBlockers,
branchQualityBlockers: result.analysis.branchQualityBlockers,
accountStateBlockers: result.analysis.accountStateBlockers,
baseFreshness: result.analysis.baseFreshness,
localFindings: result.analysis.localFindings,
recommendedRerunCondition: result.analysis.recommendedRerunCondition,
});
},
);

Expand Down Expand Up @@ -314,7 +340,7 @@ server.registerTool(
analyses.sort(
(left, right) =>
Number(right.analysis.nextActions?.[0]?.priorityScore ?? 0) - Number(left.analysis.nextActions?.[0]?.priorityScore ?? 0) ||
Number(right.analysis.scorePreview?.scoreEstimate?.estimatedMergedScore ?? 0) - Number(left.analysis.scorePreview?.scoreEstimate?.estimatedMergedScore ?? 0),
Number(right.analysis.scorePreview?.effectiveEstimatedScore ?? right.analysis.scorePreview?.scoreEstimate?.estimatedMergedScore ?? 0) - Number(left.analysis.scorePreview?.effectiveEstimatedScore ?? left.analysis.scorePreview?.scoreEstimate?.estimatedMergedScore ?? 0),
);
return toolResult("Gittensory local variant comparison.", {
variants: analyses.map((entry) => ({
Expand Down Expand Up @@ -352,6 +378,12 @@ async function runCli(args) {
body: options.body,
labels: options.label,
linkedIssues: options.issue?.map((value) => Number(value)).filter((value) => Number.isInteger(value) && value > 0),
pendingMergedPrCount: optionalInteger(options.pendingMergedPrs),
pendingClosedPrCount: optionalInteger(options.pendingClosedPrs),
approvedPrCount: optionalInteger(options.approvedPrs),
expectedOpenPrCountAfterMerge: optionalInteger(options.expectedOpenPrs),
projectedCredibility: optionalNumber(options.projectedCredibility),
scenarioNotes: options.scenarioNote,
validation: validationFromOptions(options),
scorePreviewCommand: options.scorePreviewCommand,
});
Expand Down Expand Up @@ -383,8 +415,8 @@ function printHelp() {
gittensory-mcp status [--json]
gittensory-mcp doctor [--cwd path] [--json]
gittensory-mcp init-client --print codex|claude|cursor [--json]
gittensory-mcp analyze-branch --login <github-login> [--repo owner/repo] [--base origin/main] [--validation "passed|npm test|summary"] [--json]
gittensory-mcp preflight --login <github-login> [--repo owner/repo] [--base origin/main] [--validation "passed|npm test|summary"] [--json]
gittensory-mcp analyze-branch --login <github-login> [--repo owner/repo] [--base origin/main] [--pending-merged-prs 3] [--expected-open-prs 0] [--projected-credibility 0.8] [--scenario-note "..."] [--validation "passed|npm test|summary"] [--json]
gittensory-mcp preflight --login <github-login> [--repo owner/repo] [--base origin/main] [--pending-merged-prs 3] [--expected-open-prs 0] [--projected-credibility 0.8] [--validation "passed|npm test|summary"] [--json]

Environment:
GITTENSORY_API_URL
Expand All @@ -399,7 +431,7 @@ Environment:

function parseOptions(args) {
const options = {};
const repeatable = new Set(["label", "issue", "validation", "validationCommand", "validationStatus", "validationSummary"]);
const repeatable = new Set(["label", "issue", "validation", "validationCommand", "validationStatus", "validationSummary", "scenarioNote"]);
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--json") {
Expand Down Expand Up @@ -609,6 +641,18 @@ function validationFromOptions(options) {
return [...direct, ...expanded].filter((entry) => typeof entry.command === "string" && entry.command.length > 0);
}

function optionalInteger(value) {
if (value === undefined || value === true) return undefined;
const parsed = Number(value);
return Number.isInteger(parsed) && parsed >= 0 ? parsed : undefined;
}

function optionalNumber(value) {
if (value === undefined || value === true) return undefined;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : undefined;
}

function isValidationStatus(value) {
return value === "passed" || value === "failed" || value === "not_run";
}
Expand Down Expand Up @@ -701,7 +745,13 @@ async function analyzeCurrentBranch(input) {
baseRef: body.baseRef,
headRef: body.headRef,
branchName: body.branchName,
baseSha: body.baseSha,
headSha: body.headSha,
mergeBaseSha: body.mergeBaseSha,
remoteTrackingSha: body.remoteTrackingSha,
changedFileCount: body.changedFiles?.length ?? 0,
testFileCount: body.changedFiles?.filter((file) => /(^|\/)(test|tests|spec|__tests__)\/|(^|\/)src\/test\/|(^|\/)[^/]+_test\.(go|py|rb)$|(^|\/)[^/]+_spec\.rb$|\.(test|spec)\.(ts|tsx|js|jsx|py|rb|rs)$/i.test(file.path)).length ?? 0,
passedValidationCount: body.validation?.filter((entry) => entry.status === "passed").length ?? 0,
localScorerStatus,
setupGuidance: setupGuidanceForLocalScorer(localScorerStatus),
},
Expand Down Expand Up @@ -729,6 +779,12 @@ async function previewLocalScore(input) {
openPrCount: input.openPrCount,
credibility: input.credibility,
changesRequestedCount: input.changesRequestedCount,
pendingMergedPrCount: input.pendingMergedPrCount,
pendingClosedPrCount: input.pendingClosedPrCount,
approvedPrCount: input.approvedPrCount,
expectedOpenPrCountAfterMerge: input.expectedOpenPrCountAfterMerge,
projectedCredibility: input.projectedCredibility,
scenarioNotes: input.scenarioNotes,
metadataOnly: !upstreamPreview.ok,
};
return {
Expand Down
32 changes: 30 additions & 2 deletions packages/gittensory-mcp/lib/local-branch.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export function collectLocalBranchMetadata(input) {
if (!repoFullName) throw new Error("Could not infer repoFullName from git remote; pass --repo owner/repo.");
const branchName = input.branchName ?? gitLines(cwd, ["branch", "--show-current"])[0] ?? "local-branch";
const headRef = input.headRef ?? gitLines(cwd, ["rev-parse", "--abbrev-ref", "HEAD"])[0] ?? branchName;
const baseSha = gitLines(cwd, ["rev-parse", "--verify", baseRef])[0];
const headSha = gitLines(cwd, ["rev-parse", "--verify", "HEAD"])[0];
const mergeBaseSha = gitLines(cwd, ["merge-base", baseRef, "HEAD"])[0];
const remoteTrackingSha = collectRemoteTrackingSha(cwd, baseRef);
const changedFiles = collectChangedFiles(cwd, baseRef);
const commitMessages = input.commitMessages ?? collectCommitMessages(cwd, baseRef);
const title = input.title ?? titleFromBranch(branchName) ?? firstCommitTitle(commitMessages);
Expand All @@ -47,13 +51,23 @@ export function collectLocalBranchMetadata(input) {
baseRef,
headRef,
branchName,
baseSha,
headSha,
mergeBaseSha,
remoteTrackingSha,
commitMessages,
changedFiles,
validation: input.validation,
linkedIssues,
labels: input.labels,
title,
body: input.body,
pendingMergedPrCount: input.pendingMergedPrCount,
pendingClosedPrCount: input.pendingClosedPrCount,
approvedPrCount: input.approvedPrCount,
expectedOpenPrCountAfterMerge: input.expectedOpenPrCountAfterMerge,
projectedCredibility: input.projectedCredibility,
scenarioNotes: input.scenarioNotes,
};
return stripUndefined(payload);
}
Expand Down Expand Up @@ -101,7 +115,7 @@ export function setupGuidanceForLocalScorer(status) {

export function gitLines(cwd, args) {
try {
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 })
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
Expand Down Expand Up @@ -169,6 +183,14 @@ function defaultBaseRef(cwd) {
return "HEAD";
}

function collectRemoteTrackingSha(cwd, baseRef) {
const match = String(baseRef ?? "").replace(/^refs\/remotes\//, "").match(/^origin\/(.+)$/);
const branch = match?.[1];
if (!branch) return undefined;
const remoteRow = gitLines(cwd, ["ls-remote", "--heads", "origin", branch])[0];
return remoteRow?.split(/\s+/)[0];
}

function normalizeScorerOutput(payload) {
return stripUndefined({
mode: "external_command",
Expand Down Expand Up @@ -226,7 +248,13 @@ function firstCommitTitle(messages) {
}

function isTestFile(file) {
return /(^|\/)(test|tests|spec|__tests__)\//i.test(file) || /\.(test|spec)\.(ts|tsx|js|jsx|py|rb|rs)$/i.test(file);
return (
/(^|\/)(test|tests|spec|__tests__)\//i.test(file) ||
/(^|\/)src\/test\//i.test(file) ||
/(^|\/)[^/]+_test\.(go|py|rb)$/i.test(file) ||
/(^|\/)[^/]+_spec\.rb$/i.test(file) ||
/\.(test|spec)\.(ts|tsx|js|jsx|py|rb|rs)$/i.test(file)
);
}

function isCodeFile(file) {
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.1.2",
"version": "0.1.3",
"license": "AGPL-3.0-only",
"type": "module",
"description": "Local stdio MCP wrapper for Gittensory contributor intelligence.",
Expand Down
15 changes: 15 additions & 0 deletions site/guide/miners.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,26 @@ The response includes:
- role context
- preflight findings
- private score blockers
- current vs projected scoreability scenarios
- reward/risk reasoning
- base freshness warnings when the local diff may be inflated
- maintainer-fit notes
- public-safe PR packet
- ranked next actions

When the current score is blocked by temporary account/queue state, pass the assumptions explicitly:

```sh
gittensory-mcp analyze-branch --login YOUR_GITHUB_LOGIN \
--pending-merged-prs 3 \
--expected-open-prs 0 \
--projected-credibility 0.8 \
--scenario-note "approved PRs expected to merge" \
--json
```

Gittensory labels that as a user-supplied scenario. It shows the current effective score, the underlying potential score, and what changes if the open-PR and credibility gates clear.

## Preflight

```sh
Expand Down
16 changes: 16 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ const localBranchAnalysisSchema = z
baseRef: z.string().min(1).optional(),
headRef: z.string().min(1).optional(),
branchName: z.string().min(1).optional(),
baseSha: z.string().min(1).optional(),
headSha: z.string().min(1).optional(),
mergeBaseSha: z.string().min(1).optional(),
remoteTrackingSha: z.string().min(1).optional(),
commitMessages: z.array(z.string()).max(30).optional(),
changedFiles: z.array(localBranchChangedFileSchema).max(500).optional(),
validation: z.array(localBranchValidationSchema).max(50).optional(),
Expand All @@ -163,6 +167,12 @@ const localBranchAnalysisSchema = z
title: z.string().min(1).optional(),
body: z.string().optional(),
localScorer: localBranchScorerSchema.optional(),
pendingMergedPrCount: z.number().int().min(0).optional(),
pendingClosedPrCount: z.number().int().min(0).optional(),
approvedPrCount: z.number().int().min(0).optional(),
expectedOpenPrCountAfterMerge: z.number().int().min(0).optional(),
projectedCredibility: z.number().min(0).max(1).optional(),
scenarioNotes: z.array(z.string()).max(20).optional(),
})
.strict();

Expand All @@ -184,6 +194,12 @@ const scorePreviewSchema = z.object({
changesRequestedCount: z.number().int().min(0).optional(),
fixedBaseScore: z.number().min(0).optional(),
metadataOnly: z.boolean().default(false),
pendingMergedPrCount: z.number().int().min(0).optional(),
pendingClosedPrCount: z.number().int().min(0).optional(),
approvedPrCount: z.number().int().min(0).optional(),
expectedOpenPrCountAfterMerge: z.number().int().min(0).optional(),
projectedCredibility: z.number().min(0).max(1).optional(),
scenarioNotes: z.array(z.string()).max(20).optional(),
});

const repositorySettingsSchema = z.object({
Expand Down
Loading