@blogic-cz/agent-tools 1.16.1 → 1.17.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.
Files changed (57) hide show
  1. package/README.md +25 -0
  2. package/dist/gh-tool/issue/commands.d.ts +5 -0
  3. package/dist/gh-tool/issue/commands.d.ts.map +1 -1
  4. package/dist/gh-tool/issue/core.d.ts +19 -0
  5. package/dist/gh-tool/issue/core.d.ts.map +1 -1
  6. package/dist/gh-tool/issue/index.d.ts +1 -1
  7. package/dist/gh-tool/issue/index.d.ts.map +1 -1
  8. package/dist/gh-tool/issue/triage.d.ts +1 -1
  9. package/dist/gh-tool/issue/triage.d.ts.map +1 -1
  10. package/dist/gh-tool/main.d.ts.map +1 -1
  11. package/dist/gh-tool/pr/commands.d.ts +7 -7
  12. package/dist/gh-tool/pr/commands.d.ts.map +1 -1
  13. package/dist/gh-tool/pr/core.d.ts +9 -9
  14. package/dist/gh-tool/pr/review.d.ts +3 -3
  15. package/dist/gh-tool/pr/review.d.ts.map +1 -1
  16. package/dist/gh-tool/read/graphql-fallback.d.ts +21 -0
  17. package/dist/gh-tool/read/graphql-fallback.d.ts.map +1 -0
  18. package/dist/gh-tool/read/issues.d.ts +11 -1
  19. package/dist/gh-tool/read/issues.d.ts.map +1 -1
  20. package/dist/gh-tool/read/pulls.d.ts +13 -12
  21. package/dist/gh-tool/read/pulls.d.ts.map +1 -1
  22. package/dist/gh-tool/read/reads.d.ts +22 -5
  23. package/dist/gh-tool/read/reads.d.ts.map +1 -1
  24. package/dist/gh-tool/read/rest-porcelain.d.ts +131 -0
  25. package/dist/gh-tool/read/rest-porcelain.d.ts.map +1 -0
  26. package/dist/gh-tool/read/review-guards.d.ts +1 -1
  27. package/dist/gh-tool/read/review-guards.d.ts.map +1 -1
  28. package/dist/gh-tool/read/review-model.d.ts +1 -1
  29. package/dist/gh-tool/read/review-model.d.ts.map +1 -1
  30. package/dist/gh-tool/read/reviews.d.ts.map +1 -1
  31. package/dist/gh-tool/read/telemetry.d.ts +1 -1
  32. package/dist/gh-tool/read/telemetry.d.ts.map +1 -1
  33. package/dist/gh-tool/read/transport.d.ts.map +1 -1
  34. package/dist/gh-tool/repo.d.ts +3 -0
  35. package/dist/gh-tool/repo.d.ts.map +1 -1
  36. package/dist/gh-tool/types.d.ts +2 -2
  37. package/dist/gh-tool/types.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/src/gh-tool/issue/commands.ts +18 -0
  40. package/src/gh-tool/issue/core.ts +45 -1
  41. package/src/gh-tool/issue/index.ts +1 -0
  42. package/src/gh-tool/issue/triage.ts +1 -1
  43. package/src/gh-tool/main.ts +7 -2
  44. package/src/gh-tool/pr/commands.ts +15 -6
  45. package/src/gh-tool/pr/review.ts +27 -7
  46. package/src/gh-tool/read/graphql-fallback.ts +366 -0
  47. package/src/gh-tool/read/issues.ts +23 -1
  48. package/src/gh-tool/read/pulls.ts +60 -10
  49. package/src/gh-tool/read/reads.ts +12 -1
  50. package/src/gh-tool/read/rest-porcelain.ts +571 -0
  51. package/src/gh-tool/read/review-guards.ts +8 -2
  52. package/src/gh-tool/read/review-model.ts +1 -1
  53. package/src/gh-tool/read/reviews.ts +19 -3
  54. package/src/gh-tool/read/telemetry.ts +6 -3
  55. package/src/gh-tool/read/transport.ts +314 -7
  56. package/src/gh-tool/repo.ts +7 -1
  57. package/src/gh-tool/types.ts +2 -2
@@ -6,7 +6,7 @@ export type RequestEvent = {
6
6
  method: string;
7
7
  endpoint: string;
8
8
  status: number | null;
9
- outcome: "cache" | "304" | "200" | "bypass" | "error" | "subprocess";
9
+ outcome: "cache" | "304" | "200" | "bypass" | "error" | "subprocess" | "fallback";
10
10
  resource: string | null;
11
11
  used: number | null;
12
12
  remaining: number | null;
@@ -132,13 +132,16 @@ const subprocessSpendsResource = (row: RequestRow, resource: string): boolean =>
132
132
  };
133
133
 
134
134
  export const summarizeRequests = (rows: readonly RequestRow[]) => {
135
- const requests = rows.filter((row) => row.outcome !== "cache" && row.outcome !== "subprocess");
135
+ const requests = rows.filter(
136
+ (row) => row.outcome !== "cache" && row.outcome !== "subprocess" && row.outcome !== "fallback",
137
+ );
136
138
  const subprocesses = rows.filter((row) => row.outcome === "subprocess");
137
139
  const previous = new Map<string, { used: number; index: number }>();
138
140
  let externalUsageEstimate = 0;
139
141
  let intervals = 0;
140
142
  for (const [index, row] of rows.entries()) {
141
- if (row.outcome === "cache" || row.outcome === "subprocess") continue;
143
+ if (row.outcome === "cache" || row.outcome === "subprocess" || row.outcome === "fallback")
144
+ continue;
142
145
  if (row.resource === null || row.used === null || row.reset === null) continue;
143
146
  const resource = row.resource;
144
147
  const key = `${resource}:${row.reset}`;
@@ -1,6 +1,23 @@
1
1
  import { ghSubprocessEvent, recordRequest } from "./telemetry";
2
2
  import { ChildProcess, ChildProcessSpawner } from "effect/process";
3
3
  import { Context, Effect, Layer, Schema, Stream } from "effect";
4
+ import {
5
+ graphFieldsRequested,
6
+ restPorcelain,
7
+ restReviewDecision,
8
+ parseRest,
9
+ type ReviewDecisionInput,
10
+ type ReviewDecision,
11
+ } from "./rest-porcelain";
12
+ import {
13
+ graphqlLimitError,
14
+ isFallbackResult,
15
+ isGraphqlLimit,
16
+ markFallbackResult,
17
+ reportFallback,
18
+ restGraphql,
19
+ rateLimitResponse,
20
+ } from "./graphql-fallback";
4
21
 
5
22
  import type { GitHubRepoConfig } from "#config";
6
23
  import type { RepoInfo } from "#gh/types";
@@ -177,14 +194,14 @@ export class GitHubService extends Context.Service<
177
194
  return getGitHubConfig(config);
178
195
  });
179
196
 
180
- const executeGh = (args: string[]) =>
197
+ const executeGh = (args: string[], binary: string = GH_BINARY) =>
181
198
  Effect.scoped(
182
199
  Effect.gen(function* () {
183
200
  const ghRepo = yield* RepoTarget;
184
201
  const cache = yield* ReadCache;
185
202
  const credential = yield* SessionCredential;
186
203
  const cacheDirectory = cache?.store?.ghCacheDirectory;
187
- const command = ChildProcess.make(GH_BINARY, args, {
204
+ const command = ChildProcess.make(binary, args, {
188
205
  stdout: "pipe",
189
206
  stderr: "pipe",
190
207
  env: {
@@ -207,7 +224,8 @@ export class GitHubService extends Context.Service<
207
224
  const stdout = stdoutChunk.join("");
208
225
  const stderr = stderrChunk.join("");
209
226
  const exitCode = yield* proc.exitCode;
210
- yield* recordRequest(ghSubprocessEvent(args, Number(exitCode)));
227
+ if (binary === GH_BINARY)
228
+ yield* recordRequest(ghSubprocessEvent(args, Number(exitCode)));
211
229
 
212
230
  return {
213
231
  stdout,
@@ -233,6 +251,15 @@ export class GitHubService extends Context.Service<
233
251
  const result = yield* executeGh(args);
234
252
 
235
253
  if (result.exitCode !== 0) {
254
+ const limit =
255
+ args[0] === "api" && args[1] === "graphql" ? rateLimitResponse(result.stdout) : null;
256
+ if (limit !== null)
257
+ return yield* new GitHubCommandError({
258
+ command: "gh api graphql",
259
+ exitCode: result.exitCode,
260
+ message: limit,
261
+ stderr: limit,
262
+ });
236
263
  if (
237
264
  result.stderr.includes("not logged in") ||
238
265
  result.stderr.includes("gh auth login")
@@ -326,7 +353,7 @@ export class GitHubService extends Context.Service<
326
353
  });
327
354
 
328
355
  // Auto-retry transient failures, but only for idempotent reads (never replay a mutation).
329
- const runGh = (args: string[]): Effect.Effect<GhResult, GhError> => {
356
+ const runGhRaw = (args: string[]): Effect.Effect<GhResult, GhError> => {
330
357
  const canRetry = isSafeRetryRead(args);
331
358
  return Effect.gen(function* () {
332
359
  yield* validateOutboundText(args.join("\n"), "gh-tool");
@@ -339,6 +366,248 @@ export class GitHubService extends Context.Service<
339
366
  });
340
367
  };
341
368
 
369
+ let graphqlBlocked = false;
370
+ const ruleReads = new Map<string, Effect.Effect<unknown, GhError>>();
371
+ const uncachedJson = (args: string[]) =>
372
+ runGhRaw(args).pipe(
373
+ Effect.flatMap((result) =>
374
+ Effect.try({
375
+ try: () => JSON.parse(result.stdout) as unknown,
376
+ catch: (error) =>
377
+ new GitHubCommandError({
378
+ command: "gh REST",
379
+ exitCode: 1,
380
+ message: String(error),
381
+ stderr: String(error),
382
+ }),
383
+ }),
384
+ ),
385
+ );
386
+ const rawJson = (args: string[]): Effect.Effect<unknown, GhError> =>
387
+ Effect.gen(function* () {
388
+ const path = args[3];
389
+ if (
390
+ args[0] !== "api" ||
391
+ args[2] !== "GET" ||
392
+ path === undefined ||
393
+ !/^repos\/[^/]+\/[^/]+\/(?:rules\/branches\/[^/]+|branches\/[^/]+(?:\/protection\/required_pull_request_reviews)?)$/.test(
394
+ path,
395
+ )
396
+ )
397
+ return yield* uncachedJson(args);
398
+ let read = ruleReads.get(path);
399
+ if (read === undefined) {
400
+ read = yield* Effect.cached(uncachedJson(args));
401
+ ruleReads.set(path, read);
402
+ }
403
+ return yield* read;
404
+ });
405
+ const resolveDecision = (
406
+ input: ReviewDecisionInput,
407
+ ): Effect.Effect<ReviewDecision, GhError> =>
408
+ Effect.gen(function* () {
409
+ const decision = yield* restReviewDecision(rawJson, input);
410
+ if (decision !== "UNKNOWN" || graphqlBlocked) return decision;
411
+ return yield* rawJson([
412
+ "pr",
413
+ "view",
414
+ String(input.number),
415
+ "--repo",
416
+ input.repo,
417
+ "--json",
418
+ "reviewDecision",
419
+ ]).pipe(
420
+ Effect.flatMap((value) =>
421
+ parseRest(
422
+ Schema.Struct({
423
+ reviewDecision: Schema.NullOr(
424
+ Schema.Literals([
425
+ "APPROVED",
426
+ "CHANGES_REQUESTED",
427
+ "REVIEW_REQUIRED",
428
+ "UNKNOWN",
429
+ "",
430
+ ]),
431
+ ),
432
+ }),
433
+ value,
434
+ ),
435
+ ),
436
+ Effect.map((value) => value.reviewDecision ?? ""),
437
+ Effect.catch((error) => {
438
+ if (!isGraphqlLimit(error)) return Effect.fail(error);
439
+ graphqlBlocked = true;
440
+ return Effect.succeed("UNKNOWN" as const);
441
+ }),
442
+ );
443
+ });
444
+ const runGh = (args: string[]): Effect.Effect<GhResult, GhError> =>
445
+ Effect.gen(function* () {
446
+ yield* validateOutboundText(args.join("\n"), "gh-tool");
447
+ const target = yield* RepoTarget;
448
+ if (
449
+ args[0] === "pr" &&
450
+ ["view", "checks"].includes(args[1] ?? "") &&
451
+ (args[2] === undefined || args[2].startsWith("-"))
452
+ ) {
453
+ const branch = yield* executeGh(["branch", "--show-current"], "git");
454
+ if (branch.exitCode !== 0 || branch.stdout.trim() === "")
455
+ return yield* new GitHubCommandError({
456
+ command: "git branch --show-current",
457
+ exitCode: 1,
458
+ message: "Cannot determine the current branch; pass --pr",
459
+ stderr: branch.stderr,
460
+ });
461
+ const localBranch = branch.stdout.trim();
462
+ const trackingBranch = yield* executeGh(
463
+ ["config", "--get", "branch." + localBranch + ".merge"],
464
+ "git",
465
+ );
466
+ const trackingRemote = yield* executeGh(
467
+ ["config", "--get", "branch." + localBranch + ".remote"],
468
+ "git",
469
+ );
470
+ if (trackingBranch.exitCode > 1 || trackingRemote.exitCode > 1)
471
+ return yield* new GitHubCommandError({
472
+ command: "pr view",
473
+ exitCode: 1,
474
+ message: "Cannot read tracking configuration; pass --pr",
475
+ stderr: trackingBranch.stderr + trackingRemote.stderr,
476
+ });
477
+ const pullNumber =
478
+ trackingBranch.exitCode === 0
479
+ ? /^refs\/pull\/(\d+)\/head$/.exec(trackingBranch.stdout.trim())?.[1]
480
+ : undefined;
481
+ if (pullNumber !== undefined) {
482
+ args = [args[0], args[1] ?? "view", pullNumber, ...args.slice(2)];
483
+ } else {
484
+ let head =
485
+ trackingBranch.exitCode === 0
486
+ ? trackingBranch.stdout.trim().replace(/^refs\/heads\//, "")
487
+ : localBranch;
488
+ if (trackingRemote.exitCode === 0 && trackingRemote.stdout.trim() !== ".") {
489
+ const remote = yield* executeGh(
490
+ ["remote", "get-url", trackingRemote.stdout.trim()],
491
+ "git",
492
+ );
493
+ const owner =
494
+ remote.exitCode === 0
495
+ ? /[:/]([^/:]+)\/[^/]+?(?:\.git)?$/.exec(remote.stdout.trim())?.[1]
496
+ : undefined;
497
+ if (owner === undefined)
498
+ return yield* new GitHubCommandError({
499
+ command: "pr view",
500
+ exitCode: 1,
501
+ message: "Cannot determine the tracking repository; pass --pr",
502
+ stderr: remote.stderr,
503
+ });
504
+ head = owner + ":" + head;
505
+ }
506
+ const repoIndex = args.indexOf("--repo");
507
+ const selectedRepo = repoIndex < 0 ? target : args[repoIndex + 1];
508
+ const pulls = yield* runGh([
509
+ "pr",
510
+ "list",
511
+ "--head",
512
+ head,
513
+ "--state",
514
+ "all",
515
+ "--limit",
516
+ "100",
517
+ "--json",
518
+ "number,state",
519
+ ...(selectedRepo === undefined ? [] : ["--repo", selectedRepo]),
520
+ ]);
521
+ const candidates = yield* Effect.try({
522
+ try: () =>
523
+ Schema.decodeUnknownSync(
524
+ Schema.Array(Schema.Struct({ number: Schema.Number, state: Schema.String })),
525
+ )(JSON.parse(pulls.stdout)),
526
+ catch: (error) =>
527
+ new GitHubCommandError({
528
+ command: "pr view",
529
+ exitCode: 1,
530
+ message: String(error),
531
+ stderr: String(error),
532
+ }),
533
+ });
534
+ const open = candidates.filter((pull) => pull.state === "OPEN");
535
+ const matches = open.length > 0 ? open : candidates.slice(0, 1);
536
+ if (matches.length !== 1 || matches[0] === undefined)
537
+ return yield* new GitHubCommandError({
538
+ command: "pr view",
539
+ exitCode: 1,
540
+ message: "Cannot identify one PR for the current branch; pass --pr",
541
+ stderr: "Current branch has zero or multiple PRs",
542
+ });
543
+ args = [args[0], args[1] ?? "view", String(matches[0].number), ...args.slice(2)];
544
+ }
545
+ }
546
+ const rest = restPorcelain(args, rawJson, target, resolveDecision);
547
+ const usesGraphql =
548
+ args[0] === "pr" ||
549
+ args[0] === "repo" ||
550
+ (args[0] === "api" && args[1] === "graphql");
551
+ const mutation = args[0] === "pr" && args[1] === "merge";
552
+ const fallback = (
553
+ error: GhError,
554
+ resolved?: { value: unknown },
555
+ ): Effect.Effect<GhResult, GhError> =>
556
+ Effect.gen(function* () {
557
+ if (!usesGraphql || !isGraphqlLimit(error)) return yield* Effect.fail(error);
558
+ graphqlBlocked = true;
559
+ if (rest === null) return yield* Effect.fail(error);
560
+ const value = resolved === undefined ? yield* rest : resolved.value;
561
+ const values = Array.isArray(value) ? value : [value];
562
+ const unknown = ["mergeStateStatus"].filter((field) =>
563
+ args.some((arg) => arg.split(",").includes(field)),
564
+ );
565
+ if (
566
+ args.some((arg) => arg.split(",").includes("reviewDecision")) &&
567
+ values.some(
568
+ (item) =>
569
+ typeof item === "object" &&
570
+ item !== null &&
571
+ "reviewDecision" in item &&
572
+ item.reviewDecision === "UNKNOWN",
573
+ )
574
+ )
575
+ unknown.push("reviewDecision");
576
+ const notice = yield* reportFallback(unknown);
577
+ const output = Array.isArray(value)
578
+ ? value.map((item) => ({ ...item, fallback: notice }))
579
+ : { ...Object(value), fallback: notice };
580
+ return markFallbackResult({
581
+ stdout: JSON.stringify(output),
582
+ stderr: "",
583
+ exitCode: 0,
584
+ });
585
+ });
586
+ const jsonFields = args[args.indexOf("--json") + 1]?.split(",") ?? [];
587
+ if (
588
+ rest !== null &&
589
+ !mutation &&
590
+ (!graphFieldsRequested(args) || !jsonFields.includes("mergeStateStatus"))
591
+ ) {
592
+ const value = yield* rest;
593
+ if (graphqlBlocked && graphFieldsRequested(args))
594
+ return yield* fallback(graphqlLimitError(), { value });
595
+ return { stdout: JSON.stringify(value), stderr: "", exitCode: 0 };
596
+ }
597
+ if (graphqlBlocked && rest !== null) return yield* fallback(graphqlLimitError());
598
+ if (graphqlBlocked && args[0] === "api" && args[1] === "graphql")
599
+ return yield* graphqlLimitError();
600
+ if (graphqlBlocked && args[0] === "pr" && args[1] === "ready")
601
+ return yield* new GitHubCommandError({
602
+ command: "gh pr ready",
603
+ exitCode: 1,
604
+ message: "GitHub GraphQL is blocked; REST cannot change draft status",
605
+ stderr: "GitHub GraphQL is blocked; REST cannot change draft status",
606
+ });
607
+ if (graphqlBlocked && usesGraphql) return yield* graphqlLimitError();
608
+ return yield* runGhRaw(args).pipe(Effect.catch(fallback));
609
+ });
610
+
342
611
  const writePRCheckbox = Effect.fn("GitHubService.writePRCheckbox")(function* (
343
612
  opts: PRCheckboxWrite,
344
613
  ) {
@@ -423,10 +692,12 @@ export class GitHubService extends Context.Service<
423
692
  }),
424
693
  }).pipe(Effect.mapError((error) => error as GhError));
425
694
 
426
- return parsed;
695
+ return isFallbackResult(result) && typeof parsed === "object" && parsed !== null
696
+ ? markFallbackResult(parsed)
697
+ : parsed;
427
698
  }).pipe(Effect.withSpan("GitHubService.runGhJson"));
428
699
 
429
- const runGraphQL = Effect.fn("GitHubService.runGraphQL")(function* (
700
+ const runGraphQLRaw = Effect.fn("GitHubService.runGraphQL")(function* (
430
701
  query: string,
431
702
  variables: Record<string, string | number | null>,
432
703
  ) {
@@ -447,7 +718,13 @@ export class GitHubService extends Context.Service<
447
718
  const result = yield* runGh(args);
448
719
 
449
720
  const response = yield* Effect.try({
450
- try: () => JSON.parse(result.stdout) as { errors?: unknown[]; data?: unknown },
721
+ try: () =>
722
+ JSON.parse(result.stdout) as {
723
+ errors?: unknown[];
724
+ data?: unknown;
725
+ type?: string;
726
+ code?: string;
727
+ },
451
728
  catch: (error) =>
452
729
  new GitHubCommandError({
453
730
  message: `Failed to parse GraphQL response: ${error instanceof Error ? error.message : String(error)}`,
@@ -466,9 +743,39 @@ export class GitHubService extends Context.Service<
466
743
  });
467
744
  }
468
745
 
746
+ if (response.type === "RATE_LIMIT" || response.code === "graphql_rate_limit")
747
+ return yield* new GitHubCommandError({
748
+ command: "gh api graphql",
749
+ exitCode: 0,
750
+ message: "GitHub GraphQL RATE_LIMIT",
751
+ stderr: "GitHub GraphQL RATE_LIMIT",
752
+ });
469
753
  return response.data as unknown;
470
754
  });
471
755
 
756
+ const runGraphQL = (query: string, variables: Record<string, string | number | null>) =>
757
+ Effect.gen(function* () {
758
+ const rest = restGraphql(query, variables, rawJson);
759
+ const fallback = (error: GhError) => {
760
+ if (!isGraphqlLimit(error)) return Effect.fail(error);
761
+ graphqlBlocked = true;
762
+ return (
763
+ rest ??
764
+ Effect.fail(
765
+ new GitHubCommandError({
766
+ command: "gh api graphql",
767
+ exitCode: 1,
768
+ message: "GitHub GraphQL is blocked; REST cannot perform this operation",
769
+ stderr: "GitHub GraphQL is blocked; REST cannot perform this operation",
770
+ hint: "Retry after GitHub lifts the GraphQL limit. No equivalent REST operation is available.",
771
+ }),
772
+ )
773
+ );
774
+ };
775
+ if (graphqlBlocked) return yield* fallback(graphqlLimitError());
776
+ return yield* runGraphQLRaw(query, variables).pipe(Effect.catch(fallback));
777
+ });
778
+
472
779
  const getRepoInfo = Effect.fn("GitHubService.getRepoInfo")(function* () {
473
780
  const ghRepo = yield* RepoTarget;
474
781
  const cacheKey = ghRepo ?? null;
@@ -5,7 +5,7 @@ import { formatOption, logFormatted } from "#shared";
5
5
  import { passthrough } from "./read/catalog";
6
6
  import { GitHubService } from "./read/transport";
7
7
  import { commitAuthor } from "./read/commits";
8
- import { resolveRepo } from "./read/reads";
8
+ import { reads, resolveRepo } from "./read/reads";
9
9
 
10
10
  // ---------------------------------------------------------------------------
11
11
  // Types
@@ -58,6 +58,12 @@ export const repoInfoCommand = Command.make(
58
58
  }),
59
59
  ).pipe(Command.withDescription("Show repository information (owner, name, default branch, URL)"));
60
60
 
61
+ export const viewerCommand = Command.make("viewer", { format: formatOption }, ({ format }) =>
62
+ Effect.gen(function* () {
63
+ yield* logFormatted(yield* reads.viewerLogin(), format);
64
+ }),
65
+ ).pipe(Command.withDescription("Show the authenticated GitHub user's login"));
66
+
61
67
  export const repoListCommand = Command.make(
62
68
  "list",
63
69
  {
@@ -22,7 +22,7 @@ export type PRViewInfo = PRInfo & {
22
22
  baseSha: string | null;
23
23
  body: string;
24
24
  author: { login: string; is_bot: boolean };
25
- reviewDecision: "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED" | "";
25
+ reviewDecision: "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED" | "UNKNOWN" | "";
26
26
  reviewRequests: ReviewRequest[];
27
27
  };
28
28
 
@@ -40,7 +40,7 @@ export type ReviewThread = {
40
40
  originalCommitSha: string | null;
41
41
  diffHunk: string | null;
42
42
  body: string;
43
- isResolved: boolean;
43
+ isResolved: boolean | null;
44
44
  hasReply: boolean;
45
45
  replyCount: number;
46
46
  needsHumanReply: boolean;