@reneturcios/greenlight 0.0.0-stage → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Greenlight MCP server (stdio). Same engine as the CLI, exposed as tools
3
+ * so coding agents (Claude, Cursor, Cline) can pre-flight an issue before
4
+ * spending tokens on code for a taken or gated issue.
5
+ */
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { z } from "zod";
9
+ import { parseIssueUrl, buildIssueSnapshot, scanCandidates, fetchContributing, countStarterIssues, detectRepoPolicy, } from "./github.js";
10
+ import { scoreIssue } from "./verdict.js";
11
+ import { parseGates } from "./gates.js";
12
+ import { scoreFriction } from "./friction.js";
13
+ import { createRestClient, claimIssue, prepIssue, openPr, AutomationRefusal, } from "./automate.js";
14
+ const server = new McpServer({ name: "greenlight", version: "0.3.2" });
15
+ const text = (obj) => ({
16
+ content: [{ type: "text", text: JSON.stringify(obj, null, 2) }],
17
+ });
18
+ server.tool("check_issue", "Verdict on whether a GitHub issue is actually landable for an outside contributor. Returns GO, RISKY, TAKEN, GATED, or STALE with reasons, claim signals, and detected repo gates. Call this before writing code for an issue.", {
19
+ issue_url: z.string().describe("Full GitHub issue URL, e.g. https://github.com/owner/repo/issues/123"),
20
+ }, async ({ issue_url }) => {
21
+ try {
22
+ const { owner, repo, number } = parseIssueUrl(issue_url);
23
+ const snapshot = await buildIssueSnapshot(owner, repo, number);
24
+ const result = scoreIssue(snapshot);
25
+ return text({ issue: snapshot.sourceUrl, title: snapshot.title, verdict: result.verdict, reasons: result.reasons, flags: result.flags, claims: result.claims, gates: result.gates });
26
+ }
27
+ catch (err) {
28
+ return text({ error: err instanceof Error ? err.message : String(err) });
29
+ }
30
+ });
31
+ server.tool("scan_repo", "Rank a repo's open issues by verdict, GO first. Starts with label-tagged good-first-issue / help-wanted issues; when the repo does not use those labels, falls back to the most recent open unassigned issues (flagged as fallback). Use this to pick a winnable issue instead of a taken one.", {
32
+ repo: z.string().describe("owner/repo, e.g. PrefectHQ/fastmcp"),
33
+ }, async ({ repo }) => {
34
+ try {
35
+ const m = /^([^/]+)\/([^/]+)$/.exec(repo.trim());
36
+ if (!m)
37
+ return text({ error: `Expected owner/repo, got: ${repo}` });
38
+ const { issues, fallbackUsed } = await scanCandidates(m[1], m[2], { limit: 10 });
39
+ const results = [];
40
+ for (const issue of issues) {
41
+ try {
42
+ const snapshot = await buildIssueSnapshot(m[1], m[2], issue.number);
43
+ const scored = scoreIssue(snapshot);
44
+ results.push({ url: issue.url, title: issue.title, verdict: scored.verdict, reasons: scored.reasons, flags: scored.flags });
45
+ }
46
+ catch (err) {
47
+ results.push({ url: issue.url, title: issue.title, verdict: "ERROR", reasons: [err instanceof Error ? err.message : String(err)], flags: [] });
48
+ }
49
+ }
50
+ const rank = { GO: 0, RISKY: 1, GATED: 2, STALE: 3, TAKEN: 4, ERROR: 5 };
51
+ results.sort((a, b) => (rank[a.verdict] ?? 9) - (rank[b.verdict] ?? 9));
52
+ return text({ repo, fallback: fallbackUsed, results });
53
+ }
54
+ catch (err) {
55
+ return text({ error: err instanceof Error ? err.message : String(err) });
56
+ }
57
+ });
58
+ server.tool("repo_friction", "Friction score 0-100 for a repo (100 = easiest to land an outside PR). Based on detected contribution gates and computed outcome signals.", {
59
+ repo: z.string().describe("owner/repo, e.g. PrefectHQ/fastmcp"),
60
+ }, async ({ repo }) => {
61
+ try {
62
+ const m = /^([^/]+)\/([^/]+)$/.exec(repo.trim());
63
+ if (!m)
64
+ return text({ error: `Expected owner/repo, got: ${repo}` });
65
+ const [contributing, policy, starterCount] = await Promise.all([
66
+ fetchContributing(m[1], m[2]),
67
+ detectRepoPolicy(m[1], m[2]),
68
+ countStarterIssues(m[1], m[2]),
69
+ ]);
70
+ const gates = parseGates(contributing, { collaboratorsOnlyPolicy: policy.collaboratorsOnly });
71
+ const report = scoreFriction({
72
+ owner: m[1],
73
+ repo: m[2],
74
+ gates,
75
+ externalPRMergeRate: null,
76
+ medianFirstReviewHours: null,
77
+ maintainerIssueResponseHours: null,
78
+ openGoodFirstIssueCount: starterCount,
79
+ note: "live",
80
+ });
81
+ return text(report);
82
+ }
83
+ catch (err) {
84
+ return text({ error: err instanceof Error ? err.message : String(err) });
85
+ }
86
+ });
87
+ server.tool("claim_issue", "Post a claim comment on a GitHub issue, shaped by the repo's contribution gates. Runs the verdict pipeline first and refuses on TAKEN/STALE verdicts or vouch-gated repos. DRY RUN BY DEFAULT: returns the comment plan without posting unless execute is true, and posting requires GITHUB_TOKEN.", {
88
+ issue_url: z.string().describe("Full GitHub issue URL, e.g. https://github.com/owner/repo/issues/123"),
89
+ execute: z.boolean().default(false).describe("Post the comment for real. Defaults to false: dry run returns the plan only."),
90
+ force: z.boolean().default(false).describe("Claim even on a TAKEN or STALE verdict."),
91
+ }, async ({ issue_url, execute, force }) => {
92
+ try {
93
+ const { owner, repo, number } = parseIssueUrl(issue_url);
94
+ const snapshot = await buildIssueSnapshot(owner, repo, number);
95
+ const plan = await claimIssue(createRestClient(), snapshot, { yes: execute, force });
96
+ return text({
97
+ issue: snapshot.sourceUrl,
98
+ verdict: plan.verdict.verdict,
99
+ reasons: plan.verdict.reasons,
100
+ commentBody: plan.commentBody,
101
+ commentUrl: plan.commentUrl,
102
+ executed: plan.executed,
103
+ });
104
+ }
105
+ catch (err) {
106
+ if (err instanceof AutomationRefusal)
107
+ return text({ refused: true, reason: err.message });
108
+ return text({ error: err instanceof Error ? err.message : String(err) });
109
+ }
110
+ });
111
+ server.tool("prep_issue", "Prepare to work an issue: check/create your fork and a branch named from the issue title (learning the repo's branch convention from recent PRs when detectable), and write a local PREP.md checklist (verdict, gates, DCO reminders, next commands). Never clones. DRY RUN BY DEFAULT: no fork or branch is created unless execute is true; creating them requires GITHUB_TOKEN.", {
112
+ issue_url: z.string().describe("Full GitHub issue URL, e.g. https://github.com/owner/repo/issues/123"),
113
+ execute: z.boolean().default(false).describe("Actually create the fork/branch. Defaults to false: dry run returns the plan only."),
114
+ dir: z.string().optional().describe("Directory for PREP.md. Defaults to ./greenlight-prep/<repo>-<number>/"),
115
+ }, async ({ issue_url, execute, dir }) => {
116
+ try {
117
+ const { owner, repo, number } = parseIssueUrl(issue_url);
118
+ const snapshot = await buildIssueSnapshot(owner, repo, number);
119
+ const plan = await prepIssue(createRestClient(), snapshot, { yes: execute, dir });
120
+ return text({
121
+ issue: snapshot.sourceUrl,
122
+ verdict: plan.verdict.verdict,
123
+ reasons: plan.verdict.reasons,
124
+ branch: plan.branch,
125
+ branchDefault: plan.branchDefault,
126
+ branchPatternNote: plan.branchPatternNote,
127
+ forkReady: plan.forkReady,
128
+ forkCreated: plan.forkCreated,
129
+ branchReady: plan.branchReady,
130
+ branchCreated: plan.branchCreated,
131
+ prepFile: plan.prepFile,
132
+ nextCommands: plan.nextCommands,
133
+ conventions: plan.conventions ?? null,
134
+ });
135
+ }
136
+ catch (err) {
137
+ if (err instanceof AutomationRefusal)
138
+ return text({ refused: true, reason: err.message });
139
+ return text({ error: err instanceof Error ? err.message : String(err) });
140
+ }
141
+ });
142
+ server.tool("open_pr", "Open a pull request for an issue from your fork branch. Verifies the branch exists, is ahead of upstream, and (on DCO repos) that every commit carries a Signed-off-by line. Composes the PR title and body (Closes #N plus verdict and gates at open time). DRY RUN BY DEFAULT: returns the composed PR without opening it unless execute is true; opening requires GITHUB_TOKEN.", {
143
+ issue_url: z.string().describe("Full GitHub issue URL, e.g. https://github.com/owner/repo/issues/123"),
144
+ branch: z.string().describe("Fork branch to open the PR from, e.g. feat/resume-61"),
145
+ execute: z.boolean().default(false).describe("Open the PR for real. Defaults to false: dry run returns the plan only."),
146
+ title: z.string().optional().describe("PR title override. Defaults to the branch's latest commit subject or the issue title."),
147
+ }, async ({ issue_url, branch, execute, title }) => {
148
+ try {
149
+ const { owner, repo, number } = parseIssueUrl(issue_url);
150
+ const snapshot = await buildIssueSnapshot(owner, repo, number);
151
+ const plan = await openPr(createRestClient(), snapshot, { branch, title, yes: execute });
152
+ return text({
153
+ issue: snapshot.sourceUrl,
154
+ branch: plan.branch,
155
+ head: plan.head,
156
+ base: plan.base,
157
+ title: plan.title,
158
+ body: plan.body,
159
+ checks: plan.checks,
160
+ prUrl: plan.prUrl,
161
+ executed: plan.executed,
162
+ });
163
+ }
164
+ catch (err) {
165
+ if (err instanceof AutomationRefusal)
166
+ return text({ refused: true, reason: err.message });
167
+ return text({ error: err instanceof Error ? err.message : String(err) });
168
+ }
169
+ });
170
+ const transport = new StdioServerTransport();
171
+ await server.connect(transport);
172
+ console.error("greenlight MCP server running on stdio");
173
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Link strength: how strongly a PR is tied to an issue.
3
+ *
4
+ * Issue #1 (2026-10-13): a plain text search for "#N" in PR bodies produced
5
+ * false positives. shadcn-labs/pdfcn PR #91 closes issue #70 but lists sibling
6
+ * issues #60-#65 in its body, which wrongly marked #61 and #71 as TAKEN.
7
+ *
8
+ * Two levels only:
9
+ * - "closing": the PR is this issue's PR. Closing keywords in title/body
10
+ * ("Closes #61"), the issue number in the PR title, or the issue number
11
+ * as a standalone token in the branch name ("feat/61-resume").
12
+ * - "mention": anything else. Bare "#61" in the body, a timeline-only
13
+ * cross-reference, or a search hit with no closing evidence.
14
+ */
15
+ import type { LinkedPR } from "./types.js";
16
+ export type LinkStrength = "closing" | "mention";
17
+ export interface LinkInput {
18
+ title: string;
19
+ body: string;
20
+ branch: string;
21
+ issueNumber: number;
22
+ }
23
+ export declare function classifyLinkStrength({ title, body, branch, issueNumber }: LinkInput): LinkStrength;
24
+ /** A PR counts as the issue's PR only when its link strength is "closing". */
25
+ export declare function isClosingPR(pr: Pick<LinkedPR, "linkStrength"> & {
26
+ linkStrength?: LinkStrength;
27
+ }): boolean;
@@ -0,0 +1,25 @@
1
+ export function classifyLinkStrength({ title, body, branch, issueNumber }) {
2
+ const n = String(Math.trunc(Math.abs(issueNumber)));
3
+ // Closing keywords aimed at this issue: "Closes #61", "fixes #61", "Resolved: #61".
4
+ const closingRe = new RegExp(`(close[sd]?|closing|fix(e[sd])?|fixes|resolve[sd]?|resolves)\\s*:?\\s*#${n}\\b`, "i");
5
+ if (closingRe.test(title) || closingRe.test(body))
6
+ return "closing";
7
+ // Issue number in the PR title ("#61: add resume block") is author intent.
8
+ const titleRe = new RegExp(`#${n}\\b`, "i");
9
+ if (titleRe.test(title))
10
+ return "closing";
11
+ // Issue number as a standalone token in the branch name ("61-resume-block",
12
+ // "feat/61-x", "issue-61", "fix-61"). The token test keeps "feat/610-x"
13
+ // from matching issue 61.
14
+ if (branch) {
15
+ const branchRe = new RegExp(`(^|[^0-9])${n}([^0-9]|$)`);
16
+ if (branchRe.test(branch))
17
+ return "closing";
18
+ }
19
+ return "mention";
20
+ }
21
+ /** A PR counts as the issue's PR only when its link strength is "closing". */
22
+ export function isClosingPR(pr) {
23
+ return (pr.linkStrength ?? "mention") === "closing";
24
+ }
25
+ //# sourceMappingURL=linkstrength.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linkstrength.js","sourceRoot":"","sources":["../src/linkstrength.ts"],"names":[],"mappings":"AAyBA,MAAM,UAAU,oBAAoB,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAa;IAClF,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAEpD,oFAAoF;IACpF,MAAM,SAAS,GAAG,IAAI,MAAM,CAC1B,0EAA0E,CAAC,KAAK,EAChF,GAAG,CACJ,CAAC;IACF,IAAI,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IAEpE,2EAA2E;IAC3E,MAAM,OAAO,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAC5C,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAE1C,4EAA4E;IAC5E,wEAAwE;IACxE,0BAA0B;IAC1B,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC;QACxD,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC;YAAE,OAAO,SAAS,CAAC;IAC9C,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,WAAW,CAAC,EAAoE;IAC9F,OAAO,CAAC,EAAE,CAAC,YAAY,IAAI,SAAS,CAAC,KAAK,SAAS,CAAC;AACtD,CAAC"}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Core data shapes for Greenlight. All scoring runs on these shapes,
3
+ * whether they came from the live GitHub API or from a fixture file.
4
+ */
5
+ export type Verdict = "GO" | "RISKY" | "TAKEN" | "GATED" | "STALE";
6
+ /** Minimal comment representation used for claim detection. */
7
+ export interface CommentSignal {
8
+ author: string;
9
+ body: string;
10
+ createdAt: string;
11
+ }
12
+ /** A pull request that references the issue (linked, closes, or mentions). */
13
+ export interface LinkedPR {
14
+ number: number;
15
+ state: "open" | "closed" | "merged";
16
+ title: string;
17
+ url: string;
18
+ /** How strongly the PR is tied to the issue. Only "closing" PRs take an issue. */
19
+ linkStrength: "closing" | "mention";
20
+ }
21
+ /** A pull request referenced in an issue comment, fetched and checked for issue overlap. */
22
+ export interface MentionedPR {
23
+ number: number;
24
+ url: string;
25
+ state: "open" | "closed" | "merged";
26
+ /** True when the PR title/body references this issue's number. */
27
+ mentionsIssue: boolean;
28
+ /**
29
+ * True when the citing comment came from the issue author. The open-PR
30
+ * downgrade (rule 11) only trusts non-author heads-ups, but a
31
+ * maintainer narrating what already shipped (rule 12) is usually the
32
+ * issue author, so partially-addressed detection reads both.
33
+ */
34
+ fromAuthor?: boolean;
35
+ }
36
+ /** A snapshot of one issue: everything the verdict engine needs. */
37
+ export interface IssueSnapshot {
38
+ sourceUrl: string;
39
+ owner: string;
40
+ repo: string;
41
+ number: number;
42
+ state: "open" | "closed";
43
+ title: string;
44
+ body?: string;
45
+ /** Per-file policy texts (CONTRIBUTING variants, AGENTS.md, CLAUDE.md). */
46
+ houseRules?: {
47
+ file: string;
48
+ text: string;
49
+ }[];
50
+ author: string;
51
+ assignees: string[];
52
+ labels: string[];
53
+ comments: CommentSignal[];
54
+ linkedPRs: LinkedPR[];
55
+ /** PRs cited in non-author comments, scored for overlap with this issue. Absent on older fixtures. */
56
+ commentMentionedPRs?: MentionedPR[];
57
+ createdAt: string;
58
+ updatedAt: string;
59
+ contributing?: string;
60
+ /** Gates supplied from a gatefile contribution-gates.json, already mapped to Greenlight kinds. */
61
+ externalGates?: Gate[];
62
+ }
63
+ /** One detected claim signal from a comment. */
64
+ export interface ClaimSignal {
65
+ author: string;
66
+ createdAt: string;
67
+ ageDays: number;
68
+ strength: "strong" | "soft";
69
+ phrase: string;
70
+ /** "full" claims the whole issue; "partial" scopes itself to part of the work. */
71
+ scope: "full" | "partial";
72
+ /** The scope word matched for partial claims (e.g. "fallback", "remaining"). */
73
+ scopeWord?: string;
74
+ }
75
+ /** A contribution gate parsed from CONTRIBUTING / repo policy. */
76
+ export type GateKind = "assignment_required" | "claim_first" | "vouch_required" | "cla_required" | "dco_required" | "discussion_first" | "collaborators_only";
77
+ export interface Gate {
78
+ kind: GateKind;
79
+ detected: boolean;
80
+ evidence: string;
81
+ /** File the gate evidence came from, e.g. "CONTRIBUTING.md" or "AGENTS.md". */
82
+ source?: string;
83
+ }
84
+ /** Signals used to score repo friction. Fields that cannot be computed stay null. */
85
+ export interface RepoSignals {
86
+ owner: string;
87
+ repo: string;
88
+ gates: Gate[];
89
+ externalPRMergeRate?: number | null;
90
+ medianFirstReviewHours?: number | null;
91
+ maintainerIssueResponseHours?: number | null;
92
+ openGoodFirstIssueCount?: number | null;
93
+ note?: string;
94
+ }
95
+ /** The verdict for one issue, with the reasons in scoring order. */
96
+ export interface VerdictResult {
97
+ verdict: Verdict;
98
+ reasons: string[];
99
+ claims: ClaimSignal[];
100
+ gates: Gate[];
101
+ flags: string[];
102
+ }
103
+ /**
104
+ * Repo conventions learned during prep: the local rituals a contributor
105
+ * must follow. All fields are best-effort; a repo may provide none.
106
+ */
107
+ export interface RepoConventions {
108
+ /** Policy files present at the repo, e.g. ["CONTRIBUTING.md", "AGENTS.md"]. */
109
+ houseRuleFiles: string[];
110
+ /** Backticked run commands quoted in the house rules, e.g. ["pnpm verify"]. */
111
+ verifyCommands: string[];
112
+ /** True when a .changes/ or .changeset directory exists at the repo root. */
113
+ changesDir: boolean;
114
+ /** Which one was found, e.g. ".changes" or ".changeset". */
115
+ changesDirName?: string;
116
+ /** Root package.json script names worth running before pushing (subset of test/verify/lint/typecheck/build). */
117
+ packageScripts: string[];
118
+ /** True when 60%+ of the last 20 default-branch commits match Conventional Commits. */
119
+ conventionalCommits: boolean;
120
+ /** One short example commit subject when conventionalCommits is true. */
121
+ exampleCommit?: string;
122
+ }
123
+ /** A scored repo, stored in data/friction-index.json. */
124
+ export interface FrictionReport {
125
+ owner: string;
126
+ repo: string;
127
+ score: number;
128
+ gates: Gate[];
129
+ reasons: string[];
130
+ signals: RepoSignals;
131
+ }
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Core data shapes for Greenlight. All scoring runs on these shapes,
3
+ * whether they came from the live GitHub API or from a fixture file.
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Deterministic availability verdict.
3
+ *
4
+ * Scoring order (first match wins; all checks are deterministic, no LLM):
5
+ *
6
+ * 1. Closed issue -> TAKEN (reason: issue closed). A closed issue is never landable.
7
+ * 2. Any assignee -> TAKEN. Someone officially owns it.
8
+ * 3. Open linked PR that CLOSES the issue -> TAKEN. Code already exists.
9
+ * A PR that merely mentions the issue number (bare #N in the body,
10
+ * timeline cross-reference only) does not take it: it only adds the
11
+ * "weak-pr-mention" flag (issue #1, seen on shadcn-labs/pdfcn#61 and
12
+ * #71, where PR #91 closes #70 but lists sibling issues in its body).
13
+ * 4. Two or more strong whole-issue claimants -> TAKEN (claim race). Seen on
14
+ * langchain-ai/langchain#41185 (3 claimants while still "unassigned").
15
+ * Scoped (partial) claims do not count toward a race: part of the work
16
+ * stays open by definition.
17
+ * 5. One strong, fresh (<= 45 days) whole-issue claim -> RISKY, flag "claimed".
18
+ * Seen on shadcn-labs/pdfcn#58, claimed the same day we vetted it.
19
+ * 5b. A fresh strong claim scoped to part of the issue -> RISKY, flags
20
+ * "claimed" + "partial-claim", with a reason naming the claimer and the
21
+ * scope ("claimed the fallback part ...; the rest may still be open").
22
+ * Seen on reticlehq/reticle#691 ("Taking the remaining fallback ask").
23
+ * 6. Strong whole-issue claim older than 45 days with no closing PR -> RISKY,
24
+ * flag "stale_claim". The claimant probably walked away but nobody
25
+ * knows for sure (seen on modelcontextprotocol/python-sdk#3287,
26
+ * claimed 2026-08-11, never opened a PR). A closing PR, open or not,
27
+ * counts as "a PR was opened" here; a mere mention does not. Stale
28
+ * partial claims get the same treatment, scoped.
29
+ * 7. Any hard gate (assignment / vouch / collaborators_only) with no
30
+ * assignee -> GATED. The code might be welcome, but the ritual blocks
31
+ * you: PrefectHQ/fastmcp#5718 (assignment), triggerdotdev/trigger.dev#5016 (vouch).
32
+ * 8. No activity for 180+ days -> STALE. The issue is probably abandoned,
33
+ * the repro may not even hold on current code.
34
+ * 9. Soft claim-first gate (comment before starting, shadcn-labs/pdfcn
35
+ * style): never GATED. It caps an otherwise-GO verdict at RISKY with a
36
+ * note, following the same downgrade shape as the weak-PR-mention rule.
37
+ * 10. Otherwise -> GO, unless an open PR only mentions the issue, which
38
+ * downgrades GO to RISKY ("verify it is not already being worked").
39
+ * 11. An open PR referenced in a non-author comment that mentions this
40
+ * issue number downgrades an otherwise-GO verdict to RISKY, flag
41
+ * "comment-mentioned-pr" ("PR #NNN referenced in comments may already
42
+ * cover part of this issue"). A merged one is only an informational
43
+ * note; neither can produce TAKEN. Seen on reticlehq/reticle#691.
44
+ * 12. Partially-addressed: merged PRs that reference the issue number
45
+ * (comment-mentioned or linked), or maintainer comments signaling part
46
+ * of the work already shipped ("Half addressed", "leaving this open
47
+ * for ...", "still open", "part of this is shipped in v..."), mean
48
+ * part of the issue is done while the rest stays open. A plain GO
49
+ * hides that. Add flag "partially-addressed", cap an otherwise-GO
50
+ * verdict at RISKY with a scoping reason; never TAKEN. Seen on
51
+ * reticlehq/reticle#894 (half A shipped in v3.1.0 via #905/#915,
52
+ * half B still open).
53
+ */
54
+ import type { IssueSnapshot, VerdictResult } from "./types.js";
55
+ declare const FRESH_CLAIM_DAYS = 45;
56
+ declare const STALE_INACTIVITY_DAYS = 180;
57
+ export declare function scoreIssue(snapshot: IssueSnapshot, now?: Date): VerdictResult;
58
+ /** "/shipped in v/i near an issue reference" is the last pattern; the
59
+ * four above run on every comment, and this one runs below with its
60
+ * issue-reference guard. */
61
+ export declare function hasShippedInVersionSignal(comments: {
62
+ body: string;
63
+ }[], issueNumber: number): boolean;
64
+ export { FRESH_CLAIM_DAYS, STALE_INACTIVITY_DAYS };