@blogic-cz/agent-tools 0.14.34 → 0.14.36

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.34",
3
+ "version": "0.14.36",
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";
@@ -91,13 +92,14 @@ const prCommand = Command.make("pr", {}).pipe(
91
92
 
92
93
  const issueCommand = Command.make("issue", {}).pipe(
93
94
  Command.withDescription(
94
- "Issue operations (list, view, comments, triage, close, reopen, comment, edit)",
95
+ "Issue operations (list, view, comments, triage, snapshot-batch, close, reopen, comment, edit)",
95
96
  ),
96
97
  Command.withSubcommands([
97
98
  issueListCommand,
98
99
  issueViewCommand,
99
100
  issueCommentsCommand,
100
101
  issueTriageCommand,
102
+ issueSnapshotBatchCommand,
101
103
  issueCloseCommand,
102
104
  issueReopenCommand,
103
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
+ );
@@ -244,7 +244,10 @@ export const viewPR = Effect.fn("pr.viewPR")(function* (prNumber: number | null)
244
244
  if (prNumber !== null) {
245
245
  args.push(String(prNumber));
246
246
  }
247
- args.push("--json", "number,url,title,headRefName,baseRefName,state,isDraft,mergeable,body");
247
+ args.push(
248
+ "--json",
249
+ "number,url,title,headRefName,baseRefName,state,isDraft,mergeable,body,reviewDecision,reviewRequests",
250
+ );
248
251
 
249
252
  const info = yield* gh.runGhJson<PRViewInfo>(args);
250
253
  return info;
@@ -13,8 +13,14 @@ export type PRInfo = {
13
13
  mergeable: "MERGEABLE" | "CONFLICTING" | "UNKNOWN";
14
14
  };
15
15
 
16
+ export type ReviewRequest =
17
+ | { __typename: "User"; login: string }
18
+ | { __typename: "Team"; name: string; slug: string };
19
+
16
20
  export type PRViewInfo = PRInfo & {
17
21
  body: string;
22
+ reviewDecision: "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED" | "";
23
+ reviewRequests: ReviewRequest[];
18
24
  };
19
25
 
20
26
  export type ReviewThread = {