pr-shepherd 0.9.0 → 0.10.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 (51) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +2 -2
  3. package/bin/checks/triage.mjs +32 -40
  4. package/bin/cli/fence.mjs +4 -0
  5. package/bin/cli/fix-formatter.mjs +21 -24
  6. package/bin/cli/formatters.mjs +33 -51
  7. package/bin/cli/handlers.mjs +1 -1
  8. package/bin/cli/iterate-formatter.mjs +20 -35
  9. package/bin/cli/iterate-lean.mjs +0 -3
  10. package/bin/cli/list-formatters.mjs +32 -0
  11. package/bin/cli/suggestion-renderer.mjs +31 -0
  12. package/bin/cli-parser.iterate-fixtures.mjs +1 -2
  13. package/bin/cli-parser.mjs +27 -3
  14. package/bin/commands/check.mjs +1 -2
  15. package/bin/commands/commit-suggestion.mjs +12 -10
  16. package/bin/commands/iterate/classify.mjs +5 -29
  17. package/bin/commands/iterate/escalate.mjs +3 -12
  18. package/bin/commands/iterate/fix-code.mjs +4 -9
  19. package/bin/commands/iterate/render.mjs +9 -2
  20. package/bin/commands/log-file.mjs +7 -0
  21. package/bin/commands/monitor.mjs +7 -4
  22. package/bin/commands/ready-delay.mjs +2 -2
  23. package/bin/commands/resolve-instructions.mjs +5 -2
  24. package/bin/commands/resolve.mjs +1 -20
  25. package/bin/commands/status.mjs +40 -28
  26. package/bin/comments/resolve.mjs +75 -65
  27. package/bin/config.json +2 -2
  28. package/bin/github/batch-parsers.mjs +2 -0
  29. package/bin/github/client.mjs +11 -32
  30. package/bin/github/gql/batch-pr.gql +4 -0
  31. package/bin/github/gql/get-pr-head-sha.gql +7 -0
  32. package/bin/github/http.mjs +166 -11
  33. package/bin/github/queries.mjs +7 -11
  34. package/bin/log/log-file.mjs +88 -0
  35. package/bin/log/session.mjs +100 -0
  36. package/bin/log/setup.mjs +54 -0
  37. package/bin/reporters/agent.mjs +14 -1
  38. package/bin/reporters/text.mjs +81 -102
  39. package/bin/state/base.mjs +5 -0
  40. package/bin/state/fix-attempts.mjs +2 -2
  41. package/bin/state/iterate-stall.mjs +2 -2
  42. package/bin/state/seen-comments.mjs +2 -2
  43. package/bin/suggestions/extract.mjs +15 -0
  44. package/bin/suggestions/parse.mjs +1 -26
  45. package/bin/util/markdown.mjs +7 -0
  46. package/bin/util/worktree.mjs +23 -0
  47. package/package.json +2 -2
  48. package/bin/github/gql/dismiss-review.gql +0 -7
  49. package/bin/github/gql/minimize-comment.gql +0 -7
  50. package/bin/github/gql/multi-pr-status.gql +0 -32
  51. package/bin/github/gql/resolve-thread.gql +0 -7
@@ -1,167 +1,146 @@
1
1
  import { buildCheckInstructions } from "./check-instructions.mjs";
2
+ import { renderThreadBullet, renderCommentBullet, renderReviewBullet, renderFirstLookStatusTag, } from "../cli/list-formatters.mjs";
3
+ import { joinSections } from "../util/markdown.mjs";
2
4
  export function formatText(report) {
3
- const parts = [];
4
- parts.push(`\nPR #${report.pr} — ${report.repo}`);
5
- parts.push(`Status: ${report.status}`);
6
- parts.push(`Base: ${report.baseBranch}`);
7
- parts.push("");
5
+ const header = [
6
+ `# PR #${report.pr} [CHECK] — ${report.repo}`,
7
+ `Status: ${report.status}`,
8
+ `Base: ${report.baseBranch}`,
9
+ ].join("\n");
8
10
  const ms = report.mergeStatus;
9
- parts.push("## Merge Status");
10
- parts.push("");
11
- parts.push(`${ms.status}`);
12
- parts.push(` mergeStateStatus: ${ms.mergeStateStatus}`);
13
- parts.push(` mergeable: ${ms.mergeable}`);
14
- parts.push(` reviewDecision: ${ms.reviewDecision ?? "(none)"}`);
15
- parts.push(` isDraft: ${ms.isDraft}`);
16
- parts.push(` copilotReviewInProgress: ${ms.copilotReviewInProgress}`);
17
- parts.push("");
11
+ const mergeStatusSection = [
12
+ "## Merge Status",
13
+ "",
14
+ `- status: \`${ms.status}\``,
15
+ `- mergeStateStatus: \`${ms.mergeStateStatus}\``,
16
+ `- mergeable: \`${ms.mergeable}\``,
17
+ `- reviewDecision: \`${ms.reviewDecision ?? "(none)"}\``,
18
+ `- isDraft: \`${ms.isDraft}\``,
19
+ `- copilotReviewInProgress: \`${ms.copilotReviewInProgress}\``,
20
+ ].join("\n");
18
21
  const { passing, failing, inProgress, skipped } = report.checks;
19
22
  const total = passing.length + failing.length + inProgress.length + skipped.length;
20
- parts.push("## CI Checks");
21
- parts.push("");
22
- parts.push(`${passing.length}/${total} passed`);
23
- parts.push("");
23
+ const ciSubsections = [`${passing.length}/${total} passed`];
24
24
  if (failing.length > 0) {
25
- parts.push(`### Failed (${failing.length})`);
26
- parts.push("");
25
+ const lines = [`### Failed (${failing.length})`, ""];
27
26
  for (const c of failing) {
28
27
  const triaged = c;
29
28
  const prefix = triaged.workflowName ? `${triaged.workflowName} › ` : "";
30
- parts.push(`- ${prefix}${c.name}: ${c.conclusion ?? c.status}`);
29
+ lines.push(`- ${prefix}${c.name}: ${c.conclusion ?? c.status}`);
31
30
  if (triaged.failedStep) {
32
- parts.push(` failed step: ${triaged.failedStep}`);
31
+ lines.push(` failed step: ${triaged.failedStep}`);
33
32
  }
34
33
  if (triaged.summary) {
35
- parts.push(` summary: ${triaged.summary}`);
34
+ lines.push(` summary: ${triaged.summary}`);
36
35
  }
37
36
  }
38
- parts.push("");
37
+ ciSubsections.push(lines.join("\n"));
39
38
  }
40
39
  if (inProgress.length > 0) {
41
- parts.push(`### In Progress (${inProgress.length})`);
42
- parts.push("");
40
+ const lines = [`### In Progress (${inProgress.length})`, ""];
43
41
  for (const c of inProgress) {
44
- parts.push(`- ${c.name}: ${c.status}`);
42
+ lines.push(`- ${c.name}: ${c.status}`);
45
43
  }
46
- parts.push("");
44
+ ciSubsections.push(lines.join("\n"));
47
45
  }
48
46
  if (skipped.length > 0) {
49
- parts.push(`### Skipped (${skipped.length}): ${skipped.map((c) => c.name).join(", ")}`);
50
- parts.push("");
47
+ ciSubsections.push(`### Skipped (${skipped.length}): ${skipped.map((c) => c.name).join(", ")}`);
51
48
  }
52
49
  if (report.checks.filtered.length > 0) {
53
- parts.push(`### Filtered non-PR-trigger (${report.checks.filtered.length}): ${report.checks.filtered.map((c) => c.name).join(", ")}`);
54
- parts.push("");
50
+ const lines = [
51
+ `### Filtered non-PR-trigger (${report.checks.filtered.length}): ${report.checks.filtered.map((c) => c.name).join(", ")}`,
52
+ "",
53
+ ];
55
54
  if (report.checks.blockedByFilteredCheck) {
56
- parts.push(" Note: PR is BLOCKED and all filtered checks are non-PR-trigger — one of these filtered checks may be a required status check blocking merge.");
55
+ lines.push("> Note: PR is BLOCKED and all filtered checks are non-PR-trigger — one of these filtered checks may be a required status check blocking merge.");
57
56
  }
58
57
  else if (report.mergeStatus.status === "BLOCKED") {
59
- parts.push(" Note: one or more of these filtered checks may be a required status check blocking merge.");
58
+ lines.push("> Note: one or more of these filtered checks may be a required status check blocking merge.");
60
59
  }
61
- parts.push("");
60
+ ciSubsections.push(lines.join("\n"));
62
61
  }
62
+ const ciSection = `## CI Checks\n\n${ciSubsections.join("\n\n")}`;
63
63
  const { actionable: actionableThreads, autoResolved, autoResolveErrors, firstLook: firstLookThreads, } = report.threads;
64
64
  const hasThreadSection = autoResolved.length > 0 || autoResolveErrors.length > 0 || actionableThreads.length > 0;
65
+ let threadsSection = null;
65
66
  if (hasThreadSection) {
66
- parts.push("## Review Threads");
67
- parts.push("");
67
+ const subparts = [];
68
68
  if (autoResolved.length > 0) {
69
- parts.push(`Auto-resolved outdated (${autoResolved.length}):`);
69
+ const lines = [`Auto-resolved outdated (${autoResolved.length}):`];
70
70
  for (const t of autoResolved) {
71
- parts.push(`- threadId=${t.id} ${t.path ?? ""}:${t.line ?? "?"} (@${t.author})`);
71
+ lines.push(renderThreadBullet(t));
72
72
  }
73
- parts.push("");
73
+ subparts.push(lines.join("\n"));
74
74
  }
75
75
  if (autoResolveErrors.length > 0) {
76
- parts.push(`Auto-resolve errors (${autoResolveErrors.length}):`);
76
+ const lines = [`Auto-resolve errors (${autoResolveErrors.length}):`];
77
77
  for (const e of autoResolveErrors) {
78
- parts.push(`- ${e}`);
78
+ lines.push(`- ${e}`);
79
79
  }
80
- parts.push("");
80
+ subparts.push(lines.join("\n"));
81
81
  }
82
82
  if (actionableThreads.length > 0) {
83
- parts.push(`### Actionable (${actionableThreads.length})`);
84
- parts.push("");
83
+ const lines = [`### Actionable (${actionableThreads.length})`, ""];
85
84
  for (const t of actionableThreads) {
86
- const label = t.path ? `${t.path}:${t.line ?? "?"}` : "(general)";
87
- parts.push(`- threadId=${t.id} ${label} (@${t.author})`);
88
- parts.push(` ${firstLine(t.body)}`);
85
+ lines.push(renderThreadBullet(t));
89
86
  }
90
- parts.push("");
87
+ subparts.push(lines.join("\n"));
91
88
  }
89
+ threadsSection = `## Review Threads\n\n${subparts.join("\n\n")}`;
92
90
  }
93
91
  const { actionable: actionableComments, firstLook: firstLookComments } = report.comments;
92
+ let commentsSection = null;
94
93
  if (actionableComments.length > 0) {
95
- parts.push("## PR Comments");
96
- parts.push("");
97
- parts.push(`### Actionable (${actionableComments.length})`);
98
- parts.push("");
94
+ const lines = [`### Actionable (${actionableComments.length})`, ""];
99
95
  for (const c of actionableComments) {
100
- parts.push(`- commentId=${c.id} (@${c.author}): ${firstLine(c.body)}`);
96
+ lines.push(renderCommentBullet(c));
101
97
  }
102
- parts.push("");
103
- }
104
- if (report.changesRequestedReviews.length > 0) {
105
- parts.push("## CHANGES_REQUESTED Reviews");
106
- parts.push("");
107
- for (const r of report.changesRequestedReviews) {
108
- parts.push(`- reviewId=${r.id} (@${r.author}): ${firstLine(r.body)}`);
109
- }
110
- parts.push("");
111
- }
112
- if (report.reviewSummaries.length > 0) {
113
- parts.push("## Review Summaries");
114
- parts.push("");
115
- for (const r of report.reviewSummaries) {
116
- parts.push(`- reviewId=${r.id} (@${r.author}): ${firstLine(r.body)}`);
117
- }
118
- parts.push("");
119
- }
120
- if (report.approvedReviews.length > 0) {
121
- parts.push("## Approved Reviews");
122
- parts.push("");
123
- for (const r of report.approvedReviews) {
124
- parts.push(`- reviewId=${r.id} (@${r.author}): ${firstLine(r.body)}`);
125
- }
126
- parts.push("");
98
+ commentsSection = `## PR Comments\n\n${lines.join("\n")}`;
127
99
  }
100
+ const changesRequestedSection = reviewListSection("CHANGES_REQUESTED Reviews", report.changesRequestedReviews);
101
+ const reviewSummariesSection = reviewListSection("Review Summaries", report.reviewSummaries);
102
+ const approvedReviewsSection = reviewListSection("Approved Reviews", report.approvedReviews);
128
103
  const firstLookTotal = firstLookThreads.length + firstLookComments.length;
104
+ let firstLookSection = null;
129
105
  if (firstLookTotal > 0) {
130
- parts.push("## First-look items");
131
- parts.push("");
106
+ const lines = [
107
+ `## First-look items (${firstLookTotal}) — already closed on GitHub; acknowledge only`,
108
+ "",
109
+ ];
132
110
  for (const t of firstLookThreads) {
133
- const statusTag = t.autoResolved
134
- ? `[status: outdated, auto-resolved]`
135
- : `[status: ${t.firstLookStatus}]`;
136
- const loc = t.path ? `${t.path}:${t.line ?? "?"}` : "(no location)";
137
- parts.push(`- threadId=${t.id} ${loc} (@${t.author}) ${statusTag}`);
138
- parts.push(` ${firstLine(t.body)}`);
111
+ lines.push(renderThreadBullet(t, { statusTag: renderFirstLookStatusTag(t) }));
139
112
  }
140
113
  for (const c of firstLookComments) {
141
- parts.push(`- commentId=${c.id} (@${c.author}) [status: minimized]`);
142
- parts.push(` ${firstLine(c.body)}`);
114
+ lines.push(renderCommentBullet(c, { statusTag: "[status: minimized]" }));
143
115
  }
144
- parts.push("");
116
+ firstLookSection = lines.join("\n");
145
117
  }
146
118
  const totalActionable = actionableThreads.length + actionableComments.length + report.changesRequestedReviews.length;
147
- parts.push("## Summary");
148
- parts.push("");
149
119
  const counts = [];
150
120
  if (totalActionable > 0)
151
121
  counts.push(`${totalActionable} actionable`);
152
122
  if (firstLookTotal > 0)
153
123
  counts.push(`${firstLookTotal} first-look`);
154
124
  const summaryLine = counts.join(", ") || "0 actionable — all threads resolved/minimized";
155
- parts.push(summaryLine);
156
- parts.push("");
157
- parts.push("## Instructions");
158
- parts.push("");
125
+ const summarySection = `## Summary\n\n${summaryLine}`;
159
126
  const instructions = buildCheckInstructions(report);
160
- instructions.forEach((step, i) => {
161
- parts.push(`${i + 1}. ${step}`);
162
- });
163
- return parts.join("\n");
127
+ const instructionsSection = `## Instructions\n\n${instructions.map((step, i) => `${i + 1}. ${step}`).join("\n")}`;
128
+ return joinSections([
129
+ header,
130
+ mergeStatusSection,
131
+ ciSection,
132
+ threadsSection,
133
+ commentsSection,
134
+ changesRequestedSection,
135
+ reviewSummariesSection,
136
+ approvedReviewsSection,
137
+ firstLookSection,
138
+ summarySection,
139
+ instructionsSection,
140
+ ]);
164
141
  }
165
- function firstLine(text) {
166
- return (text.split("\n")[0] ?? "").trim().slice(0, 120);
142
+ function reviewListSection(heading, items) {
143
+ if (items.length === 0)
144
+ return null;
145
+ return `## ${heading}\n\n${items.map((r) => renderReviewBullet(r, { includeBody: true })).join("\n")}`;
167
146
  }
@@ -0,0 +1,5 @@
1
+ import { join } from "node:path";
2
+ import { tmpdir } from "node:os";
3
+ export function resolveStateBase() {
4
+ return process.env["PR_SHEPHERD_STATE_DIR"] ?? join(tmpdir(), "pr-shepherd-state");
5
+ }
@@ -10,8 +10,8 @@
10
10
  import { readFile, writeFile, rename, unlink, mkdir } from "node:fs/promises";
11
11
  import { randomUUID } from "node:crypto";
12
12
  import { join, dirname } from "node:path";
13
- import { tmpdir } from "node:os";
14
13
  import { SAFE_SEGMENT } from "../util/path-segment.mjs";
14
+ import { resolveStateBase } from "./base.mjs";
15
15
  // ---------------------------------------------------------------------------
16
16
  // Public API
17
17
  // ---------------------------------------------------------------------------
@@ -62,6 +62,6 @@ function resolvePath(key) {
62
62
  throw new Error(`Invalid state key segment "${field}": ${value}`);
63
63
  }
64
64
  }
65
- const base = process.env["PR_SHEPHERD_STATE_DIR"] ?? join(tmpdir(), "pr-shepherd-state");
65
+ const base = resolveStateBase();
66
66
  return join(base, `${key.owner}-${key.repo}`, String(key.pr), "fix-attempts.json");
67
67
  }
@@ -10,8 +10,8 @@
10
10
  import { readFile, writeFile, rename, unlink, mkdir } from "node:fs/promises";
11
11
  import { randomUUID } from "node:crypto";
12
12
  import { join, dirname } from "node:path";
13
- import { tmpdir } from "node:os";
14
13
  import { SAFE_SEGMENT } from "../util/path-segment.mjs";
14
+ import { resolveStateBase } from "./base.mjs";
15
15
  // ---------------------------------------------------------------------------
16
16
  // Public API
17
17
  // ---------------------------------------------------------------------------
@@ -69,6 +69,6 @@ function resolvePath(key) {
69
69
  throw new Error(`Invalid state key segment "${field}": ${value}`);
70
70
  }
71
71
  }
72
- const base = process.env["PR_SHEPHERD_STATE_DIR"] ?? join(tmpdir(), "pr-shepherd-state");
72
+ const base = resolveStateBase();
73
73
  return join(base, `${key.owner}-${key.repo}`, String(key.pr), "iterate-stall.json");
74
74
  }
@@ -1,7 +1,7 @@
1
1
  import { readFile, writeFile, mkdir, access, readdir } from "node:fs/promises";
2
2
  import { join, dirname } from "node:path";
3
- import { tmpdir } from "node:os";
4
3
  import { SAFE_SEGMENT } from "../util/path-segment.mjs";
4
+ import { resolveStateBase } from "./base.mjs";
5
5
  // ---------------------------------------------------------------------------
6
6
  // Public API
7
7
  // ---------------------------------------------------------------------------
@@ -65,7 +65,7 @@ function resolveDir(key) {
65
65
  throw new Error(`Invalid state key segment "${field}": ${value}`);
66
66
  }
67
67
  }
68
- const base = process.env["PR_SHEPHERD_STATE_DIR"] ?? join(tmpdir(), "pr-shepherd-state");
68
+ const base = resolveStateBase();
69
69
  return join(base, `${key.owner}-${key.repo}`, String(key.pr), "seen");
70
70
  }
71
71
  function resolvePath(key, id) {
@@ -0,0 +1,15 @@
1
+ import { parseSuggestion } from "./parse.mjs";
2
+ export function extractSuggestion(thread) {
3
+ if (!thread.path || thread.line === null)
4
+ return null;
5
+ const parsed = parseSuggestion(thread.body);
6
+ if (!parsed)
7
+ return null;
8
+ const startLine = thread.startLine ?? thread.line;
9
+ return {
10
+ startLine,
11
+ endLine: thread.line,
12
+ lines: parsed.lines,
13
+ author: thread.author,
14
+ };
15
+ }
@@ -3,8 +3,7 @@
3
3
  *
4
4
  * GitHub's "Commit suggestion" UI button treats the first ```suggestion fenced
5
5
  * block in a review comment as a replacement for the commented line range.
6
- * This module extracts that block from the comment body, plus applies a
7
- * parsed suggestion to a file's contents.
6
+ * This module extracts that block from the comment body.
8
7
  *
9
8
  * There is no GitHub API for applying suggestions — tools that reproduce the
10
9
  * button (this one included) must parse + commit themselves.
@@ -93,27 +92,3 @@ export function isCommittableSuggestion(parsed) {
93
92
  const fenceRuns = (replacement.match(/`{3,}/g) ?? []).length;
94
93
  return fenceRuns % 2 === 0;
95
94
  }
96
- /**
97
- * Apply a suggestion to a file's full contents by replacing lines
98
- * [startLine..endLine] (1-indexed, inclusive) with the given replacement lines.
99
- *
100
- * Pass `[]` to delete the range, `[""]` to replace with a single blank line,
101
- * or `["a", "b", ...]` for arbitrary replacements. The file's trailing-newline
102
- * state is preserved exactly.
103
- */
104
- export function applySuggestionToFile(fileContent, startLine, endLine, replacementLines) {
105
- if (startLine < 1 || endLine < startLine) {
106
- throw new Error(`Invalid line range: start=${startLine}, end=${endLine}`);
107
- }
108
- const endsWithNewline = fileContent.endsWith("\n");
109
- // Strip a single trailing \n so split/join round-trips exactly.
110
- const body = endsWithNewline ? fileContent.slice(0, -1) : fileContent;
111
- const lines = body.split("\n");
112
- if (endLine > lines.length) {
113
- throw new Error(`Line ${endLine} is out of range (file has ${lines.length} line(s))`);
114
- }
115
- const before = lines.slice(0, startLine - 1);
116
- const after = lines.slice(endLine);
117
- const result = [...before, ...replacementLines, ...after].join("\n");
118
- return endsWithNewline ? `${result}\n` : result;
119
- }
@@ -0,0 +1,7 @@
1
+ export function joinSections(sections) {
2
+ return sections
3
+ .filter((s) => s != null)
4
+ .map((s) => s.replace(/^[\r\n]+|[\r\n]+$/g, ""))
5
+ .filter((s) => s !== "")
6
+ .join("\n\n");
7
+ }
@@ -0,0 +1,23 @@
1
+ import { execFile as execFileCb } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { createHash } from "node:crypto";
4
+ import { basename } from "node:path";
5
+ import { SAFE_SEGMENT } from "./path-segment.mjs";
6
+ const execFile = promisify(execFileCb);
7
+ /** Returns the absolute path to the current git worktree root. Throws outside a git repo. */
8
+ export async function getWorktreeRoot() {
9
+ const { stdout } = await execFile("git", ["rev-parse", "--show-toplevel"]);
10
+ return stdout.trim();
11
+ }
12
+ /**
13
+ * Returns a filesystem-safe key for the current worktree, unique across
14
+ * worktrees of the same repo. Format: `<basename>-<sha8>` where sha8 is
15
+ * the first 8 hex chars of sha256(toplevel). Falls back to sha8-only if
16
+ * the basename contains characters outside SAFE_SEGMENT.
17
+ */
18
+ export async function getWorktreeKey() {
19
+ const root = await getWorktreeRoot();
20
+ const sha8 = createHash("sha256").update(root).digest("hex").slice(0, 8);
21
+ const base = basename(root);
22
+ return SAFE_SEGMENT.test(base) ? `${base}-${sha8}` : sha8;
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for Claude Code",
5
5
  "license": "MIT",
6
6
  "author": "Jonathan Ong",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^25.6.0",
27
- "oxfmt": "^0.45.0",
27
+ "oxfmt": "^0.46.0",
28
28
  "oxlint": "^1.60.0",
29
29
  "typescript": "^6.0.3",
30
30
  "vitest": "^4.1.4",
@@ -1,7 +0,0 @@
1
- mutation DismissReview($reviewId: ID!, $message: String!) {
2
- dismissPullRequestReview(input: { pullRequestReviewId: $reviewId, message: $message }) {
3
- pullRequestReview {
4
- state
5
- }
6
- }
7
- }
@@ -1,7 +0,0 @@
1
- mutation MinimizeComment($commentId: ID!, $classifier: ReportedContentClassifiers!) {
2
- minimizeComment(input: { subjectId: $commentId, classifier: $classifier }) {
3
- minimizedComment {
4
- isMinimized
5
- }
6
- }
7
- }
@@ -1,32 +0,0 @@
1
- query MultiPrStatus($owner: String!, $repo: String!, $pr: Int!) {
2
- repository(owner: $owner, name: $repo) {
3
- pullRequest(number: $pr) {
4
- number
5
- state
6
- isDraft
7
- title
8
- mergeStateStatus
9
- reviewDecision
10
- # totalCount lets callers detect truncation; pageInfo enables backward pagination.
11
- reviewThreads(last: 100) {
12
- totalCount
13
- pageInfo {
14
- hasPreviousPage
15
- startCursor
16
- }
17
- nodes {
18
- isResolved
19
- }
20
- }
21
- commits(last: 1) {
22
- nodes {
23
- commit {
24
- statusCheckRollup {
25
- state
26
- }
27
- }
28
- }
29
- }
30
- }
31
- }
32
- }
@@ -1,7 +0,0 @@
1
- mutation ResolveThread($threadId: ID!) {
2
- resolveReviewThread(input: { threadId: $threadId }) {
3
- thread {
4
- isResolved
5
- }
6
- }
7
- }