@@ -89,6 +89,7 @@ const CLI_COMMAND_SPEC = {
8989 "repo-decision" : [ ] ,
9090 "contributor-profile" : [ ] ,
9191 "monitor-open-prs" : [ ] ,
92+ "pr-outcomes" : [ ] ,
9293 "analyze-branch" : [ ] ,
9394 preflight : [ ] ,
9495 "review-pr" : [ ] ,
@@ -1130,6 +1131,12 @@ const STDIO_TOOL_DESCRIPTORS = [
11301131 description :
11311132 "Inspect a contributor's open PRs on registered repos, classify queue state, and return public-safe next-step packets from cached metadata." ,
11321133 } ,
1134+ {
1135+ name : "loopover_pr_outcome" ,
1136+ category : "review" ,
1137+ description :
1138+ "Return a contributor's own post-merge outcome records — for each merged PR, a public-safe attribution of what it did for their standing on the repo. Self-scoped: only the authenticated login's outcomes." ,
1139+ } ,
11331140 {
11341141 name : "loopover_compare_pr_variants" ,
11351142 category : "branch" ,
@@ -2070,6 +2077,21 @@ registerStdioTool(
20702077 } ,
20712078) ;
20722079
2080+ registerStdioTool (
2081+ "loopover_pr_outcome" ,
2082+ {
2083+ description : stdioToolDescription ( "loopover_pr_outcome" ) ,
2084+ inputSchema : {
2085+ login : z . string ( ) . min ( 1 ) ,
2086+ limit : z . number ( ) . int ( ) . positive ( ) . max ( 100 ) . optional ( ) ,
2087+ } ,
2088+ } ,
2089+ async ( { login, limit } ) => {
2090+ const payload = await getPrOutcomes ( login , limit ) ;
2091+ return toolResult ( prOutcomesToolSummary ( login , payload ) , payload ) ;
2092+ } ,
2093+ ) ;
2094+
20732095registerStdioTool (
20742096 "loopover_compare_pr_variants" ,
20752097 {
@@ -3376,6 +3398,7 @@ async function runCli(args) {
33763398 if ( command === "repo-decision" ) return repoDecisionCli ( options ) ;
33773399 if ( command === "contributor-profile" ) return contributorProfileCli ( options ) ;
33783400 if ( command === "monitor-open-prs" ) return monitorOpenPrsCli ( options ) ;
3401+ if ( command === "pr-outcomes" ) return prOutcomesCli ( options ) ;
33793402 if ( command === "review-pr" ) return reviewPrCli ( options ) ;
33803403 if ( command !== "analyze-branch" && command !== "preflight" ) {
33813404 const suggestion = suggestCommand ( command ) ;
@@ -3838,6 +3861,45 @@ async function monitorOpenPrsCli(options) {
38383861 }
38393862}
38403863
3864+ function printPrOutcomesHelp ( ) {
3865+ process . stdout . write (
3866+ [
3867+ "Usage: loopover-mcp pr-outcomes --login <github-login> [--limit N] [--json]" ,
3868+ "" ,
3869+ "List your post-merge PR outcome history (public-safe attribution per merged PR)." ,
3870+ "Mirrors the loopover_pr_outcome MCP tool and GET /v1/contributors/{login}/pr-outcomes. No source upload." ,
3871+ "" ,
3872+ "Pass --json for machine-readable output." ,
3873+ ] . join ( "\n" ) + "\n" ,
3874+ ) ;
3875+ }
3876+
3877+ async function prOutcomesCli ( options ) {
3878+ if ( options . help === true ) return printPrOutcomesHelp ( ) ;
3879+ const login = options . login ?? process . env . LOOPOVER_LOGIN ?? process . env . GITHUB_LOGIN ;
3880+ if ( ! login ) throw new Error ( "Pass --login <github-login> or set LOOPOVER_LOGIN." ) ;
3881+ const limitRaw = options . limit ;
3882+ let limit ;
3883+ if ( limitRaw !== undefined && limitRaw !== true ) {
3884+ const parsed = Number ( limitRaw ) ;
3885+ if ( ! Number . isInteger ( parsed ) || parsed < 1 || parsed > 100 ) {
3886+ throw new Error ( "Pass --limit as an integer between 1 and 100." ) ;
3887+ }
3888+ limit = parsed ;
3889+ }
3890+ const payload = await getPrOutcomes ( login , limit ) ;
3891+ if ( options . json ) {
3892+ process . stdout . write ( `${ JSON . stringify ( payload , null , 2 ) } \n` ) ;
3893+ return ;
3894+ }
3895+ process . stdout . write ( `${ sanitizePlainTextTerminalOutput ( prOutcomesToolSummary ( login , payload ) ) } \n` ) ;
3896+ for ( const outcome of payload ?. outcomes ?? [ ] ) {
3897+ const heading = `${ outcome . repoFullName } #${ outcome . pullNumber ?? "?" } [${ outcome . outcome } ]` ;
3898+ process . stdout . write ( `${ sanitizePlainTextTerminalOutput ( heading ) } \n` ) ;
3899+ if ( outcome . attribution ) process . stdout . write ( ` ${ sanitizePlainTextTerminalOutput ( outcome . attribution ) } \n` ) ;
3900+ }
3901+ }
3902+
38413903function printRepoDecisionHelp ( ) {
38423904 process . stdout . write (
38433905 [
@@ -4317,6 +4379,7 @@ function printHelp() {
43174379 loopover-mcp decision-pack --login <github-login> [--json]
43184380 loopover-mcp repo-decision --login <github-login> --repo owner/repo [--json]
43194381 loopover-mcp monitor-open-prs --login <github-login> [--json]
4382+ loopover-mcp pr-outcomes --login <github-login> [--limit N] [--json]
43204383 loopover-mcp analyze-branch --login <github-login> [--repo owner/repo] [--base origin/main] [--branch-eligibility eligible|ineligible|unknown] [--pending-merged-prs 3] [--expected-open-prs 0] [--projected-credibility 0.8] [--scenario-note "..."] [--validation "passed|npm test|summary"] [--format table] [--json]
43214384 loopover-mcp preflight --login <github-login> [--repo owner/repo] [--base origin/main] [--branch-eligibility eligible|ineligible|unknown] [--pending-merged-prs 3] [--expected-open-prs 0] [--projected-credibility 0.8] [--validation "passed|npm test|summary"] [--format table] [--json]
43224385 loopover-mcp review-pr --login <github-login> [--repo owner/repo] [--base origin/main] [--commit <message>]... [--body <text>] [--body-file <path>] [--linked-issue <number>] [--json]
@@ -4335,7 +4398,7 @@ function printHelp() {
43354398 LOOPOVER_PROFILE
43364399 LOOPOVER_CONFIG_PATH or LOOPOVER_CONFIG_DIR
43374400 LOOPOVER_API_TOKEN, LOOPOVER_MCP_TOKEN, LOOPOVER_TOKEN, or a session from loopover-mcp login
4338- LOOPOVER_LOGIN or GITHUB_LOGIN (default --login for analyze-branch, preflight, review-pr, decision-pack, repo-decision, monitor-open-prs, and agent plan/packet)
4401+ LOOPOVER_LOGIN or GITHUB_LOGIN (default --login for analyze-branch, preflight, review-pr, decision-pack, repo-decision, monitor-open-prs, pr-outcomes, and agent plan/packet)
43394402 GITHUB_TOKEN for non-interactive login bootstrap
43404403 GITTENSOR_SCORE_PREVIEW_CMD
43414404 GITTENSOR_ROOT
@@ -5463,6 +5526,13 @@ function getOpenPrMonitor(login) {
54635526 return apiGet ( `/v1/contributors/${ encodeURIComponent ( login ) } /open-pr-monitor` ) ;
54645527}
54655528
5529+ function getPrOutcomes ( login , limit ) {
5530+ const query = new URLSearchParams ( ) ;
5531+ if ( limit != null ) query . set ( "limit" , String ( limit ) ) ;
5532+ const suffix = query . size > 0 ? `?${ query } ` : "" ;
5533+ return apiGet ( `/v1/contributors/${ encodeURIComponent ( login ) } /pr-outcomes${ suffix } ` ) ;
5534+ }
5535+
54665536// Mirror the API's own `summary` when it sends one, so the CLI and the loopover_monitor_open_prs MCP
54675537// tool (which returns monitor.summary verbatim) never drift into two different sentences for one payload.
54685538function openPrMonitorToolSummary ( login , payload ) {
@@ -5471,6 +5541,12 @@ function openPrMonitorToolSummary(login, payload) {
54715541 return `LoopOver open-PR monitor for ${ login } .` ;
54725542}
54735543
5544+ function prOutcomesToolSummary ( login , payload ) {
5545+ const summary = typeof payload ?. summary === "string" ? payload . summary . trim ( ) : "" ;
5546+ if ( summary ) return summary ;
5547+ return `LoopOver post-merge outcomes for ${ login } .` ;
5548+ }
5549+
54745550function isCacheableDecisionPack ( payload , login ) {
54755551 return payload ?. status === "ready" && typeof payload . login === "string" && payload . login . toLowerCase ( ) === login . toLowerCase ( ) ;
54765552}
0 commit comments