@blogic-cz/agent-tools 0.14.33 → 0.14.35

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "0.14.33",
3
+ "version": "0.14.35",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, sessions, and audit",
5
5
  "keywords": [
6
6
  "agent",
@@ -13,6 +13,7 @@ import {
13
13
  issueEditCommand,
14
14
  issueListCommand,
15
15
  issueReopenCommand,
16
+ issueSnapshotBatchCommand,
16
17
  issueTriageCommand,
17
18
  issueViewCommand,
18
19
  } from "./issue";
@@ -36,6 +37,7 @@ import {
36
37
  prChecksFailedCommand,
37
38
  prRerunChecksCommand,
38
39
  prReplyAndResolveCommand,
40
+ prReviewTriageBatchCommand,
39
41
  prReviewTriageCommand,
40
42
  } from "./pr/index";
41
43
  import { branchRenameCommand } from "./branch";
@@ -84,18 +86,20 @@ const prCommand = Command.make("pr", {}).pipe(
84
86
  prRerunChecksCommand,
85
87
  prReplyAndResolveCommand,
86
88
  prReviewTriageCommand,
89
+ prReviewTriageBatchCommand,
87
90
  ]),
88
91
  );
89
92
 
90
93
  const issueCommand = Command.make("issue", {}).pipe(
91
94
  Command.withDescription(
92
- "Issue operations (list, view, comments, triage, close, reopen, comment, edit)",
95
+ "Issue operations (list, view, comments, triage, snapshot-batch, close, reopen, comment, edit)",
93
96
  ),
94
97
  Command.withSubcommands([
95
98
  issueListCommand,
96
99
  issueViewCommand,
97
100
  issueCommentsCommand,
98
101
  issueTriageCommand,
102
+ issueSnapshotBatchCommand,
99
103
  issueCloseCommand,
100
104
  issueReopenCommand,
101
105
  issueCommentCommand,
@@ -7,4 +7,4 @@ export {
7
7
  issueReopenCommand,
8
8
  issueViewCommand,
9
9
  } from "./commands";
10
- export { issueTriageCommand } from "./triage";
10
+ export { issueSnapshotBatchCommand, issueTriageCommand } from "./triage";
@@ -1,10 +1,11 @@
1
1
  import { Command, Flag } from "effect/unstable/cli";
2
2
  import { Effect, Option, Schema } from "effect";
3
3
 
4
- import type { IssueComment } from "#gh/types";
4
+ import type { CheckResult, IssueComment, PRViewInfo, ReviewThread } from "#gh/types";
5
5
 
6
6
  import { formatOption, logFormatted } from "#shared";
7
7
  import { GitHubService } from "#gh/service";
8
+ import { fetchReviewTriage } from "#gh/pr/commands";
8
9
 
9
10
  import { fetchIssueComments } from "./core";
10
11
 
@@ -61,6 +62,39 @@ type FullIssueTriage = {
61
62
  comments: IssueComment[];
62
63
  };
63
64
 
65
+ type ReviewTriageSnapshot = {
66
+ readonly classification: {
67
+ readonly status: "clear" | "needs_investigation";
68
+ readonly reasons: readonly string[];
69
+ };
70
+ readonly info: PRViewInfo;
71
+ readonly unresolvedThreads: readonly ReviewThread[];
72
+ readonly visibleOpenThreads: readonly ReviewThread[];
73
+ readonly summary: {
74
+ readonly visibleOpenReviewThreadsCount: number;
75
+ readonly unrepliedReviewThreadsCount: number;
76
+ readonly unresolvedReviewThreadsCount: number;
77
+ readonly latestIssueComment: IssueComment | null;
78
+ };
79
+ readonly checks: readonly CheckResult[];
80
+ };
81
+
82
+ type IssueSnapshotClassification = {
83
+ readonly status: "clear" | "needs_investigation";
84
+ readonly reasons: readonly string[];
85
+ };
86
+
87
+ type IssueSnapshot = {
88
+ readonly issue: TriageIssue;
89
+ readonly body: string;
90
+ readonly commentsCount: number;
91
+ readonly comments: readonly IssueComment[];
92
+ readonly eligible: boolean;
93
+ readonly linkedPullRequestNumbers: readonly number[];
94
+ readonly linkedPullRequests: readonly ReviewTriageSnapshot[];
95
+ readonly classification: IssueSnapshotClassification;
96
+ };
97
+
64
98
  function truncateBody(body: string, maxLength = 500): string {
65
99
  if (body.length <= maxLength) return body;
66
100
  return body.slice(0, maxLength) + "…";
@@ -129,6 +163,97 @@ export const fetchIssueTriage = Effect.fn("issue.fetchIssueTriage")(function* (o
129
163
  return result;
130
164
  });
131
165
 
166
+ export const parseIssueNumbers = (input: string): readonly number[] =>
167
+ input
168
+ .split(",")
169
+ .map((part) => Number.parseInt(part.trim(), 10))
170
+ .filter((number) => Number.isInteger(number) && number > 0);
171
+
172
+ export const collectLinkedPullRequestNumbers = (
173
+ body: string,
174
+ comments: readonly { readonly body: string }[],
175
+ ): readonly number[] => {
176
+ const text = [body, ...comments.map((comment) => comment.body)].join("\n");
177
+ const numbers = new Set<number>();
178
+ for (const match of text.matchAll(/(?:pull\/|\/pulls\/)(\d+)|\bPR\s+#?(\d+)/gi)) {
179
+ const number = Number.parseInt(match[1] ?? match[2] ?? "", 10);
180
+ if (Number.isInteger(number) && number > 0) {
181
+ numbers.add(number);
182
+ }
183
+ }
184
+
185
+ return Array.from(numbers).toSorted((left, right) => left - right);
186
+ };
187
+
188
+ function classifyIssueSnapshot(opts: {
189
+ eligible: boolean;
190
+ linkedPullRequestNumbers: readonly number[];
191
+ linkedPullRequests: readonly ReviewTriageSnapshot[];
192
+ }): IssueSnapshotClassification {
193
+ const reasons = [
194
+ ...(!opts.eligible ? ["not_owned_by_automation_owner"] : []),
195
+ ...(opts.eligible && opts.linkedPullRequestNumbers.length === 0
196
+ ? ["no_linked_pull_request"]
197
+ : []),
198
+ ...opts.linkedPullRequests.flatMap((pr) =>
199
+ pr.classification.status === "needs_investigation"
200
+ ? pr.classification.reasons.map((reason) => `linked_pr_${pr.info.number}_${reason}`)
201
+ : [],
202
+ ),
203
+ ];
204
+ return { status: reasons.length > 0 ? "needs_investigation" : "clear", reasons };
205
+ }
206
+
207
+ export const fetchIssueSnapshot = Effect.fn("issue.fetchIssueSnapshot")(function* (opts: {
208
+ issue: number;
209
+ owner: string | null;
210
+ }) {
211
+ const triage = yield* fetchIssueTriage({ issue: opts.issue, verbosity: "full" });
212
+ if (!("comments" in triage)) {
213
+ throw new Error("Expected full issue triage");
214
+ }
215
+
216
+ const owner = opts.owner?.toLowerCase() ?? null;
217
+ const eligible =
218
+ owner !== null &&
219
+ triage.issue.assignees.length === 1 &&
220
+ triage.issue.assignees[0]?.toLowerCase() === owner;
221
+ const linkedPullRequestNumbers = collectLinkedPullRequestNumbers(triage.body, triage.comments);
222
+ const linkedPullRequests = eligible
223
+ ? yield* Effect.all(
224
+ linkedPullRequestNumbers.map((prNumber) => fetchReviewTriage(prNumber)),
225
+ { concurrency: "unbounded" },
226
+ )
227
+ : [];
228
+ const classification = classifyIssueSnapshot({
229
+ eligible,
230
+ linkedPullRequestNumbers,
231
+ linkedPullRequests,
232
+ });
233
+
234
+ const result: IssueSnapshot = {
235
+ issue: triage.issue,
236
+ body: triage.body,
237
+ commentsCount: triage.commentsCount,
238
+ comments: triage.comments,
239
+ eligible,
240
+ linkedPullRequestNumbers,
241
+ linkedPullRequests,
242
+ classification,
243
+ };
244
+ return result;
245
+ });
246
+
247
+ export const fetchIssueSnapshotBatch = Effect.fn("issue.fetchIssueSnapshotBatch")(function* (opts: {
248
+ issues: readonly number[];
249
+ owner: string | null;
250
+ }) {
251
+ return yield* Effect.all(
252
+ opts.issues.map((issue) => fetchIssueSnapshot({ issue, owner: opts.owner })),
253
+ { concurrency: "unbounded" },
254
+ );
255
+ });
256
+
132
257
  export const issueTriageCommand = Command.make(
133
258
  "triage",
134
259
  {
@@ -151,3 +276,31 @@ export const issueTriageCommand = Command.make(
151
276
  ).pipe(
152
277
  Command.withDescription("Composite: fetch issue details and discussion comments in one call"),
153
278
  );
279
+
280
+ export const issueSnapshotBatchCommand = Command.make(
281
+ "snapshot-batch",
282
+ {
283
+ format: formatOption,
284
+ issues: Flag.string("issues").pipe(Flag.withDescription("Comma-separated issue numbers")),
285
+ owner: Flag.string("owner").pipe(
286
+ Flag.withDescription("Automation owner login used to mark eligible issues"),
287
+ Flag.optional,
288
+ ),
289
+ repo: repoOption,
290
+ },
291
+ ({ format, issues, owner, repo }) =>
292
+ withRepo(
293
+ repo,
294
+ Effect.gen(function* () {
295
+ const result = yield* fetchIssueSnapshotBatch({
296
+ issues: parseIssueNumbers(issues),
297
+ owner: Option.getOrNull(owner),
298
+ });
299
+ yield* logFormatted(result, format);
300
+ }),
301
+ ),
302
+ ).pipe(
303
+ Command.withDescription(
304
+ "Composite: fetch full issue triage plus linked PR review-triage for multiple issues",
305
+ ),
306
+ );
@@ -62,10 +62,15 @@ type ReviewTriageSummary = {
62
62
  readonly unresolvedReviewThreadsCount: number;
63
63
  };
64
64
 
65
+ type ReviewTriageClassification = {
66
+ readonly status: "clear" | "needs_investigation";
67
+ readonly reasons: readonly string[];
68
+ };
69
+
65
70
  export const classifyReviewTriage = (
66
71
  summary: ReviewTriageSummary,
67
72
  checks: readonly CheckResult[],
68
- ) => {
73
+ ): ReviewTriageClassification => {
69
74
  const reasons = [
70
75
  ...(checks.some((check) => check.bucket === "fail") ? ["failed_checks"] : []),
71
76
  ...(summary.visibleOpenReviewThreadsCount > 0 ? ["visible_open_review_threads"] : []),
@@ -75,6 +80,26 @@ export const classifyReviewTriage = (
75
80
  return { status: reasons.length > 0 ? "needs_investigation" : "clear", reasons };
76
81
  };
77
82
 
83
+ export const parsePrNumbers = (input: string): readonly number[] =>
84
+ input
85
+ .split(",")
86
+ .map((part) => Number.parseInt(part.trim(), 10))
87
+ .filter((number) => Number.isInteger(number) && number > 0);
88
+
89
+ export const fetchReviewTriage = Effect.fn("pr.fetchReviewTriage")(function* (
90
+ prNumber: number | null,
91
+ ) {
92
+ const [info, unresolvedThreads, visibleOpenThreads, summary, checks] = yield* Effect.all([
93
+ viewPR(prNumber),
94
+ fetchThreads(prNumber, true),
95
+ fetchThreads(prNumber, false, true),
96
+ fetchDiscussionSummary(prNumber),
97
+ fetchChecks(prNumber, false, false, 0),
98
+ ]);
99
+ const classification = classifyReviewTriage(summary, checks);
100
+ return { classification, info, unresolvedThreads, visibleOpenThreads, summary, checks };
101
+ });
102
+
78
103
  export const prViewCommand = Command.make(
79
104
  "view",
80
105
  {
@@ -682,18 +707,8 @@ export const prReviewTriageCommand = Command.make(
682
707
  repo,
683
708
  Effect.gen(function* () {
684
709
  const prNumber = Option.getOrNull(pr);
685
- const [info, unresolvedThreads, visibleOpenThreads, summary, checks] = yield* Effect.all([
686
- viewPR(prNumber),
687
- fetchThreads(prNumber, true),
688
- fetchThreads(prNumber, false, true),
689
- fetchDiscussionSummary(prNumber),
690
- fetchChecks(prNumber, false, false, 0),
691
- ]);
692
- const classification = classifyReviewTriage(summary, checks);
693
- yield* logFormatted(
694
- { classification, info, unresolvedThreads, visibleOpenThreads, summary, checks },
695
- format,
696
- );
710
+ const result = yield* fetchReviewTriage(prNumber);
711
+ yield* logFormatted(result, format);
697
712
  }),
698
713
  ),
699
714
  ).pipe(
@@ -702,6 +717,30 @@ export const prReviewTriageCommand = Command.make(
702
717
  ),
703
718
  );
704
719
 
720
+ export const prReviewTriageBatchCommand = Command.make(
721
+ "review-triage-batch",
722
+ {
723
+ format: formatOption,
724
+ prs: Flag.string("prs").pipe(Flag.withDescription("Comma-separated PR numbers")),
725
+ repo: repoOption,
726
+ },
727
+ ({ format, prs, repo }) =>
728
+ withRepo(
729
+ repo,
730
+ Effect.gen(function* () {
731
+ const results = yield* Effect.all(
732
+ parsePrNumbers(prs).map((prNumber) => fetchReviewTriage(prNumber)),
733
+ { concurrency: "unbounded" },
734
+ );
735
+ yield* logFormatted(results, format);
736
+ }),
737
+ ),
738
+ ).pipe(
739
+ Command.withDescription(
740
+ "Composite: fetch review-triage output for multiple PRs in one gh-tool invocation",
741
+ ),
742
+ );
743
+
705
744
  export const prReplyAndResolveCommand = Command.make(
706
745
  "reply-and-resolve",
707
746
  {
@@ -18,5 +18,6 @@ export {
18
18
  prSubmitReviewCommand,
19
19
  prThreadsCommand,
20
20
  prReviewTriageCommand,
21
+ prReviewTriageBatchCommand,
21
22
  prViewCommand,
22
23
  } from "./commands";