@blogic-cz/agent-tools 0.2.7 → 0.4.0

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.
@@ -0,0 +1,320 @@
1
+ import { Command, Flag } from "effect/unstable/cli";
2
+ import { Effect } from "effect";
3
+
4
+ import { formatOption, logFormatted } from "#shared";
5
+ import { GitHubService } from "#gh/service";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Raw types (gh CLI JSON output)
9
+ // ---------------------------------------------------------------------------
10
+
11
+ type RawTriageIssue = {
12
+ number: number;
13
+ title: string;
14
+ state: string;
15
+ url: string;
16
+ labels: Array<{ name: string }>;
17
+ assignees: Array<{ login: string }>;
18
+ author: { login: string };
19
+ body: string;
20
+ comments: Array<unknown>;
21
+ createdAt: string;
22
+ };
23
+
24
+ type RawTriagePR = {
25
+ number: number;
26
+ title: string;
27
+ state: string;
28
+ url: string;
29
+ labels: Array<{ name: string }>;
30
+ author: { login: string };
31
+ body: string;
32
+ headRefName: string;
33
+ baseRefName: string;
34
+ isDraft: boolean;
35
+ mergeable: string;
36
+ reviewDecision: string;
37
+ statusCheckRollup: Array<{
38
+ name: string;
39
+ status: string;
40
+ conclusion: string | null;
41
+ context: string;
42
+ }>;
43
+ };
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Classification types
47
+ // ---------------------------------------------------------------------------
48
+
49
+ type IssueClassification = "QUESTION" | "BUG" | "FEATURE" | "OTHER";
50
+ type PRClassification = "BUGFIX" | "OTHER";
51
+ type Confidence = "HIGH" | "MEDIUM" | "LOW";
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Output types
55
+ // ---------------------------------------------------------------------------
56
+
57
+ type TriageIssue = {
58
+ number: number;
59
+ title: string;
60
+ author: string;
61
+ labels: string[];
62
+ classification: IssueClassification;
63
+ confidence: Confidence;
64
+ body: string;
65
+ commentsCount: number;
66
+ createdAt: string;
67
+ url: string;
68
+ };
69
+
70
+ type TriagePR = {
71
+ number: number;
72
+ title: string;
73
+ author: string;
74
+ labels: string[];
75
+ classification: PRClassification;
76
+ confidence: Confidence;
77
+ headRefName: string;
78
+ baseRefName: string;
79
+ isDraft: boolean;
80
+ mergeable: string;
81
+ reviewDecision: string;
82
+ ciStatus: string;
83
+ body: string;
84
+ url: string;
85
+ };
86
+
87
+ type TriageSummary = {
88
+ repo: string;
89
+ fetchedAt: string;
90
+ issues: TriageIssue[];
91
+ prs: TriagePR[];
92
+ summary: {
93
+ totalIssues: number;
94
+ totalPRs: number;
95
+ issuesByType: Record<string, number>;
96
+ prsByType: Record<string, number>;
97
+ };
98
+ };
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // Classification logic (pure functions)
102
+ // ---------------------------------------------------------------------------
103
+
104
+ function classifyIssue(
105
+ labels: string[],
106
+ title: string,
107
+ ): { classification: IssueClassification; confidence: Confidence } {
108
+ const lowerLabels = new Set(labels.map((l) => l.toLowerCase()));
109
+ const lowerTitle = title.toLowerCase();
110
+
111
+ // Labels first — HIGH confidence
112
+ if (lowerLabels.has("bug")) {
113
+ return { classification: "BUG", confidence: "HIGH" };
114
+ }
115
+ if (lowerLabels.has("question") || lowerLabels.has("help wanted")) {
116
+ return { classification: "QUESTION", confidence: "HIGH" };
117
+ }
118
+ if (
119
+ lowerLabels.has("enhancement") ||
120
+ lowerLabels.has("feature") ||
121
+ lowerLabels.has("feature request")
122
+ ) {
123
+ return { classification: "FEATURE", confidence: "HIGH" };
124
+ }
125
+
126
+ // Title patterns — MEDIUM confidence
127
+ if (/\[bug\]/i.test(title) || /^bug:/i.test(title) || /^fix:/i.test(title)) {
128
+ return { classification: "BUG", confidence: "MEDIUM" };
129
+ }
130
+ if (
131
+ lowerTitle.includes("?") ||
132
+ /\[question\]/i.test(title) ||
133
+ /how to/i.test(title) ||
134
+ /is it possible/i.test(title)
135
+ ) {
136
+ return { classification: "QUESTION", confidence: "MEDIUM" };
137
+ }
138
+ if (
139
+ /\[feature\]/i.test(title) ||
140
+ /\[enhancement\]/i.test(title) ||
141
+ /\[rfe\]/i.test(title) ||
142
+ /^feat:/i.test(title)
143
+ ) {
144
+ return { classification: "FEATURE", confidence: "MEDIUM" };
145
+ }
146
+
147
+ // Default — LOW confidence
148
+ return { classification: "OTHER", confidence: "LOW" };
149
+ }
150
+
151
+ function classifyPR(
152
+ labels: string[],
153
+ title: string,
154
+ branch: string,
155
+ ): { classification: PRClassification; confidence: Confidence } {
156
+ const lowerLabels = new Set(labels.map((l) => l.toLowerCase()));
157
+
158
+ // Labels first — HIGH confidence
159
+ if (lowerLabels.has("bug")) {
160
+ return { classification: "BUGFIX", confidence: "HIGH" };
161
+ }
162
+
163
+ // Title/branch patterns — MEDIUM confidence
164
+ if (/^fix/i.test(title)) {
165
+ return { classification: "BUGFIX", confidence: "MEDIUM" };
166
+ }
167
+ if (branch.startsWith("fix/") || branch.startsWith("bugfix/")) {
168
+ return { classification: "BUGFIX", confidence: "MEDIUM" };
169
+ }
170
+
171
+ // Default — LOW confidence
172
+ return { classification: "OTHER", confidence: "LOW" };
173
+ }
174
+
175
+ function aggregateCIStatus(checks: RawTriagePR["statusCheckRollup"]): string {
176
+ if (checks.length === 0) return "UNKNOWN";
177
+ if (checks.some((c) => c.conclusion === "failure")) return "FAIL";
178
+ if (checks.some((c) => c.status !== "COMPLETED")) return "PENDING";
179
+ return "PASS";
180
+ }
181
+
182
+ function truncateBody(body: string, maxLength = 500): string {
183
+ if (body.length <= maxLength) return body;
184
+ return body.slice(0, maxLength) + "…";
185
+ }
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // Handler
189
+ // ---------------------------------------------------------------------------
190
+
191
+ const fetchTriageSummary = Effect.fn("issue.fetchTriageSummary")(function* (opts: {
192
+ state: string;
193
+ limit: number;
194
+ }) {
195
+ const gh = yield* GitHubService;
196
+ const repoInfo = yield* gh.getRepoInfo();
197
+
198
+ // Parallel fetch: issues + PRs
199
+ const [rawIssues, rawPRs] = yield* Effect.all(
200
+ [
201
+ gh.runGhJson<RawTriageIssue[]>([
202
+ "issue",
203
+ "list",
204
+ "--state",
205
+ opts.state,
206
+ "--limit",
207
+ String(opts.limit),
208
+ "--json",
209
+ "number,title,state,url,labels,assignees,author,body,comments,createdAt",
210
+ ]),
211
+ gh.runGhJson<RawTriagePR[]>([
212
+ "pr",
213
+ "list",
214
+ "--state",
215
+ opts.state,
216
+ "--limit",
217
+ String(opts.limit),
218
+ "--json",
219
+ "number,title,state,url,labels,author,body,headRefName,baseRefName,isDraft,mergeable,reviewDecision,statusCheckRollup",
220
+ ]),
221
+ ],
222
+ { concurrency: "unbounded" },
223
+ );
224
+
225
+ // Classify + transform issues
226
+ const issues: TriageIssue[] = rawIssues.map((issue) => {
227
+ const labelNames = issue.labels.map((l) => l.name);
228
+ const { classification, confidence } = classifyIssue(labelNames, issue.title);
229
+
230
+ return {
231
+ number: issue.number,
232
+ title: issue.title,
233
+ author: issue.author.login,
234
+ labels: labelNames,
235
+ classification,
236
+ confidence,
237
+ body: truncateBody(issue.body),
238
+ commentsCount: issue.comments.length,
239
+ createdAt: issue.createdAt,
240
+ url: issue.url,
241
+ };
242
+ });
243
+
244
+ // Classify + transform PRs
245
+ const prs: TriagePR[] = rawPRs.map((pr) => {
246
+ const labelNames = pr.labels.map((l) => l.name);
247
+ const { classification, confidence } = classifyPR(labelNames, pr.title, pr.headRefName);
248
+
249
+ return {
250
+ number: pr.number,
251
+ title: pr.title,
252
+ author: pr.author.login,
253
+ labels: labelNames,
254
+ classification,
255
+ confidence,
256
+ headRefName: pr.headRefName,
257
+ baseRefName: pr.baseRefName,
258
+ isDraft: pr.isDraft,
259
+ mergeable: pr.mergeable,
260
+ reviewDecision: pr.reviewDecision,
261
+ ciStatus: aggregateCIStatus(pr.statusCheckRollup ?? []),
262
+ body: truncateBody(pr.body),
263
+ url: pr.url,
264
+ };
265
+ });
266
+
267
+ // Build summary counters
268
+ const issuesByType: Record<string, number> = {};
269
+ for (const issue of issues) {
270
+ issuesByType[issue.classification] = (issuesByType[issue.classification] ?? 0) + 1;
271
+ }
272
+
273
+ const prsByType: Record<string, number> = {};
274
+ for (const pr of prs) {
275
+ prsByType[pr.classification] = (prsByType[pr.classification] ?? 0) + 1;
276
+ }
277
+
278
+ const result: TriageSummary = {
279
+ repo: `${repoInfo.owner}/${repoInfo.name}`,
280
+ fetchedAt: new Date().toISOString(),
281
+ issues,
282
+ prs,
283
+ summary: {
284
+ totalIssues: issues.length,
285
+ totalPRs: prs.length,
286
+ issuesByType,
287
+ prsByType,
288
+ },
289
+ };
290
+
291
+ return result;
292
+ });
293
+
294
+ // ---------------------------------------------------------------------------
295
+ // CLI Command
296
+ // ---------------------------------------------------------------------------
297
+
298
+ export const issueTriageSummaryCommand = Command.make(
299
+ "triage-summary",
300
+ {
301
+ format: formatOption,
302
+ limit: Flag.integer("limit").pipe(
303
+ Flag.withDescription("Maximum number of issues and PRs to fetch"),
304
+ Flag.withDefault(100),
305
+ ),
306
+ state: Flag.choice("state", ["open", "closed", "all"]).pipe(
307
+ Flag.withDescription("Filter by state: open, closed, all"),
308
+ Flag.withDefault("open"),
309
+ ),
310
+ },
311
+ ({ format, limit, state }) =>
312
+ Effect.gen(function* () {
313
+ const summary = yield* fetchTriageSummary({ limit, state });
314
+ yield* logFormatted(summary, format);
315
+ }),
316
+ ).pipe(
317
+ Command.withDescription(
318
+ "Composite: fetch all issues + PRs, classify each, return structured triage summary",
319
+ ),
320
+ );