@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.
- package/README.md +82 -5
- package/package.json +1 -1
- package/src/gh-tool/index.ts +33 -6
- package/src/gh-tool/issue/commands.ts +145 -0
- package/src/gh-tool/issue/core.ts +208 -0
- package/src/gh-tool/issue/index.ts +9 -0
- package/src/gh-tool/issue/triage.ts +320 -0
- package/src/gh-tool/release.ts +561 -0
- package/src/gh-tool/issue.ts +0 -361
|
@@ -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
|
+
);
|