Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

ai-generators.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

ai-generators.tsBlame257 lines · 1 contributor
3ef4c9dClaude1/**
2 * AI generators — commit messages, PR descriptions, changelogs, issue triage.
3 * All exposed via POST endpoints for CLI hooks + web UI convenience.
4 */
5
6import {
7 getAnthropic,
8 MODEL_HAIKU,
9 MODEL_SONNET,
10 extractText,
11 parseJsonResponse,
12 isAiAvailable,
13} from "./ai-client";
14
15export async function generateCommitMessage(diff: string): Promise<string> {
16 if (!isAiAvailable() || !diff.trim()) {
17 return "chore: update files";
18 }
19 const client = getAnthropic();
20 const message = await client.messages.create({
21 model: MODEL_HAIKU,
22 max_tokens: 512,
23 messages: [
24 {
25 role: "user",
26 content: `Write a single conventional-commit message for this diff. One line subject under 72 chars, lowercase type (feat/fix/refactor/chore/docs/test/perf/style), then optional body wrapped at 72 chars. No backticks or markdown.
27
28Diff:
29\`\`\`
30${diff.slice(0, 40000)}
31\`\`\``,
32 },
33 ],
34 });
35 return extractText(message).trim();
36}
37
38export async function generatePrSummary(
39 title: string,
40 diff: string
41): Promise<string> {
42 if (!isAiAvailable() || !diff.trim()) return "";
43 const client = getAnthropic();
44 const message = await client.messages.create({
45 model: MODEL_SONNET,
46 max_tokens: 1024,
47 messages: [
48 {
49 role: "user",
50 content: `Generate a Markdown PR description for the following changes. Include: Summary (1-3 sentences focused on why), Key changes (bullets), Test plan (bullets), Risks (bullets — omit if minor).
51
52Title: ${title}
53
54Diff:
55\`\`\`
56${diff.slice(0, 60000)}
57\`\`\``,
58 },
59 ],
60 });
61 return extractText(message).trim();
62}
63
64export async function generateChangelog(
65 repoFullName: string,
66 fromRef: string | null,
67 toRef: string,
68 commits: Array<{ sha: string; message: string; author: string }>
69): Promise<string> {
70 if (!isAiAvailable() || commits.length === 0) {
71 const header = `## ${toRef}${fromRef ? ` (since ${fromRef})` : ""}\n\n`;
72 return (
73 header +
74 commits
75 .map((c) => `- ${c.message.split("\n")[0]} (${c.sha.slice(0, 7)}) — ${c.author}`)
76 .join("\n")
77 );
78 }
79 const client = getAnthropic();
80 const commitBlob = commits
81 .slice(0, 200)
82 .map((c) => `- ${c.sha.slice(0, 7)} ${c.message.split("\n")[0]} — ${c.author}`)
83 .join("\n");
84 const message = await client.messages.create({
85 model: MODEL_SONNET,
86 max_tokens: 2048,
87 messages: [
88 {
89 role: "user",
90 content: `Generate a polished release-notes changelog in Markdown for ${repoFullName}.
91
92Release: ${toRef}
93Previous: ${fromRef || "(initial)"}
94
95Group commits by category (Features, Fixes, Performance, Refactoring, Docs, Other). Omit empty categories. Use bullet points. Keep it concise — no marketing fluff, just the facts a user of the project needs. Reference SHAs in parentheses.
96
97Commits:
98${commitBlob}`,
99 },
100 ],
101 });
102 return extractText(message).trim();
103}
104
a9ada5fClaude105export interface IssueTriage {
3ef4c9dClaude106 suggestedLabels: string[];
107 duplicateOfIssueNumber: number | null;
108 priority: "critical" | "high" | "medium" | "low";
109 summary: string;
110}
111
112export async function triageIssue(
113 title: string,
114 body: string,
115 existingLabels: string[],
116 recentIssues: Array<{ number: number; title: string }>
117): Promise<IssueTriage> {
118 const fallback: IssueTriage = {
119 suggestedLabels: [],
120 duplicateOfIssueNumber: null,
121 priority: "medium",
122 summary: "",
123 };
124 if (!isAiAvailable()) return fallback;
125 const client = getAnthropic();
126 const message = await client.messages.create({
127 model: MODEL_HAIKU,
128 max_tokens: 512,
129 messages: [
130 {
131 role: "user",
132 content: `Triage this new GitHub-style issue.
133
134Title: ${title}
135Body:
136${body.slice(0, 4000)}
137
138Available labels: ${existingLabels.join(", ") || "(none)"}
139Recent issues (to check for duplicates):
140${recentIssues.map((i) => `#${i.number}: ${i.title}`).join("\n")}
141
142Respond ONLY with JSON:
143{
144 "suggestedLabels": ["label1", "label2"],
145 "duplicateOfIssueNumber": null,
146 "priority": "medium",
147 "summary": "one sentence"
148}
149Only suggest labels from the available list. Set duplicateOfIssueNumber only when confident.`,
150 },
151 ],
152 });
153 const parsed = parseJsonResponse<IssueTriage>(extractText(message));
154 if (!parsed) return fallback;
155 return {
156 suggestedLabels: Array.isArray(parsed.suggestedLabels)
157 ? parsed.suggestedLabels.filter((l) => existingLabels.includes(l))
158 : [],
159 duplicateOfIssueNumber:
160 typeof parsed.duplicateOfIssueNumber === "number"
161 ? parsed.duplicateOfIssueNumber
162 : null,
163 priority: (["critical", "high", "medium", "low"] as const).includes(
164 parsed.priority as never
165 )
166 ? parsed.priority
167 : "medium",
168 summary: typeof parsed.summary === "string" ? parsed.summary : "",
169 };
170}
3cbe3d6Claude171
172/**
173 * D3 — AI PR triage. Reads the PR title/body + optional diff summary and
174 * suggests labels, reviewers, and a priority. Never throws; degrades to
175 * empty suggestions when the Anthropic key is absent.
176 */
177export interface PrTriage {
178 suggestedLabels: string[];
179 suggestedReviewerUsernames: string[];
180 priority: "critical" | "high" | "medium" | "low";
181 riskArea: "frontend" | "backend" | "infra" | "docs" | "tests" | "mixed";
182 summary: string;
183}
184
185export async function triagePullRequest(
186 title: string,
187 body: string,
188 diffSummary: string,
189 availableLabels: string[],
190 candidateReviewers: string[]
191): Promise<PrTriage> {
192 const fallback: PrTriage = {
193 suggestedLabels: [],
194 suggestedReviewerUsernames: [],
195 priority: "medium",
196 riskArea: "mixed",
197 summary: "",
198 };
199 if (!isAiAvailable()) return fallback;
200 try {
201 const client = getAnthropic();
202 const message = await client.messages.create({
203 model: MODEL_HAIKU,
204 max_tokens: 512,
205 messages: [
206 {
207 role: "user",
208 content: `Triage this new pull request.
209
210Title: ${title}
211Body:
212${body.slice(0, 4000)}
213
214Diff summary (paths + line counts):
215${diffSummary.slice(0, 4000)}
216
217Available labels: ${availableLabels.join(", ") || "(none)"}
218Candidate reviewers (usernames): ${candidateReviewers.join(", ") || "(none)"}
219
220Respond ONLY with JSON:
221{
222 "suggestedLabels": ["label1"],
223 "suggestedReviewerUsernames": ["alice"],
224 "priority": "medium",
225 "riskArea": "backend",
226 "summary": "one-sentence description of the change"
227}
228Only pick labels from the available list. Only pick reviewers from the candidate list. Priority must be one of critical|high|medium|low. riskArea must be one of frontend|backend|infra|docs|tests|mixed.`,
229 },
230 ],
231 });
232 const parsed = parseJsonResponse<PrTriage>(extractText(message));
233 if (!parsed) return fallback;
234 const allowedRisk = ["frontend", "backend", "infra", "docs", "tests", "mixed"] as const;
235 const allowedPriority = ["critical", "high", "medium", "low"] as const;
236 return {
237 suggestedLabels: Array.isArray(parsed.suggestedLabels)
238 ? parsed.suggestedLabels.filter((l) => availableLabels.includes(l))
239 : [],
240 suggestedReviewerUsernames: Array.isArray(parsed.suggestedReviewerUsernames)
241 ? parsed.suggestedReviewerUsernames.filter((u) =>
242 candidateReviewers.includes(u)
243 )
244 : [],
245 priority: allowedPriority.includes(parsed.priority as never)
246 ? parsed.priority
247 : "medium",
248 riskArea: allowedRisk.includes(parsed.riskArea as never)
249 ? parsed.riskArea
250 : "mixed",
251 summary: typeof parsed.summary === "string" ? parsed.summary : "",
252 };
253 } catch (err) {
254 console.error("[triagePullRequest]", err);
255 return fallback;
256 }
257}