@blogic-cz/agent-tools 0.1.0 → 0.2.1

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.
Files changed (42) hide show
  1. package/README.md +68 -30
  2. package/package.json +7 -4
  3. package/schemas/agent-tools.schema.json +4 -0
  4. package/src/az-tool/build.ts +5 -0
  5. package/src/az-tool/errors.ts +12 -0
  6. package/src/az-tool/index.ts +129 -105
  7. package/src/az-tool/service.ts +13 -4
  8. package/src/config/index.ts +7 -1
  9. package/src/config/loader.ts +8 -0
  10. package/src/config/types.ts +2 -0
  11. package/src/credential-guard/index.ts +2 -1
  12. package/src/db-tool/config-service.ts +2 -2
  13. package/src/db-tool/errors.ts +15 -0
  14. package/src/db-tool/index.ts +47 -8
  15. package/src/db-tool/types.ts +1 -1
  16. package/src/gh-tool/errors.ts +15 -0
  17. package/src/gh-tool/index.ts +5 -1
  18. package/src/gh-tool/issue.ts +1 -1
  19. package/src/gh-tool/pr/commands.ts +58 -3
  20. package/src/gh-tool/pr/core.ts +28 -7
  21. package/src/gh-tool/pr/helpers.ts +1 -1
  22. package/src/gh-tool/pr/index.ts +2 -0
  23. package/src/gh-tool/pr/review.ts +10 -6
  24. package/src/gh-tool/repo.ts +1 -1
  25. package/src/gh-tool/service.ts +5 -0
  26. package/src/gh-tool/workflow.ts +5 -1
  27. package/src/k8s-tool/errors.ts +9 -0
  28. package/src/k8s-tool/index.ts +318 -66
  29. package/src/k8s-tool/service.ts +2 -2
  30. package/src/k8s-tool/types.ts +4 -0
  31. package/src/logs-tool/errors.ts +12 -0
  32. package/src/logs-tool/index.ts +73 -11
  33. package/src/logs-tool/service.ts +4 -4
  34. package/src/logs-tool/types.ts +4 -1
  35. package/src/session-tool/config.ts +1 -1
  36. package/src/session-tool/index.ts +1 -1
  37. package/src/session-tool/service.ts +16 -3
  38. package/src/session-tool/types.ts +1 -1
  39. package/src/shared/bun.ts +1 -1
  40. package/src/shared/error-renderer.ts +21 -11
  41. package/src/shared/index.ts +1 -0
  42. package/src/shared/types.ts +3 -0
@@ -65,6 +65,7 @@ const AgentToolsConfigSchema = Schema.Struct({
65
65
  }),
66
66
  ),
67
67
  credentialGuard: Schema.optionalKey(CredentialGuardConfigSchema),
68
+ defaultEnvironment: Schema.optionalKey(Schema.String),
68
69
  });
69
70
 
70
71
  async function findConfigFile(startDirectory: string = process.cwd()): Promise<string | undefined> {
@@ -72,11 +73,13 @@ async function findConfigFile(startDirectory: string = process.cwd()): Promise<s
72
73
 
73
74
  while (true) {
74
75
  const json5Path = `${currentDirectory}/agent-tools.json5`;
76
+ // eslint-disable-next-line eslint/no-await-in-loop -- sequential directory walk, each iteration may short-circuit
75
77
  if (await Bun.file(json5Path).exists()) {
76
78
  return json5Path;
77
79
  }
78
80
 
79
81
  const jsonPath = `${currentDirectory}/agent-tools.json`;
82
+ // eslint-disable-next-line eslint/no-await-in-loop -- sequential directory walk, each iteration may short-circuit
80
83
  if (await Bun.file(jsonPath).exists()) {
81
84
  return jsonPath;
82
85
  }
@@ -106,6 +109,7 @@ export async function loadConfig(): Promise<AgentToolsConfig | undefined> {
106
109
  `Invalid agent-tools config at ${configPath}: ${
107
110
  error instanceof Error ? error.message : String(error)
108
111
  }`,
112
+ { cause: error },
109
113
  );
110
114
  }
111
115
  }
@@ -168,3 +172,7 @@ export function getToolConfig<T>(
168
172
  `Multiple ${section} profiles found: [${keys.join(", ")}]. Use --profile <name> to select one.`,
169
173
  );
170
174
  }
175
+
176
+ export function getDefaultEnvironment(config: AgentToolsConfig | undefined): string | undefined {
177
+ return config?.defaultEnvironment;
178
+ }
@@ -79,4 +79,6 @@ export type AgentToolsConfig = {
79
79
  };
80
80
  /** Global credential guard config (merged with built-in defaults, not per-profile) */
81
81
  credentialGuard?: CredentialGuardConfig;
82
+ /** Optional default environment name (local|test|prod) used by tools when no --env flag is provided */
83
+ defaultEnvironment?: string;
82
84
  };
@@ -14,7 +14,7 @@
14
14
  * at infrastructure level (K8s RBAC, file permissions, etc.)
15
15
  */
16
16
 
17
- import type { CliToolOverride, CredentialGuardConfig } from "../config/types.ts";
17
+ import type { CliToolOverride, CredentialGuardConfig } from "#src/config/types.ts";
18
18
 
19
19
  // ============================================================================
20
20
  // TYPES
@@ -108,6 +108,7 @@ const SECRET_PATTERNS = [
108
108
  /(?:secret|token|password|passwd|pwd)[" \t:=]+["']?(?!\$\{|process\.env|z\.|generate|create|read|get|fetch|import|export|const|function|return|Schema)[^\s"']{32,}["']?/i,
109
109
  },
110
110
  {
111
+ // eslint-disable-next-line eslint/no-useless-concat -- intentionally split to avoid credential guard self-detection
111
112
  name: "Priv" + "ate Key",
112
113
  pattern: new RegExp("-----BEGIN.*PRIVATE KEY-----"),
113
114
  },
@@ -1,7 +1,7 @@
1
1
  import { Effect, Layer, ServiceMap } from "effect";
2
2
 
3
- import { ConfigService, getToolConfig } from "../config";
4
- import type { DatabaseConfig } from "../config";
3
+ import { ConfigService, getToolConfig } from "#src/config";
4
+ import type { DatabaseConfig } from "#src/config";
5
5
 
6
6
  /**
7
7
  * DbConfigService wraps the resolved DatabaseConfig for the selected profile.
@@ -5,6 +5,9 @@ export class DbConnectionError extends Schema.TaggedErrorClass<DbConnectionError
5
5
  {
6
6
  message: Schema.String,
7
7
  environment: Schema.String,
8
+ hint: Schema.optionalKey(Schema.String),
9
+ nextCommand: Schema.optionalKey(Schema.String),
10
+ retryable: Schema.optionalKey(Schema.Boolean),
8
11
  },
9
12
  ) {}
10
13
 
@@ -12,16 +15,25 @@ export class DbQueryError extends Schema.TaggedErrorClass<DbQueryError>()("DbQue
12
15
  message: Schema.String,
13
16
  sql: Schema.String,
14
17
  stderr: Schema.optionalKey(Schema.String),
18
+ hint: Schema.optionalKey(Schema.String),
19
+ nextCommand: Schema.optionalKey(Schema.String),
20
+ retryable: Schema.optionalKey(Schema.Boolean),
15
21
  }) {}
16
22
 
17
23
  export class DbTunnelError extends Schema.TaggedErrorClass<DbTunnelError>()("DbTunnelError", {
18
24
  message: Schema.String,
19
25
  port: Schema.Number,
26
+ hint: Schema.optionalKey(Schema.String),
27
+ nextCommand: Schema.optionalKey(Schema.String),
28
+ retryable: Schema.optionalKey(Schema.Boolean),
20
29
  }) {}
21
30
 
22
31
  export class DbParseError extends Schema.TaggedErrorClass<DbParseError>()("DbParseError", {
23
32
  message: Schema.String,
24
33
  rawOutput: Schema.String,
34
+ hint: Schema.optionalKey(Schema.String),
35
+ nextCommand: Schema.optionalKey(Schema.String),
36
+ retryable: Schema.optionalKey(Schema.Boolean),
25
37
  }) {}
26
38
 
27
39
  export class DbMutationBlockedError extends Schema.TaggedErrorClass<DbMutationBlockedError>()(
@@ -29,6 +41,9 @@ export class DbMutationBlockedError extends Schema.TaggedErrorClass<DbMutationBl
29
41
  {
30
42
  message: Schema.String,
31
43
  environment: Schema.String,
44
+ hint: Schema.optionalKey(Schema.String),
45
+ nextCommand: Schema.optionalKey(Schema.String),
46
+ retryable: Schema.optionalKey(Schema.Boolean),
32
47
  },
33
48
  ) {}
34
49
 
@@ -5,9 +5,10 @@ import { Console, Effect, Layer, Option } from "effect";
5
5
 
6
6
  import type { SchemaMode } from "./types";
7
7
 
8
- import { formatOption, formatOutput, renderCauseToStderr, VERSION } from "../shared";
9
- import { ConfigServiceLayer } from "../config";
8
+ import { formatOption, formatOutput, renderCauseToStderr, VERSION } from "#src/shared";
9
+ import { ConfigService, ConfigServiceLayer, getDefaultEnvironment } from "#src/config";
10
10
  import { makeDbConfigLayer } from "./config-service";
11
+ import { DbConnectionError } from "./errors";
11
12
  import { DbService } from "./service";
12
13
 
13
14
  // Extract --profile from argv before @effect/cli parsing
@@ -15,11 +16,45 @@ import { DbService } from "./service";
15
16
  const profileIndex = process.argv.indexOf("--profile");
16
17
  const profileArg = profileIndex !== -1 ? process.argv[profileIndex + 1] : undefined;
17
18
 
19
+ /**
20
+ * Resolve environment from explicit --env flag, config defaultEnvironment, or fail with hint.
21
+ */
22
+ const resolveEnv = (envOption: Option.Option<string>) =>
23
+ Effect.gen(function* () {
24
+ const explicit = Option.getOrUndefined(envOption);
25
+ if (explicit) return explicit;
26
+
27
+ const config = yield* ConfigService;
28
+ const defaultEnv = getDefaultEnvironment(config);
29
+
30
+ if (defaultEnv === "prod") {
31
+ return yield* new DbConnectionError({
32
+ message:
33
+ "Implicit prod access blocked. Config defaultEnvironment is 'prod' but --env was not passed explicitly.",
34
+ environment: "(prod-safety)",
35
+ hint: "Pass --env prod explicitly to confirm production access, or change defaultEnvironment to a non-prod value.",
36
+ nextCommand: 'agent-tools-db sql --env prod --sql "SELECT 1"',
37
+ });
38
+ }
39
+
40
+ if (defaultEnv) return defaultEnv;
41
+
42
+ return yield* new DbConnectionError({
43
+ message:
44
+ "No environment specified. Use --env <name> or set defaultEnvironment in agent-tools.json5.",
45
+ environment: "(not specified)",
46
+ hint: 'Set defaultEnvironment in agent-tools.json5 (e.g. defaultEnvironment: "local") or pass --env explicitly.',
47
+ nextCommand: 'agent-tools-db sql --env local --sql "SELECT 1"',
48
+ });
49
+ });
50
+
18
51
  const sqlCommand = Command.make(
19
52
  "sql",
20
53
  {
21
- env: Flag.string("env").pipe(
22
- Flag.withDescription("Target database environment name (e.g. local, test, prod)"),
54
+ env: Flag.optional(Flag.string("env")).pipe(
55
+ Flag.withDescription(
56
+ "Target database environment name (e.g. local, test, prod). Falls back to defaultEnvironment in config.",
57
+ ),
23
58
  ),
24
59
  sql: Flag.string("sql").pipe(Flag.withDescription("SQL query to execute")),
25
60
  format: formatOption,
@@ -29,8 +64,9 @@ const sqlCommand = Command.make(
29
64
  },
30
65
  ({ env, sql, format }) =>
31
66
  Effect.gen(function* () {
67
+ const resolvedEnv = yield* resolveEnv(env);
32
68
  const db = yield* DbService;
33
- const result = yield* db.executeQuery(env, sql);
69
+ const result = yield* db.executeQuery(resolvedEnv, sql);
34
70
  yield* Console.log(formatOutput(result, format));
35
71
  }),
36
72
  ).pipe(Command.withDescription("Execute a SQL query"));
@@ -38,8 +74,10 @@ const sqlCommand = Command.make(
38
74
  const schemaCommand = Command.make(
39
75
  "schema",
40
76
  {
41
- env: Flag.string("env").pipe(
42
- Flag.withDescription("Target database environment name (e.g. local, test, prod)"),
77
+ env: Flag.optional(Flag.string("env")).pipe(
78
+ Flag.withDescription(
79
+ "Target database environment name (e.g. local, test, prod). Falls back to defaultEnvironment in config.",
80
+ ),
43
81
  ),
44
82
  mode: Flag.choice("mode", ["tables", "columns", "full", "relationships"]).pipe(
45
83
  Flag.withDescription(
@@ -57,9 +95,10 @@ const schemaCommand = Command.make(
57
95
  },
58
96
  ({ env, mode, table, format }) =>
59
97
  Effect.gen(function* () {
98
+ const resolvedEnv = yield* resolveEnv(env);
60
99
  const db = yield* DbService;
61
100
  const result = yield* db.executeSchemaQuery(
62
- env,
101
+ resolvedEnv,
63
102
  mode as SchemaMode,
64
103
  Option.getOrUndefined(table),
65
104
  );
@@ -1,4 +1,4 @@
1
- import type { Environment, OutputFormat } from "../shared";
1
+ import type { Environment, OutputFormat } from "#src/shared";
2
2
  export type { Environment, OutputFormat };
3
3
 
4
4
  export type SchemaMode = "tables" | "columns" | "full" | "relationships";
@@ -7,6 +7,9 @@ export class GitHubCommandError extends Schema.TaggedErrorClass<GitHubCommandErr
7
7
  command: Schema.String,
8
8
  exitCode: Schema.Number,
9
9
  stderr: Schema.String,
10
+ hint: Schema.optionalKey(Schema.String),
11
+ nextCommand: Schema.optionalKey(Schema.String),
12
+ retryable: Schema.optionalKey(Schema.Boolean),
10
13
  },
11
14
  ) {}
12
15
 
@@ -16,11 +19,17 @@ export class GitHubNotFoundError extends Schema.TaggedErrorClass<GitHubNotFoundE
16
19
  message: Schema.String,
17
20
  identifier: Schema.String,
18
21
  resource: Schema.String,
22
+ hint: Schema.optionalKey(Schema.String),
23
+ nextCommand: Schema.optionalKey(Schema.String),
24
+ retryable: Schema.optionalKey(Schema.Boolean),
19
25
  },
20
26
  ) {}
21
27
 
22
28
  export class GitHubAuthError extends Schema.TaggedErrorClass<GitHubAuthError>()("GitHubAuthError", {
23
29
  message: Schema.String,
30
+ hint: Schema.optionalKey(Schema.String),
31
+ nextCommand: Schema.optionalKey(Schema.String),
32
+ retryable: Schema.optionalKey(Schema.Boolean),
24
33
  }) {}
25
34
 
26
35
  export class GitHubMergeError extends Schema.TaggedErrorClass<GitHubMergeError>()(
@@ -28,6 +37,9 @@ export class GitHubMergeError extends Schema.TaggedErrorClass<GitHubMergeError>(
28
37
  {
29
38
  message: Schema.String,
30
39
  reason: Schema.Literals(["conflicts", "checks_failing", "branch_protected", "unknown"]),
40
+ hint: Schema.optionalKey(Schema.String),
41
+ nextCommand: Schema.optionalKey(Schema.String),
42
+ retryable: Schema.optionalKey(Schema.Boolean),
31
43
  },
32
44
  ) {}
33
45
 
@@ -36,6 +48,9 @@ export class GitHubTimeoutError extends Schema.TaggedErrorClass<GitHubTimeoutErr
36
48
  {
37
49
  message: Schema.String,
38
50
  timeoutMs: Schema.Number,
51
+ hint: Schema.optionalKey(Schema.String),
52
+ nextCommand: Schema.optionalKey(Schema.String),
53
+ retryable: Schema.optionalKey(Schema.Boolean),
39
54
  },
40
55
  ) {}
41
56
 
@@ -3,7 +3,7 @@ import { Command } from "effect/unstable/cli";
3
3
  import { BunRuntime, BunServices } from "@effect/platform-bun";
4
4
  import { Effect, Layer } from "effect";
5
5
 
6
- import { renderCauseToStderr, VERSION } from "../shared";
6
+ import { renderCauseToStderr, VERSION } from "#src/shared";
7
7
  import {
8
8
  issueListCommand,
9
9
  issueViewCommand,
@@ -30,6 +30,8 @@ import {
30
30
  prChecksCommand,
31
31
  prChecksFailedCommand,
32
32
  prRerunChecksCommand,
33
+ prReplyAndResolveCommand,
34
+ prReviewTriageCommand,
33
35
  } from "./pr/index";
34
36
  import { repoInfoCommand, repoListCommand, repoSearchCodeCommand } from "./repo";
35
37
  import { GitHubService } from "./service";
@@ -64,6 +66,8 @@ const prCommand = Command.make("pr", {}).pipe(
64
66
  prChecksCommand,
65
67
  prChecksFailedCommand,
66
68
  prRerunChecksCommand,
69
+ prReplyAndResolveCommand,
70
+ prReviewTriageCommand,
67
71
  ]),
68
72
  );
69
73
 
@@ -1,7 +1,7 @@
1
1
  import { Command, Flag } from "effect/unstable/cli";
2
2
  import { Effect, Option } from "effect";
3
3
 
4
- import { formatOption, logFormatted } from "../shared";
4
+ import { formatOption, logFormatted } from "#src/shared";
5
5
  import { GitHubCommandError } from "./errors";
6
6
  import { GitHubService } from "./service";
7
7
 
@@ -1,15 +1,15 @@
1
1
  import { Command, Flag } from "effect/unstable/cli";
2
2
  import { Effect, Option } from "effect";
3
3
 
4
- import type { PRStatusResult } from "../types";
4
+ import type { PRStatusResult } from "#src/gh-tool/types";
5
5
 
6
- import { formatOption, logFormatted } from "../../shared";
6
+ import { formatOption, logFormatted } from "#src/shared";
7
7
  import {
8
8
  CI_CHECK_WATCH_TIMEOUT_MS,
9
9
  DEFAULT_DELETE_BRANCH,
10
10
  DEFAULT_MERGE_STRATEGY,
11
11
  MERGE_STRATEGIES,
12
- } from "../config";
12
+ } from "#src/gh-tool/config";
13
13
 
14
14
  import {
15
15
  createPR,
@@ -430,3 +430,58 @@ export const prSubmitReviewCommand = Command.make(
430
430
  "Submit a pending review as COMMENT (auto-detects your pending review if --review-id is omitted)",
431
431
  ),
432
432
  );
433
+
434
+ export const prReviewTriageCommand = Command.make(
435
+ "review-triage",
436
+ {
437
+ format: formatOption,
438
+ pr: Flag.integer("pr").pipe(
439
+ Flag.withDescription("PR number (default: current branch PR)"),
440
+ Flag.optional,
441
+ ),
442
+ },
443
+ ({ format, pr }) =>
444
+ Effect.gen(function* () {
445
+ const prNumber = Option.getOrNull(pr);
446
+ const [info, threads, summary, checks] = yield* Effect.all([
447
+ viewPR(prNumber),
448
+ fetchThreads(prNumber, true),
449
+ fetchDiscussionSummary(prNumber),
450
+ fetchChecks(prNumber, false, false, 0),
451
+ ]);
452
+ yield* logFormatted({ info, unresolvedThreads: threads, summary, checks }, format);
453
+ }),
454
+ ).pipe(
455
+ Command.withDescription(
456
+ "Composite: PR info + unresolved threads + discussion summary + checks status in one call",
457
+ ),
458
+ );
459
+
460
+ export const prReplyAndResolveCommand = Command.make(
461
+ "reply-and-resolve",
462
+ {
463
+ body: Flag.string("body").pipe(Flag.withDescription("Reply body text")),
464
+ commentId: Flag.integer("comment-id").pipe(
465
+ Flag.withDescription("ID of the comment to reply to"),
466
+ ),
467
+ format: formatOption,
468
+ pr: Flag.integer("pr").pipe(
469
+ Flag.withDescription("PR number (default: current branch PR)"),
470
+ Flag.optional,
471
+ ),
472
+ threadId: Flag.string("thread-id").pipe(
473
+ Flag.withDescription("GraphQL node ID of the thread to resolve"),
474
+ ),
475
+ },
476
+ ({ body, commentId, format, pr, threadId }) =>
477
+ Effect.gen(function* () {
478
+ const prNumber = Option.getOrNull(pr);
479
+ const replyResult = yield* replyToComment(prNumber, commentId, body);
480
+ const resolveResult = yield* resolveThread(threadId);
481
+ yield* logFormatted({ reply: replyResult, resolve: resolveResult }, format);
482
+ }),
483
+ ).pipe(
484
+ Command.withDescription(
485
+ "Composite: reply to a review comment and resolve its thread in one call",
486
+ ),
487
+ );
@@ -1,9 +1,15 @@
1
1
  import { Console, Effect, Option } from "effect";
2
2
 
3
- import type { BranchPRDetail, CheckResult, MergeResult, MergeStrategy, PRInfo } from "../types";
3
+ import type {
4
+ BranchPRDetail,
5
+ CheckResult,
6
+ MergeResult,
7
+ MergeStrategy,
8
+ PRInfo,
9
+ } from "#src/gh-tool/types";
4
10
 
5
- import { GitHubCommandError, GitHubMergeError, GitHubTimeoutError } from "../errors";
6
- import { GitHubService } from "../service";
11
+ import { GitHubCommandError, GitHubMergeError, GitHubTimeoutError } from "#src/gh-tool/errors";
12
+ import { GitHubService } from "#src/gh-tool/service";
7
13
 
8
14
  import type { ButStatusJson, PRViewJsonResult } from "./helpers";
9
15
  import { runLocalCommand } from "./helpers";
@@ -155,7 +161,7 @@ export const detectPRStatus = Effect.fn("pr.detectPRStatus")(function* () {
155
161
  { concurrency: "unbounded" },
156
162
  );
157
163
 
158
- const foundPrs = branchResults.filter((r) => r.openPr !== null).map((r) => r.openPr!);
164
+ const foundPrs = branchResults.flatMap((r) => (r.openPr === null ? [] : [r.openPr]));
159
165
 
160
166
  if (foundPrs.length === 0) {
161
167
  const branchDetails: BranchPRDetail[] = branchResults.map((r) => ({
@@ -172,7 +178,7 @@ export const detectPRStatus = Effect.fn("pr.detectPRStatus")(function* () {
172
178
  if (foundPrs.length === 1) {
173
179
  return {
174
180
  mode: "single" as const,
175
- pr: foundPrs[0]!,
181
+ pr: foundPrs[0] as PRInfo,
176
182
  };
177
183
  }
178
184
 
@@ -205,7 +211,11 @@ export const createPR = Effect.fn("pr.createPR")(function* (opts: {
205
211
  "--limit",
206
212
  "1",
207
213
  ])
208
- .pipe(Effect.map((prs) => (prs.length > 0 ? Option.some(prs[0]!) : Option.none())))
214
+ .pipe(
215
+ Effect.map((prs) =>
216
+ prs.length > 0 ? Option.some(prs[0] as PRInfo) : Option.none<PRInfo>(),
217
+ ),
218
+ )
209
219
  : gh
210
220
  .runGhJson<{ number: number; url: string }>(["pr", "view", "--json", "number,url"])
211
221
  .pipe(Effect.option);
@@ -258,7 +268,7 @@ export const createPR = Effect.fn("pr.createPR")(function* (opts: {
258
268
  "1",
259
269
  ]);
260
270
  if (prs.length > 0) {
261
- return prs[0]!;
271
+ return prs[0] as PRInfo;
262
272
  }
263
273
 
264
274
  return yield* Effect.fail(
@@ -324,6 +334,8 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
324
334
  new GitHubMergeError({
325
335
  message: `PR #${opts.pr} has merge conflicts`,
326
336
  reason: "conflicts",
337
+ hint: "Resolve merge conflicts locally, push the fix, then retry the merge.",
338
+ nextCommand: `gh pr diff ${opts.pr}`,
327
339
  }),
328
340
  );
329
341
  }
@@ -333,6 +345,9 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
333
345
  new GitHubMergeError({
334
346
  message: `PR #${opts.pr} has failing required checks`,
335
347
  reason: "checks_failing",
348
+ hint: "Wait for CI checks to pass or investigate failures before merging.",
349
+ nextCommand: `agent-tools-gh pr checks --pr ${opts.pr}`,
350
+ retryable: true,
336
351
  }),
337
352
  );
338
353
  }
@@ -342,6 +357,7 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
342
357
  new GitHubMergeError({
343
358
  message: `PR #${opts.pr} targets a protected branch`,
344
359
  reason: "branch_protected",
360
+ hint: "This branch has protection rules. Ensure required reviews and checks are satisfied, or ask a repo admin.",
345
361
  }),
346
362
  );
347
363
  }
@@ -350,6 +366,8 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
350
366
  new GitHubMergeError({
351
367
  message: `Failed to merge PR #${opts.pr}: ${error.stderr}`,
352
368
  reason: "unknown",
369
+ hint: "Check the PR state and branch protections. The PR may already be merged or closed.",
370
+ nextCommand: `agent-tools-gh pr view --pr ${opts.pr}`,
353
371
  }),
354
372
  );
355
373
  }),
@@ -426,6 +444,9 @@ export const fetchChecks = Effect.fn("pr.fetchChecks")(function* (
426
444
  new GitHubTimeoutError({
427
445
  message: `CI check monitoring timed out after ${timeoutSeconds}s`,
428
446
  timeoutMs,
447
+ hint: "CI checks are still running. Retry with a longer --timeout or check status manually.",
448
+ nextCommand: `agent-tools-gh pr checks${pr !== null ? ` --pr ${pr}` : ""}`,
449
+ retryable: true,
429
450
  }),
430
451
  ),
431
452
  }),
@@ -1,7 +1,7 @@
1
1
  import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
2
2
  import { Effect, Stream } from "effect";
3
3
 
4
- import { GitHubCommandError } from "../errors";
4
+ import { GitHubCommandError } from "#src/gh-tool/errors";
5
5
 
6
6
  export type LocalCommandResult = {
7
7
  stdout: string;
@@ -11,9 +11,11 @@ export {
11
11
  prMergeCommand,
12
12
  prReplyCommand,
13
13
  prRerunChecksCommand,
14
+ prReplyAndResolveCommand,
14
15
  prResolveCommand,
15
16
  prStatusCommand,
16
17
  prSubmitReviewCommand,
17
18
  prThreadsCommand,
19
+ prReviewTriageCommand,
18
20
  prViewCommand,
19
21
  } from "./commands";
@@ -7,10 +7,10 @@ import type {
7
7
  IsoTimestamp,
8
8
  ReviewComment,
9
9
  ReviewThread,
10
- } from "../types";
10
+ } from "#src/gh-tool/types";
11
11
 
12
- import { GitHubCommandError } from "../errors";
13
- import { GitHubService } from "../service";
12
+ import { GitHubCommandError } from "#src/gh-tool/errors";
13
+ import { GitHubService } from "#src/gh-tool/service";
14
14
 
15
15
  import { viewPR } from "./core";
16
16
 
@@ -190,9 +190,12 @@ export const fetchThreads = Effect.fn("pr.fetchThreads")(function* (
190
190
  const threads = response.repository.pullRequest.reviewThreads.nodes;
191
191
 
192
192
  const mapped: ReviewThread[] = threads
193
- .filter((node) => node.comments.nodes.length > 0)
194
193
  .map((node) => {
195
- const comment = node.comments.nodes[0]!;
194
+ const comment = node.comments.nodes[0];
195
+ if (!comment) {
196
+ return null;
197
+ }
198
+
196
199
  return {
197
200
  threadId: node.id,
198
201
  commentId: comment.databaseId,
@@ -201,7 +204,8 @@ export const fetchThreads = Effect.fn("pr.fetchThreads")(function* (
201
204
  body: comment.body,
202
205
  isResolved: node.isResolved,
203
206
  };
204
- });
207
+ })
208
+ .filter((thread): thread is ReviewThread => thread !== null);
205
209
 
206
210
  return unresolvedOnly ? mapped.filter((t) => !t.isResolved) : mapped;
207
211
  });
@@ -1,7 +1,7 @@
1
1
  import { Command, Flag } from "effect/unstable/cli";
2
2
  import { Effect, Option } from "effect";
3
3
 
4
- import { formatOption, logFormatted } from "../shared";
4
+ import { formatOption, logFormatted } from "#src/shared";
5
5
  import { GitHubCommandError } from "./errors";
6
6
  import { GitHubService } from "./service";
7
7
 
@@ -66,6 +66,8 @@ export class GitHubService extends ServiceMap.Service<
66
66
  command: `gh ${args.join(" ")}`,
67
67
  exitCode: -1,
68
68
  stderr: `Command execution failed: ${String(platformError)}`,
69
+ hint: "Ensure the 'gh' CLI is installed and available on PATH.",
70
+ nextCommand: "gh --version",
69
71
  }),
70
72
  ),
71
73
  );
@@ -80,6 +82,8 @@ export class GitHubService extends ServiceMap.Service<
80
82
  ) {
81
83
  return yield* new GitHubAuthError({
82
84
  message: "GitHub CLI not authenticated. Run 'gh auth login'.",
85
+ hint: "Authenticate with GitHub CLI or set GITHUB_TOKEN environment variable.",
86
+ nextCommand: "gh auth login",
83
87
  });
84
88
  }
85
89
 
@@ -91,6 +95,7 @@ export class GitHubService extends ServiceMap.Service<
91
95
  message: result.stderr,
92
96
  resource: "unknown",
93
97
  identifier: "unknown",
98
+ hint: "Verify the resource exists and you have access. Check repository owner/name spelling.",
94
99
  });
95
100
  }
96
101
 
@@ -1,7 +1,7 @@
1
1
  import { Command, Flag } from "effect/unstable/cli";
2
2
  import { Console, Effect, Option } from "effect";
3
3
 
4
- import { formatOption, logFormatted } from "../shared";
4
+ import { formatOption, logFormatted } from "#src/shared";
5
5
  import { GitHubCommandError, GitHubNotFoundError } from "./errors";
6
6
  import { GitHubService } from "./service";
7
7
 
@@ -287,6 +287,8 @@ const resolveJobId = Effect.fn("workflow.resolveJobId")(function* (runId: number
287
287
  command: "workflow job-logs",
288
288
  exitCode: 1,
289
289
  stderr: "",
290
+ hint: `Multiple jobs match "${jobName}". Use the exact job name from the list above.`,
291
+ nextCommand: `agent-tools-gh workflow jobs --run ${runId}`,
290
292
  });
291
293
  }
292
294
 
@@ -294,6 +296,8 @@ const resolveJobId = Effect.fn("workflow.resolveJobId")(function* (runId: number
294
296
  message: `Job "${jobName}" not found in run ${runId}. Available jobs: ${jobs.map((j) => j.name).join(", ")}`,
295
297
  identifier: jobName,
296
298
  resource: "job",
299
+ hint: "Use one of the available job names listed above. Run the jobs command to see all jobs.",
300
+ nextCommand: `agent-tools-gh workflow jobs --run ${runId}`,
297
301
  });
298
302
  });
299
303
 
@@ -3,6 +3,9 @@ import { Schema } from "effect";
3
3
  export class K8sContextError extends Schema.TaggedErrorClass<K8sContextError>()("K8sContextError", {
4
4
  message: Schema.String,
5
5
  clusterId: Schema.String,
6
+ hint: Schema.optionalKey(Schema.String),
7
+ nextCommand: Schema.optionalKey(Schema.String),
8
+ retryable: Schema.optionalKey(Schema.Boolean),
6
9
  }) {}
7
10
 
8
11
  export class K8sCommandError extends Schema.TaggedErrorClass<K8sCommandError>()("K8sCommandError", {
@@ -10,12 +13,18 @@ export class K8sCommandError extends Schema.TaggedErrorClass<K8sCommandError>()(
10
13
  command: Schema.String,
11
14
  exitCode: Schema.optionalKey(Schema.Number),
12
15
  stderr: Schema.optionalKey(Schema.String),
16
+ hint: Schema.optionalKey(Schema.String),
17
+ nextCommand: Schema.optionalKey(Schema.String),
18
+ retryable: Schema.optionalKey(Schema.Boolean),
13
19
  }) {}
14
20
 
15
21
  export class K8sTimeoutError extends Schema.TaggedErrorClass<K8sTimeoutError>()("K8sTimeoutError", {
16
22
  message: Schema.String,
17
23
  command: Schema.String,
18
24
  timeoutMs: Schema.Number,
25
+ hint: Schema.optionalKey(Schema.String),
26
+ nextCommand: Schema.optionalKey(Schema.String),
27
+ retryable: Schema.optionalKey(Schema.Boolean),
19
28
  }) {}
20
29
 
21
30
  export type K8sError = K8sContextError | K8sCommandError | K8sTimeoutError;