@blogic-cz/agent-tools 0.8.12 → 0.8.14

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.8.12",
3
+ "version": "0.8.14",
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",
@@ -114,6 +114,7 @@
114
114
  "format:check": "oxfmt --check",
115
115
  "lint": "oxlint -c ./.oxlintrc.json --deny-warnings",
116
116
  "lint:fix": "oxlint -c ./.oxlintrc.json --fix",
117
+ "update:skills": "bun run .agents/skills/update-packages/references/skills-update-local.ts",
117
118
  "test": "vitest run"
118
119
  },
119
120
  "dependencies": {
@@ -9,10 +9,11 @@ import { ConfigServiceLayer } from "#config";
9
9
  import {
10
10
  issueCloseCommand,
11
11
  issueCommentCommand,
12
+ issueCommentsCommand,
12
13
  issueEditCommand,
13
14
  issueListCommand,
14
15
  issueReopenCommand,
15
- issueTriageSummaryCommand,
16
+ issueTriageCommand,
16
17
  issueViewCommand,
17
18
  } from "./issue";
18
19
  import {
@@ -85,16 +86,17 @@ const prCommand = Command.make("pr", {}).pipe(
85
86
 
86
87
  const issueCommand = Command.make("issue", {}).pipe(
87
88
  Command.withDescription(
88
- "Issue operations (list, view, close, reopen, comment, edit, triage-summary)",
89
+ "Issue operations (list, view, comments, triage, close, reopen, comment, edit)",
89
90
  ),
90
91
  Command.withSubcommands([
91
92
  issueListCommand,
92
93
  issueViewCommand,
94
+ issueCommentsCommand,
95
+ issueTriageCommand,
93
96
  issueCloseCommand,
94
97
  issueReopenCommand,
95
98
  issueCommentCommand,
96
99
  issueEditCommand,
97
- issueTriageSummaryCommand,
98
100
  ]),
99
101
  );
100
102
 
@@ -147,18 +149,20 @@ WORKFLOW FOR AI AGENTS:
147
149
  5. Use 'pr checks' to monitor CI status
148
150
  6. Use 'pr merge' to merge (dry-run by default)
149
151
  7. Use 'issue list' to list open/closed issues
150
- 8. Use 'issue close --issue N --comment "reason"' to close issues
151
- 9. Use 'issue comment --issue N --body "text"' to comment on issues
152
- 10. Use 'repo info' to get repository metadata
153
- 11. Use 'workflow list' to list recent workflow runs
154
- 12. Use 'workflow view --run N' to inspect a specific run with jobs/steps
155
- 13. Use 'workflow logs --run N' to get logs (failed jobs by default)
156
- 14. Use 'workflow job-logs --run N --job "build-web-app"' to get clean parsed logs for a specific job
157
- 15. Use 'workflow annotations --run N' to list CI annotations (errors, warnings, notices)
158
- 16. Use 'workflow watch --run N' to watch until completion
159
- 17. Use 'release status' to inspect latest release + repository context
160
- 18. Use 'release create --tag vX.Y.Z --generate-notes' to publish a release
161
- 19. Use 'release edit/view/list/delete' to maintain existing releases`,
152
+ 8. Use 'issue triage --issue N --verbosity full' to inspect one issue in one call
153
+ 9. Use 'issue comments --issue N' to read issue discussion comments separately
154
+ 10. Use 'issue close --issue N --comment "reason"' to close issues
155
+ 11. Use 'issue comment --issue N --body "text"' to comment on issues
156
+ 12. Use 'repo info' to get repository metadata
157
+ 13. Use 'workflow list' to list recent workflow runs
158
+ 14. Use 'workflow view --run N' to inspect a specific run with jobs/steps
159
+ 15. Use 'workflow logs --run N' to get logs (failed jobs by default)
160
+ 16. Use 'workflow job-logs --run N --job "build-web-app"' to get clean parsed logs for a specific job
161
+ 17. Use 'workflow annotations --run N' to list CI annotations (errors, warnings, notices)
162
+ 18. Use 'workflow watch --run N' to watch until completion
163
+ 19. Use 'release status' to inspect latest release + repository context
164
+ 20. Use 'release create --tag vX.Y.Z --generate-notes' to publish a release
165
+ 21. Use 'release edit/view/list/delete' to maintain existing releases`,
162
166
  ),
163
167
  Command.withSubcommands([prCommand, issueCommand, repoCommand, workflowCommand, releaseCommand]),
164
168
  );
@@ -3,7 +3,15 @@ import { Effect, Option } from "effect";
3
3
 
4
4
  import { formatOption, logFormatted } from "#shared";
5
5
 
6
- import { closeIssue, commentOnIssue, editIssue, listIssues, reopenIssue, viewIssue } from "./core";
6
+ import {
7
+ closeIssue,
8
+ commentOnIssue,
9
+ editIssue,
10
+ fetchIssueComments,
11
+ listIssues,
12
+ reopenIssue,
13
+ viewIssue,
14
+ } from "./core";
7
15
 
8
16
  export const issueListCommand = Command.make(
9
17
  "list",
@@ -46,6 +54,36 @@ export const issueViewCommand = Command.make(
46
54
  }),
47
55
  ).pipe(Command.withDescription("View issue details"));
48
56
 
57
+ export const issueCommentsCommand = Command.make(
58
+ "comments",
59
+ {
60
+ author: Flag.string("author").pipe(
61
+ Flag.withDescription("Filter by author login substring"),
62
+ Flag.optional,
63
+ ),
64
+ bodyContains: Flag.string("body-contains").pipe(
65
+ Flag.withDescription("Filter comments by body substring"),
66
+ Flag.optional,
67
+ ),
68
+ format: formatOption,
69
+ issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number")),
70
+ since: Flag.string("since").pipe(
71
+ Flag.withDescription("ISO timestamp to filter comments created after"),
72
+ Flag.optional,
73
+ ),
74
+ },
75
+ ({ author, bodyContains, format, issue, since }) =>
76
+ Effect.gen(function* () {
77
+ const comments = yield* fetchIssueComments(
78
+ issue,
79
+ Option.getOrNull(since),
80
+ Option.getOrNull(author),
81
+ Option.getOrNull(bodyContains),
82
+ );
83
+ yield* logFormatted(comments, format);
84
+ }),
85
+ ).pipe(Command.withDescription("Fetch issue discussion comments"));
86
+
49
87
  export const issueCloseCommand = Command.make(
50
88
  "close",
51
89
  {
@@ -1,5 +1,7 @@
1
1
  import { Effect } from "effect";
2
2
 
3
+ import type { GitHubIssueCommentUrl, IssueComment, IssueCommentId, IsoTimestamp } from "#gh/types";
4
+
3
5
  import { GitHubCommandError } from "#gh/errors";
4
6
  import { GitHubService } from "#gh/service";
5
7
 
@@ -32,6 +34,60 @@ export type RawIssueComment = {
32
34
  html_url: string;
33
35
  };
34
36
 
37
+ const REST_PAGE_SIZE = 100;
38
+
39
+ const parseJson = <T>(
40
+ stdout: string,
41
+ command: string,
42
+ parseFailurePrefix: string,
43
+ ): Effect.Effect<T, GitHubCommandError> =>
44
+ Effect.try({
45
+ try: () => JSON.parse(stdout) as T,
46
+ catch: (error) =>
47
+ new GitHubCommandError({
48
+ command,
49
+ exitCode: 0,
50
+ stderr: `${parseFailurePrefix}: ${error instanceof Error ? error.message : String(error)}`,
51
+ message: `${parseFailurePrefix}: ${error instanceof Error ? error.message : String(error)}`,
52
+ }),
53
+ });
54
+
55
+ const fetchAllRestPages = Effect.fn("issue.fetchAllRestPages")(function* <T>(
56
+ endpoint: string,
57
+ command: string,
58
+ parseFailurePrefix: string,
59
+ ) {
60
+ const service = yield* GitHubService;
61
+
62
+ const results: T[] = [];
63
+ let page = 1;
64
+
65
+ while (true) {
66
+ const separator = endpoint.includes("?") ? "&" : "?";
67
+ const result = yield* service.runGh([
68
+ "api",
69
+ `${endpoint}${separator}per_page=${REST_PAGE_SIZE}&page=${page}`,
70
+ ]);
71
+
72
+ const rawPage = yield* parseJson<T[]>(result.stdout, command, parseFailurePrefix);
73
+ results.push(...rawPage);
74
+
75
+ if (rawPage.length < REST_PAGE_SIZE) {
76
+ return results;
77
+ }
78
+
79
+ page += 1;
80
+ }
81
+ });
82
+
83
+ const mapRawIssueComment = (comment: RawIssueComment): IssueComment => ({
84
+ id: comment.id as IssueCommentId,
85
+ author: comment.user.login,
86
+ body: comment.body,
87
+ createdAt: comment.created_at as IsoTimestamp,
88
+ url: comment.html_url as GitHubIssueCommentUrl,
89
+ });
90
+
35
91
  export const listIssues = Effect.fn("issue.listIssues")(function* (opts: {
36
92
  state: string;
37
93
  labels: string | null;
@@ -69,6 +125,41 @@ export const viewIssue = Effect.fn("issue.viewIssue")(function* (issueNumber: nu
69
125
  ]);
70
126
  });
71
127
 
128
+ export const fetchIssueComments = Effect.fn("issue.fetchIssueComments")(function* (
129
+ issueNumber: number,
130
+ since: string | null,
131
+ author: string | null,
132
+ bodyContains: string | null,
133
+ ) {
134
+ const gh = yield* GitHubService;
135
+ const repoInfo = yield* gh.getRepoInfo();
136
+
137
+ const raw = yield* fetchAllRestPages<RawIssueComment>(
138
+ `repos/${repoInfo.owner}/${repoInfo.name}/issues/${issueNumber}/comments`,
139
+ "gh-tool issue comments",
140
+ "Failed to parse response",
141
+ );
142
+
143
+ let comments = raw.map(mapRawIssueComment);
144
+
145
+ if (since !== null) {
146
+ const sinceMs = new Date(since).getTime();
147
+ comments = comments.filter((comment) => new Date(comment.createdAt).getTime() >= sinceMs);
148
+ }
149
+
150
+ if (author !== null) {
151
+ const authorFilter = author.toLowerCase();
152
+ comments = comments.filter((comment) => comment.author.toLowerCase().includes(authorFilter));
153
+ }
154
+
155
+ if (bodyContains !== null) {
156
+ const bodyFilter = bodyContains.toLowerCase();
157
+ comments = comments.filter((comment) => comment.body.toLowerCase().includes(bodyFilter));
158
+ }
159
+
160
+ return comments;
161
+ });
162
+
72
163
  export const closeIssue = Effect.fn("issue.closeIssue")(function* (opts: {
73
164
  issue: number;
74
165
  comment: string | null;
@@ -1,9 +1,10 @@
1
1
  export {
2
2
  issueCloseCommand,
3
3
  issueCommentCommand,
4
+ issueCommentsCommand,
4
5
  issueEditCommand,
5
6
  issueListCommand,
6
7
  issueReopenCommand,
7
8
  issueViewCommand,
8
9
  } from "./commands";
9
- export { issueTriageSummaryCommand } from "./triage";
10
+ export { issueTriageCommand } from "./triage";
@@ -1,12 +1,15 @@
1
1
  import { Command, Flag } from "effect/unstable/cli";
2
- import { Effect } from "effect";
2
+ import { Effect, Schema } from "effect";
3
+
4
+ import type { IssueComment } from "#gh/types";
3
5
 
4
6
  import { formatOption, logFormatted } from "#shared";
5
7
  import { GitHubService } from "#gh/service";
6
8
 
7
- // ---------------------------------------------------------------------------
8
- // Raw types (gh CLI JSON output)
9
- // ---------------------------------------------------------------------------
9
+ import { fetchIssueComments } from "./core";
10
+
11
+ const TriageVerbosity = Schema.Literals(["compact", "full"]);
12
+ type TriageVerbosity = typeof TriageVerbosity.Type;
10
13
 
11
14
  type RawTriageIssue = {
12
15
  number: number;
@@ -17,304 +20,119 @@ type RawTriageIssue = {
17
20
  assignees: Array<{ login: string }>;
18
21
  author: { login: string };
19
22
  body: string;
20
- comments: Array<unknown>;
21
23
  createdAt: string;
24
+ closedAt: string | null;
22
25
  };
23
26
 
24
- type RawTriagePR = {
27
+ type TriageIssue = {
25
28
  number: number;
26
29
  title: string;
27
30
  state: string;
28
31
  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
32
  labels: string[];
62
- classification: IssueClassification;
63
- confidence: Confidence;
64
- body: string;
65
- commentsCount: number;
33
+ assignees: string[];
34
+ author: string;
66
35
  createdAt: string;
67
- url: string;
36
+ closedAt: string | null;
68
37
  };
69
38
 
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;
39
+ type CompactIssueTriage = {
40
+ issue: TriageIssue;
83
41
  body: string;
84
- url: string;
42
+ commentsCount: number;
43
+ latestComment: IssueComment | null;
85
44
  };
86
45
 
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
- };
46
+ type FullIssueTriage = {
47
+ issue: TriageIssue;
48
+ body: string;
49
+ commentsCount: number;
50
+ comments: IssueComment[];
98
51
  };
99
52
 
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
53
  function truncateBody(body: string, maxLength = 500): string {
183
54
  if (body.length <= maxLength) return body;
184
55
  return body.slice(0, maxLength) + "…";
185
56
  }
186
57
 
187
- // ---------------------------------------------------------------------------
188
- // Handler
189
- // ---------------------------------------------------------------------------
58
+ function getLatestComment(comments: IssueComment[]): IssueComment | null {
59
+ if (comments.length === 0) {
60
+ return null;
61
+ }
190
62
 
191
- const fetchTriageSummary = Effect.fn("issue.fetchTriageSummary")(function* (opts: {
192
- state: string;
193
- limit: number;
63
+ return comments.reduce((current, next) =>
64
+ new Date(next.createdAt).getTime() > new Date(current.createdAt).getTime() ? next : current,
65
+ );
66
+ }
67
+
68
+ export const fetchIssueTriage = Effect.fn("issue.fetchIssueTriage")(function* (opts: {
69
+ issue: number;
70
+ verbosity: TriageVerbosity;
194
71
  }) {
195
72
  const gh = yield* GitHubService;
196
- const repoInfo = yield* gh.getRepoInfo();
197
73
 
198
- // Parallel fetch: issues + PRs
199
- const [rawIssues, rawPRs] = yield* Effect.all(
74
+ const [rawIssue, comments] = yield* Effect.all(
200
75
  [
201
- gh.runGhJson<RawTriageIssue[]>([
76
+ gh.runGhJson<RawTriageIssue>([
202
77
  "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),
78
+ "view",
79
+ String(opts.issue),
218
80
  "--json",
219
- "number,title,state,url,labels,author,body,headRefName,baseRefName,isDraft,mergeable,reviewDecision,statusCheckRollup",
81
+ "number,title,state,url,labels,assignees,author,body,createdAt,closedAt",
220
82
  ]),
83
+ fetchIssueComments(opts.issue, null, null, null),
221
84
  ],
222
85
  { concurrency: "unbounded" },
223
86
  );
224
87
 
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);
88
+ const issue: TriageIssue = {
89
+ number: rawIssue.number,
90
+ title: rawIssue.title,
91
+ state: rawIssue.state,
92
+ url: rawIssue.url,
93
+ labels: rawIssue.labels.map((label) => label.name),
94
+ assignees: rawIssue.assignees.map((assignee) => assignee.login),
95
+ author: rawIssue.author.login,
96
+ createdAt: rawIssue.createdAt,
97
+ closedAt: rawIssue.closedAt,
98
+ };
248
99
 
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,
100
+ if (opts.verbosity === "full") {
101
+ const result: FullIssueTriage = {
102
+ issue,
103
+ body: rawIssue.body,
104
+ commentsCount: comments.length,
105
+ comments,
264
106
  };
265
- });
266
107
 
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;
108
+ return result;
271
109
  }
272
110
 
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
- },
111
+ const result: CompactIssueTriage = {
112
+ issue,
113
+ body: truncateBody(rawIssue.body),
114
+ commentsCount: comments.length,
115
+ latestComment: getLatestComment(comments),
289
116
  };
290
117
 
291
118
  return result;
292
119
  });
293
120
 
294
- // ---------------------------------------------------------------------------
295
- // CLI Command
296
- // ---------------------------------------------------------------------------
297
-
298
- export const issueTriageSummaryCommand = Command.make(
299
- "triage-summary",
121
+ export const issueTriageCommand = Command.make(
122
+ "triage",
300
123
  {
301
124
  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"),
125
+ issue: Flag.integer("issue").pipe(Flag.withDescription("Issue number")),
126
+ verbosity: Flag.choice("verbosity", ["compact", "full"] as const).pipe(
127
+ Flag.withDescription("Output detail level: compact or full"),
128
+ Flag.withDefault("compact"),
309
129
  ),
310
130
  },
311
- ({ format, limit, state }) =>
131
+ ({ format, issue, verbosity }) =>
312
132
  Effect.gen(function* () {
313
- const summary = yield* fetchTriageSummary({ limit, state });
314
- yield* logFormatted(summary, format);
133
+ const result = yield* fetchIssueTriage({ issue, verbosity });
134
+ yield* logFormatted(result, format);
315
135
  }),
316
136
  ).pipe(
317
- Command.withDescription(
318
- "Composite: fetch all issues + PRs, classify each, return structured triage summary",
319
- ),
137
+ Command.withDescription("Composite: fetch issue details and discussion comments in one call"),
320
138
  );
@@ -128,7 +128,7 @@ export class LogsService extends ServiceMap.Service<
128
128
 
129
129
  const podResult = yield* k8s
130
130
  .runKubectl(
131
- `-n $(kubectl config view --minify -o jsonpath='{.contexts[0].context.namespace}' 2>/dev/null || echo default) get pods -o jsonpath='{.items[0].metadata.name}'`,
131
+ `get pods --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}'`,
132
132
  false,
133
133
  )
134
134
  .pipe(
@@ -228,7 +228,10 @@ export class LogsService extends ServiceMap.Service<
228
228
  const remotePath = logsConfig.remotePath;
229
229
 
230
230
  const podResult = yield* k8s
231
- .runKubectl(`get pods -o jsonpath='{.items[0].metadata.name}'`, false)
231
+ .runKubectl(
232
+ `get pods --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}'`,
233
+ false,
234
+ )
232
235
  .pipe(
233
236
  Effect.mapError(
234
237
  (error) =>