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
213 changes: 213 additions & 0 deletions apps/gittensory-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1910,6 +1910,192 @@
"missing"
]
},
"ContributorOpenPrNextStepPacket": {
"type": "object",
"properties": {
"repoFullName": {
"type": "string"
},
"number": {
"type": "number"
},
"title": {
"type": "string"
},
"classification": {
"type": "string",
"enum": [
"approved",
"blocked",
"stale",
"needs_author",
"failing_checks",
"missing_tests",
"duplicate_prone",
"reviewable",
"should_close_or_withdraw",
"maintainer_lane",
"draft"
]
},
"summary": {
"type": "string"
},
"reasons": {
"type": "array",
"items": {
"type": "string"
}
},
"nextSteps": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"repoFullName",
"number",
"title",
"classification",
"summary",
"reasons",
"nextSteps"
]
},
"ContributorOpenPrMonitor": {
"type": "object",
"properties": {
"login": {
"type": "string"
},
"generatedAt": {
"type": "string"
},
"openPrCount": {
"type": "number"
},
"registeredRepoCount": {
"type": "number"
},
"cleanupFirst": {
"type": "boolean"
},
"summary": {
"type": "string"
},
"guidance": {
"type": "array",
"items": {
"type": "string"
}
},
"pendingScenarios": {
"type": "array",
"items": {
"type": "object",
"properties": {
"repoFullName": {
"type": "string"
},
"detection": {
"type": "object",
"properties": {
"source": {
"type": "string",
"enum": [
"github_observed",
"user_supplied"
]
},
"pendingMergedPrCount": {
"type": "number"
},
"pendingClosedPrCount": {
"type": "number"
},
"approvedPrCount": {
"type": "number"
},
"expectedOpenPrCountAfterMerge": {
"type": "number"
},
"scenarioNotes": {
"type": "array",
"items": {
"type": "string"
}
},
"classified": {
"type": "array",
"items": {
"type": "object",
"properties": {
"repoFullName": {
"type": "string"
},
"number": {
"type": "number"
},
"title": {
"type": "string"
},
"classification": {
"type": "string"
},
"reasons": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"repoFullName",
"number",
"title",
"classification",
"reasons"
]
}
}
},
"required": [
"source",
"pendingMergedPrCount",
"pendingClosedPrCount",
"approvedPrCount",
"scenarioNotes",
"classified"
]
}
},
"required": [
"repoFullName",
"detection"
]
}
},
"pullRequests": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ContributorOpenPrNextStepPacket"
}
}
},
"required": [
"login",
"generatedAt",
"openPrCount",
"registeredRepoCount",
"cleanupFirst",
"summary",
"guidance",
"pendingScenarios",
"pullRequests"
]
},
"ContributorDecisionPack": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -2045,6 +2231,9 @@
"items": {
"type": "string"
}
},
"openPrMonitor": {
"$ref": "#/components/schemas/ContributorOpenPrMonitor"
}
},
"required": [
Expand Down Expand Up @@ -9003,6 +9192,30 @@
]
}
},
"/v1/contributors/{login}/open-pr-monitor": {
"get": {
"responses": {
"200": {
"description": "Contributor open-PR monitor with classifications and public-safe next-step packets from cached metadata.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ContributorOpenPrMonitor"
}
}
}
}
},
"security": [
{
"GittensoryBearer": []
},
{
"GittensorySessionCookie": []
}
]
}
},
"/v1/contributors/{login}/repos/{owner}/{repo}/decision": {
"get": {
"responses": {
Expand Down
8 changes: 8 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ import {
buildRegistryChangeReport,
} from "../signals/engine";
import { attachDataQuality, buildCoreSignalFidelity, buildFreshnessSloReport, buildRepoDataQuality, buildSignalFidelity } from "../signals/data-quality";
import { buildContributorOpenPrMonitor } from "../signals/contributor-open-pr-monitor";
import { buildPullRequestReviewability } from "../signals/reward-risk";
import { buildLocalBranchAnalysis, findCurrentBranchPullRequest } from "../signals/local-branch";
import { buildRepoSettingsPreview } from "../signals/settings-preview";
Expand Down Expand Up @@ -1150,6 +1151,13 @@ export function createApp() {
return c.json(serving.refresh, 202);
});

app.get("/v1/contributors/:login/open-pr-monitor", async (c) => {
const login = c.req.param("login");
const unauthorized = await requireContributorAccess(c, login);
if (unauthorized) return unauthorized;
return c.json(await buildContributorOpenPrMonitor(c.env, login));
});

app.get("/v1/contributors/:login/repos/:owner/:repo/decision", async (c) => {
const login = c.req.param("login");
const unauthorized = await requireContributorAccess(c, login);
Expand Down
1 change: 1 addition & 0 deletions src/auth/rate-limit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export function routeClassForPath(path: string): RateLimitClass {
path.includes("/v1/agent/") ||
path.includes("/scoring/preview") ||
path.includes("/decision-pack") ||
path.includes("/open-pr-monitor") ||
path.includes("/upstream/") ||
path.includes("/internal/jobs/generate-signal-snapshots") ||
path.includes("/internal/jobs/build-contributor-decision-packs") ||
Expand Down
20 changes: 20 additions & 0 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
buildRegistryChangeReport,
buildRoleContext,
} from "../signals/engine";
import { buildContributorOpenPrMonitor } from "../signals/contributor-open-pr-monitor";
import { buildLocalBranchAnalysis, findCurrentBranchPullRequest } from "../signals/local-branch";
import { buildRepoDataQuality } from "../signals/data-quality";
import { loadUpstreamStatus } from "../upstream/ruleset";
Expand Down Expand Up @@ -282,6 +283,16 @@ export class GittensoryMcp {
async (input) => this.toolResult(await this.getDecisionPack(input.login)),
);

server.registerTool(
"gittensory_monitor_open_prs",
{
description:
"Inspect a contributor's open PRs on registered repos, classify queue state, and return public-safe next-step packets from cached metadata.",
inputSchema: loginShape,
},
async (input) => this.toolResult(await this.monitorOpenPullRequests(input.login)),
);

server.registerTool(
"gittensory_explain_repo_decision",
{
Expand Down Expand Up @@ -610,6 +621,15 @@ export class GittensoryMcp {
};
}

private async monitorOpenPullRequests(login: string): Promise<ToolPayload> {
this.requireContributorAccess(login);
const monitor = await buildContributorOpenPrMonitor(this.env, login);
return {
summary: monitor.summary,
data: monitor as unknown as Record<string, unknown>,
};
}

private async explainRepoDecision(input: { login: string; owner: string; repo: string }): Promise<ToolPayload> {
this.requireContributorAccess(input.login);
const fullName = `${input.owner}/${input.repo}`;
Expand Down
60 changes: 60 additions & 0 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,65 @@ export const ContributorProfileSchema = z
})
.openapi("ContributorProfile");

export const ContributorOpenPrNextStepPacketSchema = z
.object({
repoFullName: z.string(),
number: z.number(),
title: z.string(),
classification: z.enum([
"approved",
"blocked",
"stale",
"needs_author",
"failing_checks",
"missing_tests",
"duplicate_prone",
"reviewable",
"should_close_or_withdraw",
"maintainer_lane",
"draft",
]),
summary: z.string(),
reasons: z.array(z.string()),
nextSteps: z.array(z.string()),
})
.openapi("ContributorOpenPrNextStepPacket");

export const ContributorOpenPrMonitorSchema = z
.object({
login: z.string(),
generatedAt: z.string(),
openPrCount: z.number(),
registeredRepoCount: z.number(),
cleanupFirst: z.boolean(),
summary: z.string(),
guidance: z.array(z.string()),
pendingScenarios: z.array(
z.object({
repoFullName: z.string(),
detection: z.object({
source: z.enum(["github_observed", "user_supplied"]),
pendingMergedPrCount: z.number(),
pendingClosedPrCount: z.number(),
approvedPrCount: z.number(),
expectedOpenPrCountAfterMerge: z.number().optional(),
scenarioNotes: z.array(z.string()),
classified: z.array(
z.object({
repoFullName: z.string(),
number: z.number(),
title: z.string(),
classification: z.string(),
reasons: z.array(z.string()),
}),
),
}),
}),
),
pullRequests: z.array(ContributorOpenPrNextStepPacketSchema),
})
.openapi("ContributorOpenPrMonitor");

export const ContributorOpportunitySchema = z
.object({
repoFullName: z.string(),
Expand Down Expand Up @@ -1208,6 +1267,7 @@ export const ContributorDecisionPackSchema = z
dataQuality: z.record(z.unknown()),
summary: z.string(),
nextActions: z.array(z.string()),
openPrMonitor: ContributorOpenPrMonitorSchema.optional(),
})
.openapi("ContributorDecisionPack");

Expand Down
Loading