pr-shepherd 0.21.0 → 0.22.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -3
- package/bin/checks/triage.mjs +1 -0
- package/bin/cli/fix-formatter.mjs +36 -8
- package/bin/cli/iterate-lean.mjs +4 -0
- package/bin/cli/list-formatters.mjs +53 -4
- package/bin/commands/check-annotations.mjs +40 -0
- package/bin/commands/check.mjs +9 -6
- package/bin/commands/check.test-support.mjs +9 -2
- package/bin/commands/iterate/escalate.mjs +16 -2
- package/bin/commands/iterate/fix-code.mjs +9 -2
- package/bin/commands/iterate/helpers.mjs +1 -0
- package/bin/commands/iterate/render.mjs +6 -0
- package/bin/commands/iterate/stall.mjs +43 -2
- package/bin/commands/iterate-test-support.mjs +1 -1
- package/bin/commands/iterate.fix-code-in-progress.test-support.mjs +6 -1
- package/bin/commands/resolve.mjs +5 -4
- package/bin/config.json +1 -1
- package/bin/github/batch-parsers.mjs +30 -3
- package/bin/github/batch.mjs +2 -0
- package/bin/github/check-annotations.mjs +75 -0
- package/bin/github/client.test-support.mjs +55 -0
- package/bin/github/gql/batch-pr.gql +15 -2
- package/bin/github/gql/check-run-annotations.gql +32 -0
- package/bin/github/gql/review-thread-comments.gql +27 -0
- package/bin/github/queries.mjs +4 -0
- package/bin/github/thread-comments.mjs +34 -0
- package/bin/reporters/agent.mjs +26 -0
- package/bin/state/seen-comments.test-support.mjs +19 -0
- package/bin/threads/transcript.mjs +31 -0
- package/bin/types/agent-thread.mjs +1 -0
- package/bin/types/check-annotations.mjs +1 -0
- package/bin/types/review-thread.mjs +1 -0
- package/bin/types.mjs +3 -0
- package/package.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,9 @@ At a high level, the skill invokes `pr-shepherd <PR>` (which polls by default),
|
|
|
34
34
|
|
|
35
35
|
## Review threads
|
|
36
36
|
|
|
37
|
-
### `PRRT_kwDOSGizTs58XB1L` — `src/commands/iterate/index.mts:42` (@alice)
|
|
37
|
+
### `threadId=PRRT_kwDOSGizTs58XB1L` — `src/commands/iterate/index.mts:42` (@alice)
|
|
38
|
+
|
|
39
|
+
#### `commentId=PRRC_kwDOSGizTs58XB1M` (@alice)
|
|
38
40
|
|
|
39
41
|
> The variable name is misleading.
|
|
40
42
|
>
|
|
@@ -97,7 +99,7 @@ Some other workflow improvements:
|
|
|
97
99
|
|
|
98
100
|
Recommendations:
|
|
99
101
|
|
|
100
|
-
- Run `pr-shepherd` on all your PRs before you go to sleep so that you wake up to reviewable PRs. Keep an active goal cycling `pr-shepherd <PR>` until Shepherd emits `[CANCEL]` for ready-delay completion or merged/closed, or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures).
|
|
102
|
+
- Run `pr-shepherd` on all your PRs before you go to sleep so that you wake up to reviewable PRs. Keep an active goal cycling `pr-shepherd <PR>` until Shepherd emits `[CANCEL]` for ready-delay completion or merged/closed, or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures or CI that never starts).
|
|
101
103
|
- Instruct your agents to write comments in a single review (comment, changes requested, or approved). This allows the review's comments/threads to be minimized or resolved together, keeping your pull request history clean. If you write inline comments outside of a review, each comment would still show up in the pull request history and take up space.
|
|
102
104
|
- Avoid sticky comments as they will continue to be hidden. Instead, just make a new comment, especially on reviews. If you really want sticky comments, instruct your agent to unhide/unminimize them when updating them.
|
|
103
105
|
- Avoid having automation edit comments, reviews, or threads in place because updated items get minimized. Instead, always make a new review, comment, thread, etc.
|
|
@@ -197,7 +199,7 @@ Iterate a PR from Codex:
|
|
|
197
199
|
pr-shepherd iterate 42
|
|
198
200
|
```
|
|
199
201
|
|
|
200
|
-
Or ask Codex to use the `pr-shepherd` skill, for example: `run pr-shepherd until this PR is ready`. Follow the output's `## Instructions`. Continue until Shepherd emits `[CANCEL]` or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures). `pr-shepherd iterate 42` remains supported for existing workflows.
|
|
202
|
+
Or ask Codex to use the `pr-shepherd` skill, for example: `run pr-shepherd until this PR is ready`. Follow the output's `## Instructions`. Continue until Shepherd emits `[CANCEL]` or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures or CI that never starts). `pr-shepherd iterate 42` remains supported for existing workflows.
|
|
201
203
|
|
|
202
204
|
## Configuration
|
|
203
205
|
|
package/bin/checks/triage.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { renderResolveCommand } from "../commands/iterate/render.mjs";
|
|
2
2
|
import { joinSections } from "../util/markdown.mjs";
|
|
3
3
|
import { renderSuggestionBlock, renderLineRange } from "./suggestion-renderer.mjs";
|
|
4
|
-
import { renderThreadBullet, renderReviewBullet, renderThreadResolutionStatusTag, renderAuthor, buildFirstLookBullets, } from "./list-formatters.mjs";
|
|
4
|
+
import { renderThreadBullet, renderReviewBullet, renderThreadResolutionStatusTag, renderAuthor, buildFirstLookBullets, renderThreadConversation, blockquote, } from "./list-formatters.mjs";
|
|
5
5
|
import { adaptFixCodeInstructions, numberInstructions } from "./iterate-instructions.mjs";
|
|
6
6
|
export function formatFixCodeResult(header, result, opts) {
|
|
7
7
|
const readyDelaySuffix = opts?.readyDelaySuffix;
|
|
@@ -14,7 +14,7 @@ export function formatFixCodeResult(header, result, opts) {
|
|
|
14
14
|
const heading = t.url ? `[threadId=${t.id}](${t.url})` : `\`threadId=${t.id}\``;
|
|
15
15
|
const suggestionMarker = t.suggestion ? " [suggestion]" : "";
|
|
16
16
|
sections.push(`### ${heading} — ${loc} (${renderAuthor(t.author, t.authorType)})${suggestionMarker}`);
|
|
17
|
-
sections.push(
|
|
17
|
+
sections.push(renderThreadConversation(t));
|
|
18
18
|
if (t.suggestion) {
|
|
19
19
|
sections.push(renderSuggestionBlock(t.suggestion, ""));
|
|
20
20
|
}
|
|
@@ -57,6 +57,21 @@ export function formatFixCodeResult(header, result, opts) {
|
|
|
57
57
|
});
|
|
58
58
|
sections.push(bullets.join("\n\n"));
|
|
59
59
|
}
|
|
60
|
+
const checksWithAnnotations = result.fix.checks.filter((ch) => (ch.annotations?.length ?? 0) > 0);
|
|
61
|
+
if (checksWithAnnotations.length > 0) {
|
|
62
|
+
sections.push("## Check annotations");
|
|
63
|
+
for (const ch of checksWithAnnotations) {
|
|
64
|
+
const workflowPrefix = ch.workflowName ? `${ch.workflowName} › ` : "";
|
|
65
|
+
const jobLabel = ch.jobName ? ch.jobName : ch.name;
|
|
66
|
+
const locator = ch.runId
|
|
67
|
+
? `\`${ch.runId}\``
|
|
68
|
+
: ch.detailsUrl
|
|
69
|
+
? `external \`${ch.detailsUrl}\``
|
|
70
|
+
: "(no runId)";
|
|
71
|
+
sections.push(`### ${locator} — \`${workflowPrefix}${jobLabel}\``);
|
|
72
|
+
sections.push(ch.annotations.map(renderCheckAnnotation).join("\n\n"));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
60
75
|
if (result.fix.changesRequestedReviews.length > 0) {
|
|
61
76
|
sections.push("## Changes-requested reviews");
|
|
62
77
|
sections.push(result.fix.changesRequestedReviews.map((r) => renderReviewBullet(r)).join("\n"));
|
|
@@ -112,10 +127,23 @@ export function formatFixCodeResult(header, result, opts) {
|
|
|
112
127
|
sections.push(numberInstructions(adaptFixCodeInstructions(result.fix.instructions, result.pr, readyDelaySuffix)));
|
|
113
128
|
return joinSections(sections);
|
|
114
129
|
}
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
130
|
+
function renderCheckAnnotation(a) {
|
|
131
|
+
const loc = `${a.path}:${renderAnnotationRange(a)}`;
|
|
132
|
+
const link = a.blobUrl ? ` [↗](${a.blobUrl})` : "";
|
|
133
|
+
const title = a.title ? ` — ${a.title}` : "";
|
|
134
|
+
const lines = [`- \`${a.id}\`${link} \`${loc}\` [${a.level}]${title}`];
|
|
135
|
+
if (a.message.trim() !== "")
|
|
136
|
+
lines.push(blockquote(a.message));
|
|
137
|
+
if (a.rawDetails !== undefined && a.rawDetails.trim() !== "")
|
|
138
|
+
lines.push(blockquote(a.rawDetails));
|
|
139
|
+
return lines.join("\n");
|
|
140
|
+
}
|
|
141
|
+
function renderAnnotationRange(a) {
|
|
142
|
+
if (a.startLine === null && a.endLine === null)
|
|
143
|
+
return "?";
|
|
144
|
+
const start = a.startLine ?? a.endLine;
|
|
145
|
+
const end = a.endLine ?? a.startLine;
|
|
146
|
+
if (start === end)
|
|
147
|
+
return String(start);
|
|
148
|
+
return `${start}-${end}`;
|
|
121
149
|
}
|
package/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -111,6 +111,10 @@ export function projectIterateLean(result, opts) {
|
|
|
111
111
|
...(result.escalate.changesRequestedReviews.length > 0 && {
|
|
112
112
|
changesRequestedReviews: result.escalate.changesRequestedReviews,
|
|
113
113
|
}),
|
|
114
|
+
...(result.escalate.stalledChecks &&
|
|
115
|
+
result.escalate.stalledChecks.length > 0 && {
|
|
116
|
+
stalledChecks: result.escalate.stalledChecks,
|
|
117
|
+
}),
|
|
114
118
|
...(result.escalate.thrashHistory &&
|
|
115
119
|
result.escalate.thrashHistory.length > 0 && {
|
|
116
120
|
thrashHistory: result.escalate.thrashHistory,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { renderLineRange, renderSuggestionBlock } from "./suggestion-renderer.mjs";
|
|
2
|
+
import { threadComments } from "../threads/transcript.mjs";
|
|
2
3
|
const BODY_PREVIEW_MAX = 100;
|
|
3
4
|
export function renderAuthor(author, authorType) {
|
|
4
5
|
return authorType ? `@${author} · ${authorType}` : `@${author}`;
|
|
@@ -27,12 +28,60 @@ export function renderThreadBullet(t, opts = {}) {
|
|
|
27
28
|
: "`(no location)`";
|
|
28
29
|
const suggestionMarker = t.suggestion ? " [suggestion]" : "";
|
|
29
30
|
const statusSuffix = opts.statusTag ? ` ${opts.statusTag}` : "";
|
|
30
|
-
const
|
|
31
|
-
|
|
31
|
+
const bulletLine = `- \`threadId=${t.id}\`${link} ${loc} (${renderAuthor(t.author, t.authorType)})${suggestionMarker}${statusSuffix}`;
|
|
32
|
+
if (!opts.noBody && (!t.comments || t.comments.length === 0)) {
|
|
33
|
+
const legacyLine = `${bulletLine}: ${renderBodyPreview(t.body)}`;
|
|
34
|
+
return t.suggestion && opts.renderSuggestion
|
|
35
|
+
? `${legacyLine}\n${renderSuggestionBlock(t.suggestion)}`
|
|
36
|
+
: legacyLine;
|
|
37
|
+
}
|
|
38
|
+
const parts = [bulletLine];
|
|
39
|
+
if (!opts.noBody) {
|
|
40
|
+
parts.push(renderThreadCommentBullets(t));
|
|
41
|
+
}
|
|
32
42
|
if (t.suggestion && opts.renderSuggestion) {
|
|
33
|
-
|
|
43
|
+
parts.push(renderSuggestionBlock(t.suggestion));
|
|
34
44
|
}
|
|
35
|
-
return
|
|
45
|
+
return parts.join("\n");
|
|
46
|
+
}
|
|
47
|
+
export function renderThreadConversation(t) {
|
|
48
|
+
if (!t.comments || t.comments.length === 0)
|
|
49
|
+
return blockquote(t.body);
|
|
50
|
+
return threadComments(t)
|
|
51
|
+
.map((c) => {
|
|
52
|
+
const heading = c.id
|
|
53
|
+
? c.url
|
|
54
|
+
? `#### [commentId=${c.id}](${c.url}) (${renderAuthor(c.author, c.authorType)})`
|
|
55
|
+
: `#### \`commentId=${c.id}\` (${renderAuthor(c.author, c.authorType)})`
|
|
56
|
+
: `#### (${renderAuthor(c.author, c.authorType)})`;
|
|
57
|
+
return `${heading}\n\n${blockquote(c.body)}`;
|
|
58
|
+
})
|
|
59
|
+
.join("\n\n");
|
|
60
|
+
}
|
|
61
|
+
export function blockquote(body) {
|
|
62
|
+
return body
|
|
63
|
+
.replace(/\r\n/g, "\n")
|
|
64
|
+
.split("\n")
|
|
65
|
+
.map((line) => (line === "" ? ">" : `> ${line}`))
|
|
66
|
+
.join("\n");
|
|
67
|
+
}
|
|
68
|
+
function renderThreadCommentBullets(t) {
|
|
69
|
+
return threadComments(t)
|
|
70
|
+
.map((c) => {
|
|
71
|
+
const link = c.url ? ` [↗](${c.url})` : "";
|
|
72
|
+
const id = c.id ? `\`commentId=${c.id}\`` : "comment";
|
|
73
|
+
return [
|
|
74
|
+
` - ${id}${link} (${renderAuthor(c.author, c.authorType)})`,
|
|
75
|
+
indentBlockquote(c.body, " "),
|
|
76
|
+
].join("\n");
|
|
77
|
+
})
|
|
78
|
+
.join("\n");
|
|
79
|
+
}
|
|
80
|
+
function indentBlockquote(body, indent) {
|
|
81
|
+
return blockquote(body)
|
|
82
|
+
.split("\n")
|
|
83
|
+
.map((line) => `${indent}${line}`)
|
|
84
|
+
.join("\n");
|
|
36
85
|
}
|
|
37
86
|
export function renderCommentBullet(c, opts = {}) {
|
|
38
87
|
const link = c.url ? ` [↗](${c.url})` : "";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { fetchCheckRunAnnotations } from "../github/check-annotations.mjs";
|
|
2
|
+
export async function attachUnseenCheckAnnotations(checks, seenMap, prNumber) {
|
|
3
|
+
const checksWithAnnotations = [];
|
|
4
|
+
for (const check of checks) {
|
|
5
|
+
// eslint-disable-next-line no-await-in-loop
|
|
6
|
+
checksWithAnnotations.push(await attachForCheck(check, seenMap, prNumber));
|
|
7
|
+
}
|
|
8
|
+
return checksWithAnnotations;
|
|
9
|
+
}
|
|
10
|
+
async function attachForCheck(check, seenMap, prNumber) {
|
|
11
|
+
if (check.id == null)
|
|
12
|
+
return check;
|
|
13
|
+
let annotations;
|
|
14
|
+
try {
|
|
15
|
+
annotations = await fetchCheckRunAnnotations(check.id);
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
19
|
+
process.stderr.write(`pr-shepherd: annotation fetch failed for PR #${prNumber} check "${check.name}" (ignored): ${msg}\n`);
|
|
20
|
+
return check;
|
|
21
|
+
}
|
|
22
|
+
const unseen = annotations.filter((a) => !seenMap.has(a.id));
|
|
23
|
+
if (unseen.length === 0)
|
|
24
|
+
return check;
|
|
25
|
+
return { ...check, annotations: unseen };
|
|
26
|
+
}
|
|
27
|
+
export function annotationMarkerBody(a) {
|
|
28
|
+
return JSON.stringify({
|
|
29
|
+
path: a.path,
|
|
30
|
+
startLine: a.startLine,
|
|
31
|
+
endLine: a.endLine,
|
|
32
|
+
startColumn: a.startColumn,
|
|
33
|
+
endColumn: a.endColumn,
|
|
34
|
+
level: a.level,
|
|
35
|
+
title: a.title,
|
|
36
|
+
message: a.message,
|
|
37
|
+
rawDetails: a.rawDetails,
|
|
38
|
+
blobUrl: a.blobUrl,
|
|
39
|
+
});
|
|
40
|
+
}
|
package/bin/commands/check.mjs
CHANGED
|
@@ -9,9 +9,11 @@ import { deriveMergeStatus } from "../merge-status/derive.mjs";
|
|
|
9
9
|
import { loadConfig } from "../config/load.mjs";
|
|
10
10
|
import { classifyVisibleComments } from "../comments/visible-comments.mjs";
|
|
11
11
|
import { computeStatus } from "./check-status.mjs";
|
|
12
|
+
import { attachUnseenCheckAnnotations } from "./check-annotations.mjs";
|
|
12
13
|
import { buildTerminalReport } from "./check-terminal-report.mjs";
|
|
13
14
|
import { isBlockedByFilteredCheck, refreshReadyMergeability, refreshUnknownMergeability, } from "./ready-mergeability.mjs";
|
|
14
15
|
import { loadSeenMap, markSeen, classifyItem } from "../state/seen-comments.mjs";
|
|
16
|
+
import { threadTranscriptBody } from "../threads/transcript.mjs";
|
|
15
17
|
export async function runCheck(opts) {
|
|
16
18
|
const repo = await getRepoInfo();
|
|
17
19
|
const prNumber = opts.prNumber ?? (await getCurrentPrNumber());
|
|
@@ -38,8 +40,10 @@ export async function runCheck(opts) {
|
|
|
38
40
|
const inProgress = classifiedChecks.filter((c) => c.category === "in_progress");
|
|
39
41
|
const skipped = classifiedChecks.filter((c) => c.category === "skipped");
|
|
40
42
|
const filtered = classifiedChecks.filter((c) => c.category === "filtered");
|
|
41
|
-
const
|
|
43
|
+
const triagedBase = failing.length > 0 && !opts.skipTriage ? await triageFailingChecks(failing, repo) : failing;
|
|
42
44
|
const stateKey = { owner: repo.owner, repo: repo.name, pr: prNumber };
|
|
45
|
+
const seenMap = await loadSeenMap(stateKey);
|
|
46
|
+
const triaged = await attachUnseenCheckAnnotations(triagedBase, seenMap, prNumber);
|
|
43
47
|
const unresolvedThreads = batchData.reviewThreads.filter((t) => !t.isResolved);
|
|
44
48
|
const outdated = getOutdatedThreads(unresolvedThreads);
|
|
45
49
|
let autoResolved = [];
|
|
@@ -55,12 +59,11 @@ export async function runCheck(opts) {
|
|
|
55
59
|
const resolvedCandidates = batchData.reviewThreads.filter((t) => t.isResolved && !t.isOutdated);
|
|
56
60
|
const minimizedThreadCandidates = batchData.reviewThreads.filter((t) => t.isMinimized && !t.isResolved && !t.isOutdated);
|
|
57
61
|
const minimizedCommentCandidates = batchData.comments.filter((c) => c.isMinimized);
|
|
58
|
-
const seenMap = await loadSeenMap(stateKey);
|
|
59
62
|
const visibleCommentClassification = classifyVisibleComments(batchData.comments, seenMap, config.iterate.minimizeComments);
|
|
60
63
|
const autoResolvedIds = new Set(autoResolved.map((t) => t.id));
|
|
61
64
|
const firstLookThreads = [
|
|
62
65
|
...outdatedCandidates.flatMap((t) => {
|
|
63
|
-
const cls = classifyItem(t.id, t
|
|
66
|
+
const cls = classifyItem(t.id, threadTranscriptBody(t), seenMap);
|
|
64
67
|
if (cls === "unchanged")
|
|
65
68
|
return [];
|
|
66
69
|
const base = {
|
|
@@ -71,14 +74,14 @@ export async function runCheck(opts) {
|
|
|
71
74
|
return cls === "edited" ? [{ ...base, edited: true }] : [base];
|
|
72
75
|
}),
|
|
73
76
|
...resolvedCandidates.flatMap((t) => {
|
|
74
|
-
const cls = classifyItem(t.id, t
|
|
77
|
+
const cls = classifyItem(t.id, threadTranscriptBody(t), seenMap);
|
|
75
78
|
if (cls === "unchanged")
|
|
76
79
|
return [];
|
|
77
80
|
const base = { ...t, firstLookStatus: "resolved" };
|
|
78
81
|
return cls === "edited" ? [{ ...base, edited: true }] : [base];
|
|
79
82
|
}),
|
|
80
83
|
...minimizedThreadCandidates.flatMap((t) => {
|
|
81
|
-
const cls = classifyItem(t.id, t
|
|
84
|
+
const cls = classifyItem(t.id, threadTranscriptBody(t), seenMap);
|
|
82
85
|
if (cls === "unchanged")
|
|
83
86
|
return [];
|
|
84
87
|
const base = { ...t, firstLookStatus: "minimized" };
|
|
@@ -105,7 +108,7 @@ export async function runCheck(opts) {
|
|
|
105
108
|
seenSummaries.push(r);
|
|
106
109
|
}
|
|
107
110
|
await Promise.allSettled([
|
|
108
|
-
...firstLookThreads.map((t) => markSeen(stateKey, t.id, t
|
|
111
|
+
...firstLookThreads.map((t) => markSeen(stateKey, t.id, threadTranscriptBody(t))),
|
|
109
112
|
...firstLookComments.map((c) => markSeen(stateKey, c.id, c.body)),
|
|
110
113
|
...visibleCommentClassification.toMarkSeen.map((c) => markSeen(stateKey, c.id, c.body)),
|
|
111
114
|
...[...firstLookSummaries, ...editedSummaries].map((r) => markSeen(stateKey, r.id, r.body)),
|
|
@@ -9,6 +9,9 @@ vi.mock("../checks/triage.mts", () => ({
|
|
|
9
9
|
triageFailingChecks: vi.fn((checks) => Promise.resolve(checks)),
|
|
10
10
|
fetchStartupFailureChecks: vi.fn().mockResolvedValue([]),
|
|
11
11
|
}));
|
|
12
|
+
vi.mock("../github/check-annotations.mts", () => ({
|
|
13
|
+
fetchCheckRunAnnotations: vi.fn().mockResolvedValue([]),
|
|
14
|
+
}));
|
|
12
15
|
vi.mock("../comments/resolve.mts", () => ({
|
|
13
16
|
autoResolveOutdated: vi.fn().mockResolvedValue({ resolved: [], errors: [] }),
|
|
14
17
|
}));
|
|
@@ -26,6 +29,7 @@ import { runCheck } from "./check.mjs";
|
|
|
26
29
|
import { fetchPrBatch } from "../github/batch.mjs";
|
|
27
30
|
import { getCurrentPrNumber, getMergeableState } from "../github/client.mjs";
|
|
28
31
|
import { fetchStartupFailureChecks, triageFailingChecks } from "../checks/triage.mjs";
|
|
32
|
+
import { fetchCheckRunAnnotations } from "../github/check-annotations.mjs";
|
|
29
33
|
import { loadSeenMap, markSeen, hashBody } from "../state/seen-comments.mjs";
|
|
30
34
|
import { autoResolveOutdated } from "../comments/resolve.mjs";
|
|
31
35
|
const mockFetchPrBatch = vi.mocked(fetchPrBatch);
|
|
@@ -33,6 +37,7 @@ const mockGetCurrentPrNumber = vi.mocked(getCurrentPrNumber);
|
|
|
33
37
|
const mockGetMergeableState = vi.mocked(getMergeableState);
|
|
34
38
|
const mockTriageFailingChecks = vi.mocked(triageFailingChecks);
|
|
35
39
|
const mockFetchStartupFailureChecks = vi.mocked(fetchStartupFailureChecks);
|
|
40
|
+
const mockFetchCheckRunAnnotations = vi.mocked(fetchCheckRunAnnotations);
|
|
36
41
|
const mockLoadSeenMap = vi.mocked(loadSeenMap);
|
|
37
42
|
const mockMarkSeen = vi.mocked(markSeen);
|
|
38
43
|
const mockAutoResolveOutdated = vi.mocked(autoResolveOutdated);
|
|
@@ -41,7 +46,7 @@ function defaultConfig() {
|
|
|
41
46
|
return {
|
|
42
47
|
iterate: {
|
|
43
48
|
fixAttemptsPerThread: 3,
|
|
44
|
-
stallTimeoutMinutes:
|
|
49
|
+
stallTimeoutMinutes: 60,
|
|
45
50
|
minimizeApprovals: false,
|
|
46
51
|
minimizeComments: "all",
|
|
47
52
|
},
|
|
@@ -63,6 +68,7 @@ function defaultConfig() {
|
|
|
63
68
|
}
|
|
64
69
|
function makeCheck(overrides = {}) {
|
|
65
70
|
return {
|
|
71
|
+
id: null,
|
|
66
72
|
name: "tests",
|
|
67
73
|
status: "COMPLETED",
|
|
68
74
|
conclusion: "SUCCESS",
|
|
@@ -135,6 +141,7 @@ export function registerHooks() {
|
|
|
135
141
|
mockFetchPrBatch.mockResolvedValue({ data: makeBatchData() });
|
|
136
142
|
mockGetMergeableState.mockResolvedValue({ mergeable: "MERGEABLE", mergeStateStatus: "CLEAN" });
|
|
137
143
|
mockFetchStartupFailureChecks.mockResolvedValue([]);
|
|
144
|
+
mockFetchCheckRunAnnotations.mockResolvedValue([]);
|
|
138
145
|
});
|
|
139
146
|
}
|
|
140
|
-
export { BASE_OPTS, autoResolveOutdated, defaultConfig, fetchPrBatch, fetchStartupFailureChecks, getCurrentPrNumber, getMergeableState, hashBody, loadSeenMap, makeBatchData, makeCheck, makeComment, makeThread, markSeen, mockAutoResolveOutdated, mockFetchPrBatch, mockFetchStartupFailureChecks, mockGetCurrentPrNumber, mockGetMergeableState, mockLoadConfig, mockLoadSeenMap, mockMarkSeen, mockTriageFailingChecks, runCheck, triageFailingChecks, };
|
|
147
|
+
export { BASE_OPTS, autoResolveOutdated, defaultConfig, fetchPrBatch, fetchStartupFailureChecks, fetchCheckRunAnnotations, getCurrentPrNumber, getMergeableState, hashBody, loadSeenMap, makeBatchData, makeCheck, makeComment, makeThread, markSeen, mockAutoResolveOutdated, mockFetchPrBatch, mockFetchStartupFailureChecks, mockFetchCheckRunAnnotations, mockGetCurrentPrNumber, mockGetMergeableState, mockLoadConfig, mockLoadSeenMap, mockMarkSeen, mockTriageFailingChecks, runCheck, triageFailingChecks, };
|
|
@@ -52,11 +52,25 @@ export function buildEscalateHumanMessage(escalate, pr) {
|
|
|
52
52
|
lines.push(escalate.suggestion);
|
|
53
53
|
const hasItems = escalate.unresolvedThreads.length > 0 ||
|
|
54
54
|
escalate.changesRequestedReviews.length > 0 ||
|
|
55
|
-
escalate.ambiguousComments.length > 0
|
|
55
|
+
escalate.ambiguousComments.length > 0 ||
|
|
56
|
+
(escalate.stalledChecks?.length ?? 0) > 0;
|
|
56
57
|
if (hasItems) {
|
|
57
58
|
lines.push("");
|
|
58
59
|
lines.push("## Items needing attention");
|
|
59
60
|
lines.push("");
|
|
61
|
+
for (const c of escalate.stalledChecks ?? []) {
|
|
62
|
+
const target = c.runId
|
|
63
|
+
? `run \`${c.runId}\``
|
|
64
|
+
: c.detailsUrl
|
|
65
|
+
? `external \`${c.detailsUrl}\``
|
|
66
|
+
: "no run ID";
|
|
67
|
+
const ageMinutes = Math.floor(c.ageSeconds / 60);
|
|
68
|
+
lines.push(`- check \`${c.name}\` — ${c.status} ${c.source}, ${target}, waiting ${ageMinutes} minute${ageMinutes === 1 ? "" : "s"}`);
|
|
69
|
+
if (c.summary)
|
|
70
|
+
lines.push(` > ${c.summary}`);
|
|
71
|
+
}
|
|
72
|
+
if ((escalate.stalledChecks?.length ?? 0) > 0)
|
|
73
|
+
lines.push("");
|
|
60
74
|
for (const t of escalate.unresolvedThreads) {
|
|
61
75
|
const loc = t.path ? `\`${t.path}:${t.line ?? "?"}\`` : "(no location)";
|
|
62
76
|
lines.push(`- thread \`${t.id}\` — ${loc} (@${t.author}):`);
|
|
@@ -96,7 +110,7 @@ export function buildEscalateHumanMessage(escalate, pr) {
|
|
|
96
110
|
}
|
|
97
111
|
export function buildEscalateSuggestion(triggers, detail) {
|
|
98
112
|
if (triggers.includes("stall-timeout")) {
|
|
99
|
-
const mins = detail ?? "
|
|
113
|
+
const mins = detail ?? "60";
|
|
100
114
|
return `No progress detected for ${mins} minute${parseInt(mins, 10) === 1 ? "" : "s"} — state has not changed. This is a manual checkpoint: inspect the PR and apply a manual fix before resuming.`;
|
|
101
115
|
}
|
|
102
116
|
if (triggers.includes("base-branch-unknown")) {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
import { readFixAttempts, writeFixAttempts } from "../../state/fix-attempts.mjs";
|
|
3
3
|
import { toAgentThread, toAgentComment, toAgentChecks } from "../../reporters/agent.mjs";
|
|
4
|
+
import { markSeen } from "../../state/seen-comments.mjs";
|
|
4
5
|
import { checkEscalateTriggers, validateBaseBranch, buildEscalateSuggestion, buildEscalateHumanMessage, } from "./escalate.mjs";
|
|
5
6
|
import { buildResolveCommand } from "./classify.mjs";
|
|
6
7
|
import { buildFixInstructions } from "./render.mjs";
|
|
7
8
|
import { applyStallGuard } from "./stall.mjs";
|
|
8
9
|
import { tryCancelRun, buildInProgressRunIds } from "./helpers.mjs";
|
|
10
|
+
import { annotationMarkerBody } from "../check-annotations.mjs";
|
|
9
11
|
export async function handleFixCode(ctx) {
|
|
10
12
|
const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, } = ctx;
|
|
11
13
|
const failingChecks = report.checks.failing;
|
|
@@ -101,7 +103,7 @@ export async function handleFixCode(ctx) {
|
|
|
101
103
|
const firstLookThreads = report.threads.firstLook;
|
|
102
104
|
const firstLookComments = report.comments.firstLook;
|
|
103
105
|
const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prNumber, cancelled.length, firstLookThreads, firstLookComments, firstLookSummaries, editedSummaries, inProgressRunIds, resolutionOnlyThreads);
|
|
104
|
-
|
|
106
|
+
const prospectiveResult = {
|
|
105
107
|
...base,
|
|
106
108
|
baseBranch: baseLookup.branch,
|
|
107
109
|
action: "fix_code",
|
|
@@ -122,5 +124,10 @@ export async function handleFixCode(ctx) {
|
|
|
122
124
|
inProgressRunIds,
|
|
123
125
|
},
|
|
124
126
|
cancelled,
|
|
125
|
-
}
|
|
127
|
+
};
|
|
128
|
+
const result = await applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, prospectiveResult, report, reviewSummaryIds);
|
|
129
|
+
if (result.action === "fix_code") {
|
|
130
|
+
await Promise.allSettled(result.fix.checks.flatMap((ch) => (ch.annotations ?? []).map((a) => markSeen(stallKey, a.id, annotationMarkerBody(a)))));
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
126
133
|
}
|
|
@@ -53,6 +53,7 @@ export function buildRelevantChecks(report) {
|
|
|
53
53
|
...(c.jobName !== undefined && { jobName: c.jobName }),
|
|
54
54
|
...(c.failedStep !== undefined && { failedStep: c.failedStep }),
|
|
55
55
|
...(c.summary !== undefined && { summary: c.summary }),
|
|
56
|
+
...(c.annotations !== undefined && { annotations: c.annotations }),
|
|
56
57
|
},
|
|
57
58
|
];
|
|
58
59
|
});
|
|
@@ -30,6 +30,9 @@ export function buildFixInstructions(threads, actionableComments, checks, change
|
|
|
30
30
|
actionableSections.push("`## Actionable comments`");
|
|
31
31
|
if (checks.length > 0)
|
|
32
32
|
actionableSections.push("`## Failing checks`");
|
|
33
|
+
if (checks.some((c) => (c.annotations?.length ?? 0) > 0)) {
|
|
34
|
+
actionableSections.push("`## Check annotations`");
|
|
35
|
+
}
|
|
33
36
|
if (changesRequestedReviews.length > 0)
|
|
34
37
|
actionableSections.push("`## Changes-requested reviews`");
|
|
35
38
|
const sectionRef = actionableSections.length > 0 ? `under ${actionableSections.join(", ")}` : "above";
|
|
@@ -65,6 +68,9 @@ export function buildFixInstructions(threads, actionableComments, checks, change
|
|
|
65
68
|
instructions.push(`Resolve the threads under \`## Review threads to resolve\` with the \`resolve:\` command shown below. These threads are already outdated or minimized, so no code edit is required for them unless their body reveals separate work you choose to do.`);
|
|
66
69
|
}
|
|
67
70
|
instructions.push(...buildFailingCheckInstructions(checks));
|
|
71
|
+
if (checks.some((c) => (c.annotations?.length ?? 0) > 0)) {
|
|
72
|
+
instructions.push(`For each item under \`## Check annotations\`: inspect the referenced file range and decide whether the annotation requires a code change. These annotations are surfaced once per PR and do not need any resolve/minimize mutation.`);
|
|
73
|
+
}
|
|
68
74
|
if (changesRequestedReviews.length > 0) {
|
|
69
75
|
instructions.push(`For each bullet under \`## Changes-requested reviews\` above: read the review body and apply the requested changes.`);
|
|
70
76
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readStallState, writeStallState } from "../../state/iterate-stall.mjs";
|
|
2
|
-
import { toAgentThread, toAgentComment } from "../../reporters/agent.mjs";
|
|
2
|
+
import { toAgentThread, toAgentComment, toAgentStalledCheck } from "../../reporters/agent.mjs";
|
|
3
3
|
import { buildEscalateSuggestion, buildEscalateHumanMessage } from "./escalate.mjs";
|
|
4
4
|
export function computeStallFingerprint(action, headSha, base, report, reviewSummaryIds) {
|
|
5
5
|
const checks = [
|
|
@@ -27,8 +27,31 @@ export function computeStallFingerprint(action, headSha, base, report, reviewSum
|
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
29
|
export async function applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, prospectiveResult, report, reviewSummaryIds) {
|
|
30
|
-
const fingerprint = computeStallFingerprint(prospectiveResult.action, headSha, base, report, reviewSummaryIds);
|
|
31
30
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
31
|
+
const stalledChecks = findCiStartStalledChecks(report.checks.inProgress, nowSeconds, {
|
|
32
|
+
stallTimeoutSeconds,
|
|
33
|
+
action: prospectiveResult.action,
|
|
34
|
+
});
|
|
35
|
+
if (stalledChecks.length > 0) {
|
|
36
|
+
const stalledMinutes = Math.floor(Math.max(...stalledChecks.map((c) => c.ageSeconds)) / 60);
|
|
37
|
+
const escalateBase = {
|
|
38
|
+
triggers: ["stall-timeout"],
|
|
39
|
+
unresolvedThreads: [],
|
|
40
|
+
ambiguousComments: [],
|
|
41
|
+
changesRequestedReviews: [],
|
|
42
|
+
stalledChecks,
|
|
43
|
+
suggestion: buildEscalateSuggestion(["stall-timeout"], String(stalledMinutes)),
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
...base,
|
|
47
|
+
action: "escalate",
|
|
48
|
+
escalate: {
|
|
49
|
+
...escalateBase,
|
|
50
|
+
humanMessage: buildEscalateHumanMessage(escalateBase, prNumber),
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const fingerprint = computeStallFingerprint(prospectiveResult.action, headSha, base, report, reviewSummaryIds);
|
|
32
55
|
const stored = await readStallState(stallKey);
|
|
33
56
|
if (stored && stored.fingerprint === fingerprint) {
|
|
34
57
|
const ageSeconds = nowSeconds - stored.firstSeenAt;
|
|
@@ -65,3 +88,21 @@ export async function applyStallGuard(stallKey, stallTimeoutSeconds, headSha, ba
|
|
|
65
88
|
await writeStallState(stallKey, { fingerprint, firstSeenAt: nowSeconds });
|
|
66
89
|
return prospectiveResult;
|
|
67
90
|
}
|
|
91
|
+
function findCiStartStalledChecks(checks, nowSeconds, opts) {
|
|
92
|
+
if (opts.stallTimeoutSeconds <= 0 || opts.action !== "wait")
|
|
93
|
+
return [];
|
|
94
|
+
return checks
|
|
95
|
+
.filter((c) => isUnstartedCheck(c))
|
|
96
|
+
.map((c) => toAgentStalledCheck(c, nowSeconds))
|
|
97
|
+
.filter((c) => c.createdAtUnix !== undefined && c.ageSeconds >= opts.stallTimeoutSeconds);
|
|
98
|
+
}
|
|
99
|
+
function isUnstartedCheck(check) {
|
|
100
|
+
if (check.source === "status_context")
|
|
101
|
+
return true;
|
|
102
|
+
if (check.startedAtUnix !== undefined)
|
|
103
|
+
return false;
|
|
104
|
+
return (check.status === "PENDING" ||
|
|
105
|
+
check.status === "QUEUED" ||
|
|
106
|
+
check.status === "REQUESTED" ||
|
|
107
|
+
check.status === "WAITING");
|
|
108
|
+
}
|
|
@@ -23,6 +23,9 @@ vi.mock("../state/iterate-stall.mts", () => ({
|
|
|
23
23
|
readStallState: vi.fn().mockResolvedValue(null),
|
|
24
24
|
writeStallState: vi.fn().mockResolvedValue(undefined),
|
|
25
25
|
}));
|
|
26
|
+
vi.mock("../state/seen-comments.mts", () => ({
|
|
27
|
+
markSeen: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
}));
|
|
26
29
|
const { mockLoadConfig } = vi.hoisted(() => ({ mockLoadConfig: vi.fn() }));
|
|
27
30
|
vi.mock("../config/load.mts", () => ({ loadConfig: mockLoadConfig }));
|
|
28
31
|
import { runIterate } from "./iterate/index.mjs";
|
|
@@ -30,12 +33,14 @@ import { runCheck } from "./check.mjs";
|
|
|
30
33
|
import { updateReadyDelay } from "./ready-delay.mjs";
|
|
31
34
|
import { readFixAttempts, writeFixAttempts } from "../state/fix-attempts.mjs";
|
|
32
35
|
import { readStallState, writeStallState } from "../state/iterate-stall.mjs";
|
|
36
|
+
import { markSeen } from "../state/seen-comments.mjs";
|
|
33
37
|
const mockRunCheck = vi.mocked(runCheck);
|
|
34
38
|
const mockUpdateReadyDelay = vi.mocked(updateReadyDelay);
|
|
35
39
|
const mockReadFixAttempts = vi.mocked(readFixAttempts);
|
|
36
40
|
const mockWriteFixAttempts = vi.mocked(writeFixAttempts);
|
|
37
41
|
const mockReadStallState = vi.mocked(readStallState);
|
|
38
42
|
const mockWriteStallState = vi.mocked(writeStallState);
|
|
43
|
+
const mockMarkSeen = vi.mocked(markSeen);
|
|
39
44
|
function makeReport(overrides = {}) {
|
|
40
45
|
return {
|
|
41
46
|
pr: 42,
|
|
@@ -115,4 +120,4 @@ export function registerHooks() {
|
|
|
115
120
|
vi.restoreAllMocks();
|
|
116
121
|
});
|
|
117
122
|
}
|
|
118
|
-
export { makeOpts, makeReport, mockExecFile, mockFetch, mockLoadConfig, mockReadFixAttempts, mockReadStallState, mockRunCheck, mockUpdateReadyDelay, mockWriteFixAttempts, mockWriteStallState, readFixAttempts, readStallState, runCheck, runIterate, updateReadyDelay, writeFixAttempts, writeStallState, };
|
|
123
|
+
export { makeOpts, makeReport, mockExecFile, mockFetch, mockLoadConfig, mockReadFixAttempts, mockReadStallState, mockMarkSeen, mockRunCheck, mockUpdateReadyDelay, mockWriteFixAttempts, mockWriteStallState, readFixAttempts, readStallState, runCheck, runIterate, updateReadyDelay, writeFixAttempts, writeStallState, };
|
package/bin/commands/resolve.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { classifyVisibleComments } from "../comments/visible-comments.mjs";
|
|
|
7
7
|
import { extractSuggestion } from "../suggestions/extract.mjs";
|
|
8
8
|
import { buildFetchInstructions } from "./resolve-instructions.mjs";
|
|
9
9
|
import { loadSeenMap, markSeen, classifyItem } from "../state/seen-comments.mjs";
|
|
10
|
+
import { threadTranscriptBody } from "../threads/transcript.mjs";
|
|
10
11
|
export { runResolveMutate } from "./resolve-mutate.mjs";
|
|
11
12
|
export async function runResolveFetch(opts) {
|
|
12
13
|
const repo = await getRepoInfo();
|
|
@@ -25,7 +26,7 @@ export async function runResolveFetch(opts) {
|
|
|
25
26
|
const unseenOutdated = [];
|
|
26
27
|
const editedOutdated = [];
|
|
27
28
|
for (const t of outdatedCandidates) {
|
|
28
|
-
const cls = classifyItem(t.id, t
|
|
29
|
+
const cls = classifyItem(t.id, threadTranscriptBody(t), seenMap);
|
|
29
30
|
if (cls === "new")
|
|
30
31
|
unseenOutdated.push(t);
|
|
31
32
|
else if (cls === "edited")
|
|
@@ -34,7 +35,7 @@ export async function runResolveFetch(opts) {
|
|
|
34
35
|
const unseenResolved = [];
|
|
35
36
|
const editedResolved = [];
|
|
36
37
|
for (const t of resolvedCandidates) {
|
|
37
|
-
const cls = classifyItem(t.id, t
|
|
38
|
+
const cls = classifyItem(t.id, threadTranscriptBody(t), seenMap);
|
|
38
39
|
if (cls === "new")
|
|
39
40
|
unseenResolved.push(t);
|
|
40
41
|
else if (cls === "edited")
|
|
@@ -43,7 +44,7 @@ export async function runResolveFetch(opts) {
|
|
|
43
44
|
const unseenMinimizedThreads = [];
|
|
44
45
|
const editedMinimizedThreads = [];
|
|
45
46
|
for (const t of minimizedThreadCandidates) {
|
|
46
|
-
const cls = classifyItem(t.id, t
|
|
47
|
+
const cls = classifyItem(t.id, threadTranscriptBody(t), seenMap);
|
|
47
48
|
if (cls === "new")
|
|
48
49
|
unseenMinimizedThreads.push(t);
|
|
49
50
|
else if (cls === "edited")
|
|
@@ -114,7 +115,7 @@ export async function runResolveFetch(opts) {
|
|
|
114
115
|
];
|
|
115
116
|
// Mark new and edited items as seen (best-effort — markSeen never throws).
|
|
116
117
|
await Promise.allSettled([
|
|
117
|
-
...firstLookThreads.map((t) => markSeen(stateKey, t.id, t
|
|
118
|
+
...firstLookThreads.map((t) => markSeen(stateKey, t.id, threadTranscriptBody(t))),
|
|
118
119
|
...firstLookComments.map((c) => markSeen(stateKey, c.id, c.body)),
|
|
119
120
|
...visibleCommentClassification.toMarkSeen.map((c) => markSeen(stateKey, c.id, c.body)),
|
|
120
121
|
]);
|
package/bin/config.json
CHANGED
|
@@ -10,19 +10,29 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
10
10
|
}));
|
|
11
11
|
const reviewThreads = rawThreadPages.map((t) => {
|
|
12
12
|
const comment = t.comments.nodes[0];
|
|
13
|
+
const comments = t.comments.nodes.map((c) => ({
|
|
14
|
+
id: c.id,
|
|
15
|
+
isMinimized: c.isMinimized,
|
|
16
|
+
author: c.author?.login ?? "unknown",
|
|
17
|
+
authorType: mapAuthorType(c.author?.__typename),
|
|
18
|
+
body: c.body,
|
|
19
|
+
url: c.url,
|
|
20
|
+
createdAtUnix: parseCreatedAt(c.createdAt),
|
|
21
|
+
}));
|
|
13
22
|
return {
|
|
14
23
|
id: t.id,
|
|
15
24
|
isResolved: t.isResolved,
|
|
16
25
|
isOutdated: t.isOutdated,
|
|
17
26
|
isMinimized: comment?.isMinimized ?? false,
|
|
18
|
-
path: comment?.path ?? null,
|
|
19
|
-
line: comment?.line ?? null,
|
|
20
|
-
startLine: comment?.startLine ?? null,
|
|
27
|
+
path: t.path ?? comment?.path ?? null,
|
|
28
|
+
line: t.line ?? comment?.line ?? null,
|
|
29
|
+
startLine: t.startLine ?? comment?.startLine ?? null,
|
|
21
30
|
author: comment?.author?.login ?? "unknown",
|
|
22
31
|
authorType: mapAuthorType(comment?.author?.__typename),
|
|
23
32
|
body: comment?.body ?? "",
|
|
24
33
|
url: comment?.url ?? "",
|
|
25
34
|
createdAtUnix: comment ? parseCreatedAt(comment.createdAt) : 0,
|
|
35
|
+
comments,
|
|
26
36
|
};
|
|
27
37
|
});
|
|
28
38
|
const comments = rawCommentNodes.map((c) => ({
|
|
@@ -64,14 +74,28 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
64
74
|
const event = node.checkSuite?.workflowRun?.event ?? null;
|
|
65
75
|
const runId = extractRunId(node.detailsUrl);
|
|
66
76
|
const summary = extractCheckRunSummary(node.title, node.summary);
|
|
77
|
+
const rawCreatedAt = node.checkSuite
|
|
78
|
+
? (node.checkSuite.workflowRun?.createdAt ?? node.checkSuite.createdAt)
|
|
79
|
+
: undefined;
|
|
80
|
+
const rawUpdatedAt = node.checkSuite
|
|
81
|
+
? (node.checkSuite.workflowRun?.updatedAt ?? node.checkSuite.updatedAt)
|
|
82
|
+
: undefined;
|
|
83
|
+
const createdAtUnix = rawCreatedAt ? parseCreatedAt(rawCreatedAt) : undefined;
|
|
84
|
+
const startedAtUnix = node.startedAt ? parseCreatedAt(node.startedAt) : undefined;
|
|
85
|
+
const updatedAtUnix = rawUpdatedAt ? parseCreatedAt(rawUpdatedAt) : undefined;
|
|
67
86
|
return [
|
|
68
87
|
{
|
|
88
|
+
id: node.id,
|
|
69
89
|
name: node.name,
|
|
70
90
|
status: node.status,
|
|
71
91
|
conclusion: node.conclusion,
|
|
92
|
+
source: "check_run",
|
|
72
93
|
detailsUrl: node.detailsUrl ?? "",
|
|
73
94
|
event,
|
|
74
95
|
runId,
|
|
96
|
+
...(createdAtUnix !== undefined && { createdAtUnix }),
|
|
97
|
+
...(startedAtUnix !== undefined && { startedAtUnix }),
|
|
98
|
+
...(updatedAtUnix !== undefined && { updatedAtUnix }),
|
|
75
99
|
...(summary !== undefined && { summary }),
|
|
76
100
|
},
|
|
77
101
|
];
|
|
@@ -79,14 +103,17 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
79
103
|
if (node.__typename === "StatusContext") {
|
|
80
104
|
const { status, conclusion } = mapStatusContextState(node.state);
|
|
81
105
|
const summary = node.description?.trim() || undefined;
|
|
106
|
+
const createdAtUnix = node.createdAt ? parseCreatedAt(node.createdAt) : undefined;
|
|
82
107
|
return [
|
|
83
108
|
{
|
|
84
109
|
name: node.context,
|
|
85
110
|
status,
|
|
86
111
|
conclusion,
|
|
112
|
+
source: "status_context",
|
|
87
113
|
detailsUrl: node.targetUrl ?? "",
|
|
88
114
|
event: null,
|
|
89
115
|
runId: null,
|
|
116
|
+
...(createdAtUnix !== undefined && { createdAtUnix }),
|
|
90
117
|
...(summary !== undefined && { summary }),
|
|
91
118
|
},
|
|
92
119
|
];
|
package/bin/github/batch.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { graphql, graphqlWithRateLimit } from "./client.mjs";
|
|
2
2
|
import { paginateForward, paginateBackward } from "./pagination.mjs";
|
|
3
|
+
import { hydrateThreadCommentPages } from "./thread-comments.mjs";
|
|
3
4
|
import { BATCH_PR_QUERY } from "./queries.mjs";
|
|
4
5
|
import { parseRawPr } from "./batch-parsers.mjs";
|
|
5
6
|
/**
|
|
@@ -36,6 +37,7 @@ export async function fetchPrBatch(pr, repo, opts = {}) {
|
|
|
36
37
|
// extra contains pages before the first page.
|
|
37
38
|
rawThreadPages = [...extra, ...rawThreadPages];
|
|
38
39
|
}
|
|
40
|
+
rawThreadPages = await hydrateThreadCommentPages(rawThreadPages);
|
|
39
41
|
// Paginate comments backward if the first page is incomplete.
|
|
40
42
|
let rawCommentNodes = raw.comments.nodes;
|
|
41
43
|
if (raw.comments.pageInfo.hasPreviousPage && raw.comments.pageInfo.startCursor) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { graphql } from "./client.mjs";
|
|
3
|
+
import { CHECK_RUN_ANNOTATIONS_QUERY } from "./queries.mjs";
|
|
4
|
+
const ANNOTATIONS_PER_PAGE = 100;
|
|
5
|
+
const MAX_ANNOTATION_PAGES = 10;
|
|
6
|
+
export async function fetchCheckRunAnnotations(checkRunId) {
|
|
7
|
+
let cursor = null;
|
|
8
|
+
const nodes = [];
|
|
9
|
+
for (let page = 1; page <= MAX_ANNOTATION_PAGES; page++) {
|
|
10
|
+
// eslint-disable-next-line no-await-in-loop
|
|
11
|
+
const result = await fetchAnnotationPage(checkRunId, cursor);
|
|
12
|
+
nodes.push(...result.nodes);
|
|
13
|
+
if (!result.pageInfo.hasNextPage || !result.pageInfo.endCursor)
|
|
14
|
+
break;
|
|
15
|
+
if (page === MAX_ANNOTATION_PAGES) {
|
|
16
|
+
process.stderr.write(`pr-shepherd: annotation pagination cap (${MAX_ANNOTATION_PAGES * ANNOTATIONS_PER_PAGE} annotations) reached for check run ${checkRunId} — annotation output may be incomplete\n`);
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
cursor = result.pageInfo.endCursor;
|
|
20
|
+
}
|
|
21
|
+
return nodes.map((node) => toCheckAnnotation(checkRunId, node));
|
|
22
|
+
}
|
|
23
|
+
async function fetchAnnotationPage(checkRunId, cursor) {
|
|
24
|
+
const res = await graphql(CHECK_RUN_ANNOTATIONS_QUERY, {
|
|
25
|
+
id: checkRunId,
|
|
26
|
+
...(cursor ? { cursor } : {}),
|
|
27
|
+
});
|
|
28
|
+
const node = res.data.node;
|
|
29
|
+
if (node?.__typename !== "CheckRun" || node.annotations === undefined) {
|
|
30
|
+
return { pageInfo: { hasNextPage: false, endCursor: null }, nodes: [] };
|
|
31
|
+
}
|
|
32
|
+
return node.annotations;
|
|
33
|
+
}
|
|
34
|
+
function toCheckAnnotation(checkRunId, raw) {
|
|
35
|
+
const id = `check_annotation_${raw.fullDatabaseId ?? fallbackId(checkRunId, raw)}`;
|
|
36
|
+
const title = raw.title?.trim() || undefined;
|
|
37
|
+
const rawDetails = raw.rawDetails?.trim() || undefined;
|
|
38
|
+
const blobUrl = raw.blobUrl?.trim() || undefined;
|
|
39
|
+
return {
|
|
40
|
+
id,
|
|
41
|
+
path: raw.path,
|
|
42
|
+
startLine: raw.location?.start.line ?? null,
|
|
43
|
+
endLine: raw.location?.end.line ?? raw.location?.start.line ?? null,
|
|
44
|
+
...(raw.location?.start.column !== undefined && {
|
|
45
|
+
startColumn: raw.location.start.column,
|
|
46
|
+
}),
|
|
47
|
+
...(raw.location?.end.column !== undefined && {
|
|
48
|
+
endColumn: raw.location.end.column,
|
|
49
|
+
}),
|
|
50
|
+
level: raw.annotationLevel,
|
|
51
|
+
...(title !== undefined && { title }),
|
|
52
|
+
message: raw.message,
|
|
53
|
+
...(rawDetails !== undefined && { rawDetails }),
|
|
54
|
+
...(blobUrl !== undefined && { blobUrl }),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function fallbackId(checkRunId, raw) {
|
|
58
|
+
const start = raw.location?.start;
|
|
59
|
+
const end = raw.location?.end;
|
|
60
|
+
const parts = [
|
|
61
|
+
checkRunId,
|
|
62
|
+
raw.path,
|
|
63
|
+
raw.annotationLevel,
|
|
64
|
+
raw.title ?? "",
|
|
65
|
+
raw.message,
|
|
66
|
+
raw.rawDetails ?? "",
|
|
67
|
+
raw.blobUrl ?? "",
|
|
68
|
+
String(start?.line ?? ""),
|
|
69
|
+
String(start?.column ?? ""),
|
|
70
|
+
String(end?.line ?? ""),
|
|
71
|
+
String(end?.column ?? ""),
|
|
72
|
+
];
|
|
73
|
+
const input = parts.map((part) => `${part.length}:${part}`).join("|");
|
|
74
|
+
return createHash("sha256").update(input).digest("hex").slice(0, 24);
|
|
75
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { _resetTokenCache } from "./http.mjs";
|
|
3
|
+
export const mockFetch = vi.fn();
|
|
4
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
5
|
+
const { _mockExecFile } = vi.hoisted(() => ({ _mockExecFile: vi.fn() }));
|
|
6
|
+
export const mockExecFile = _mockExecFile;
|
|
7
|
+
vi.mock("node:child_process", () => ({
|
|
8
|
+
execFile: (cmd, args, optsOrCb, maybeCb) => {
|
|
9
|
+
const cb = typeof optsOrCb === "function" ? optsOrCb : maybeCb;
|
|
10
|
+
_mockExecFile(cmd, args)
|
|
11
|
+
.then((result) => cb(null, result))
|
|
12
|
+
.catch((err) => cb(err, { stdout: "", stderr: "" }));
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
export function gqlOk(data) {
|
|
16
|
+
return {
|
|
17
|
+
ok: true,
|
|
18
|
+
status: 200,
|
|
19
|
+
headers: new Headers({ "content-type": "application/json" }),
|
|
20
|
+
json: () => Promise.resolve({ data }),
|
|
21
|
+
text: () => Promise.resolve(JSON.stringify({ data })),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function restOk(data) {
|
|
25
|
+
return {
|
|
26
|
+
ok: true,
|
|
27
|
+
status: 200,
|
|
28
|
+
headers: new Headers({ "content-type": "application/json" }),
|
|
29
|
+
json: () => Promise.resolve(data),
|
|
30
|
+
text: () => Promise.resolve(JSON.stringify(data)),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function gqlErrors(errors) {
|
|
34
|
+
return {
|
|
35
|
+
ok: true,
|
|
36
|
+
status: 200,
|
|
37
|
+
headers: new Headers({ "content-type": "application/json" }),
|
|
38
|
+
json: () => Promise.resolve({ data: null, errors }),
|
|
39
|
+
text: () => Promise.resolve(JSON.stringify({ data: null, errors })),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function registerClientHooks() {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
mockFetch.mockReset();
|
|
45
|
+
mockExecFile.mockReset();
|
|
46
|
+
_resetTokenCache();
|
|
47
|
+
delete process.env["GITHUB_TOKEN"];
|
|
48
|
+
delete process.env["GITHUB_PERSONAL_ACCESS_TOKEN"];
|
|
49
|
+
process.env["GH_TOKEN"] = "test-token";
|
|
50
|
+
});
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
delete process.env["GH_TOKEN"];
|
|
53
|
+
_resetTokenCache();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -67,8 +67,14 @@ query BatchPr(
|
|
|
67
67
|
id
|
|
68
68
|
isResolved
|
|
69
69
|
isOutdated
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
path
|
|
71
|
+
line
|
|
72
|
+
startLine
|
|
73
|
+
comments(first: 100) {
|
|
74
|
+
pageInfo {
|
|
75
|
+
hasNextPage
|
|
76
|
+
endCursor
|
|
77
|
+
}
|
|
72
78
|
nodes {
|
|
73
79
|
id
|
|
74
80
|
isMinimized
|
|
@@ -164,21 +170,28 @@ query BatchPr(
|
|
|
164
170
|
nodes {
|
|
165
171
|
__typename
|
|
166
172
|
... on CheckRun {
|
|
173
|
+
id
|
|
167
174
|
name
|
|
168
175
|
status
|
|
169
176
|
conclusion
|
|
170
177
|
detailsUrl
|
|
178
|
+
startedAt
|
|
171
179
|
title
|
|
172
180
|
summary
|
|
173
181
|
checkSuite {
|
|
182
|
+
createdAt
|
|
183
|
+
updatedAt
|
|
174
184
|
workflowRun {
|
|
175
185
|
event
|
|
186
|
+
createdAt
|
|
187
|
+
updatedAt
|
|
176
188
|
}
|
|
177
189
|
}
|
|
178
190
|
}
|
|
179
191
|
... on StatusContext {
|
|
180
192
|
context
|
|
181
193
|
state
|
|
194
|
+
createdAt
|
|
182
195
|
targetUrl
|
|
183
196
|
description
|
|
184
197
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
query CheckRunAnnotations($id: ID!, $cursor: String) {
|
|
2
|
+
node(id: $id) {
|
|
3
|
+
__typename
|
|
4
|
+
... on CheckRun {
|
|
5
|
+
annotations(first: 100, after: $cursor) {
|
|
6
|
+
pageInfo {
|
|
7
|
+
hasNextPage
|
|
8
|
+
endCursor
|
|
9
|
+
}
|
|
10
|
+
nodes {
|
|
11
|
+
fullDatabaseId
|
|
12
|
+
path
|
|
13
|
+
annotationLevel
|
|
14
|
+
title
|
|
15
|
+
message
|
|
16
|
+
rawDetails
|
|
17
|
+
blobUrl
|
|
18
|
+
location {
|
|
19
|
+
start {
|
|
20
|
+
line
|
|
21
|
+
column
|
|
22
|
+
}
|
|
23
|
+
end {
|
|
24
|
+
line
|
|
25
|
+
column
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
query ReviewThreadComments($threadId: ID!, $commentsCursor: String) {
|
|
2
|
+
node(id: $threadId) {
|
|
3
|
+
__typename
|
|
4
|
+
... on PullRequestReviewThread {
|
|
5
|
+
comments(first: 100, after: $commentsCursor) {
|
|
6
|
+
pageInfo {
|
|
7
|
+
hasNextPage
|
|
8
|
+
endCursor
|
|
9
|
+
}
|
|
10
|
+
nodes {
|
|
11
|
+
id
|
|
12
|
+
isMinimized
|
|
13
|
+
url
|
|
14
|
+
author {
|
|
15
|
+
__typename
|
|
16
|
+
login
|
|
17
|
+
}
|
|
18
|
+
body
|
|
19
|
+
path
|
|
20
|
+
line
|
|
21
|
+
startLine
|
|
22
|
+
createdAt
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
package/bin/github/queries.mjs
CHANGED
|
@@ -11,6 +11,10 @@ import { join } from "node:path";
|
|
|
11
11
|
const gql = (name) => readFileSync(join(import.meta.dirname, "gql", name), "utf8");
|
|
12
12
|
/** The primary batch query that fetches CI + comments + merge status in one round-trip. */
|
|
13
13
|
export const BATCH_PR_QUERY = gql("batch-pr.gql");
|
|
14
|
+
/** Fetches additional comments for a single review thread when its nested connection paginates. */
|
|
15
|
+
export const REVIEW_THREAD_COMMENTS_QUERY = gql("review-thread-comments.gql");
|
|
16
|
+
/** Fetch inline annotations for a single CheckRun by node ID. */
|
|
17
|
+
export const CHECK_RUN_ANNOTATIONS_QUERY = gql("check-run-annotations.gql");
|
|
14
18
|
/** Returns the current head commit SHA for a PR. Used by waitForSha polling. */
|
|
15
19
|
export const GET_PR_HEAD_SHA_QUERY = gql("get-pr-head-sha.gql");
|
|
16
20
|
/** Look up PR number by branch name (for getCurrentPrNumber). */
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { graphql } from "./client.mjs";
|
|
2
|
+
import { paginateForward } from "./pagination.mjs";
|
|
3
|
+
import { REVIEW_THREAD_COMMENTS_QUERY } from "./queries.mjs";
|
|
4
|
+
export async function hydrateThreadCommentPages(threads) {
|
|
5
|
+
const hydrated = [];
|
|
6
|
+
for (const thread of threads) {
|
|
7
|
+
hydrated.push(await hydrateThreadCommentPage(thread));
|
|
8
|
+
}
|
|
9
|
+
return hydrated;
|
|
10
|
+
}
|
|
11
|
+
async function hydrateThreadCommentPage(thread) {
|
|
12
|
+
const pageInfo = thread.comments.pageInfo;
|
|
13
|
+
if (!pageInfo?.hasNextPage || !pageInfo.endCursor)
|
|
14
|
+
return thread;
|
|
15
|
+
const extra = await paginateForward(async (cursor) => {
|
|
16
|
+
const res = await graphql(REVIEW_THREAD_COMMENTS_QUERY, {
|
|
17
|
+
threadId: thread.id,
|
|
18
|
+
...(cursor ? { commentsCursor: cursor } : {}),
|
|
19
|
+
});
|
|
20
|
+
const node = res.data.node;
|
|
21
|
+
if (!node?.comments) {
|
|
22
|
+
const nodeType = node?.__typename ?? "null";
|
|
23
|
+
throw new Error(`Review thread ${thread.id} did not resolve to PullRequestReviewThread while paginating comments (node type: ${nodeType})`);
|
|
24
|
+
}
|
|
25
|
+
return node.comments;
|
|
26
|
+
}, pageInfo.endCursor);
|
|
27
|
+
return {
|
|
28
|
+
...thread,
|
|
29
|
+
comments: {
|
|
30
|
+
pageInfo: { hasNextPage: false, endCursor: null },
|
|
31
|
+
nodes: [...thread.comments.nodes, ...extra],
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
package/bin/reporters/agent.mjs
CHANGED
|
@@ -22,6 +22,15 @@ export function toAgentThread(t) {
|
|
|
22
22
|
...(t.authorType !== undefined && { authorType: t.authorType }),
|
|
23
23
|
body: t.body,
|
|
24
24
|
url: t.url,
|
|
25
|
+
...(t.comments !== undefined && {
|
|
26
|
+
comments: t.comments.map((c) => ({
|
|
27
|
+
id: c.id,
|
|
28
|
+
author: c.author,
|
|
29
|
+
...(c.authorType !== undefined && { authorType: c.authorType }),
|
|
30
|
+
body: c.body,
|
|
31
|
+
url: c.url,
|
|
32
|
+
})),
|
|
33
|
+
}),
|
|
25
34
|
...(suggestion !== undefined && { suggestion }),
|
|
26
35
|
};
|
|
27
36
|
}
|
|
@@ -48,6 +57,23 @@ export function toAgentCheck(c) {
|
|
|
48
57
|
...(c.jobName !== undefined && { jobName: c.jobName }),
|
|
49
58
|
...(c.failedStep !== undefined && { failedStep: c.failedStep }),
|
|
50
59
|
...(c.summary !== undefined && { summary: c.summary }),
|
|
60
|
+
...(c.annotations !== undefined && { annotations: c.annotations }),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function toAgentStalledCheck(c, nowSeconds) {
|
|
64
|
+
const createdAtUnix = c.createdAtUnix ?? nowSeconds;
|
|
65
|
+
const activityAtUnix = c.updatedAtUnix ?? createdAtUnix;
|
|
66
|
+
return {
|
|
67
|
+
name: c.name,
|
|
68
|
+
status: c.status,
|
|
69
|
+
source: c.source ?? "check_run",
|
|
70
|
+
runId: c.runId,
|
|
71
|
+
detailsUrl: c.detailsUrl || null,
|
|
72
|
+
...(c.createdAtUnix !== undefined && { createdAtUnix: c.createdAtUnix }),
|
|
73
|
+
...(c.startedAtUnix !== undefined && { startedAtUnix: c.startedAtUnix }),
|
|
74
|
+
...(c.updatedAtUnix !== undefined && { updatedAtUnix: c.updatedAtUnix }),
|
|
75
|
+
ageSeconds: Math.max(0, nowSeconds - activityAtUnix),
|
|
76
|
+
...(c.summary !== undefined && { summary: c.summary }),
|
|
51
77
|
};
|
|
52
78
|
}
|
|
53
79
|
/**
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
3
|
+
import { rm } from "node:fs/promises";
|
|
4
|
+
export function idToFilename(id) {
|
|
5
|
+
return createHash("sha256").update(id, "utf8").digest("hex") + ".json";
|
|
6
|
+
}
|
|
7
|
+
export const testKey = { owner: "test-owner", repo: "test-repo", pr: 123 };
|
|
8
|
+
export const testId = "PRRT_kwDOTest123";
|
|
9
|
+
export let testStateDir;
|
|
10
|
+
export function registerHooks() {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
testStateDir = `${process.env["TMPDIR"] ?? "/tmp"}/shepherd-seen-test-${randomBytes(4).toString("hex")}`;
|
|
13
|
+
process.env["PR_SHEPHERD_STATE_DIR"] = testStateDir;
|
|
14
|
+
});
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
delete process.env["PR_SHEPHERD_STATE_DIR"];
|
|
17
|
+
await rm(testStateDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function threadComments(thread) {
|
|
2
|
+
if (thread.comments && thread.comments.length > 0) {
|
|
3
|
+
return thread.comments.map((c) => ({
|
|
4
|
+
id: c.id,
|
|
5
|
+
isMinimized: c.isMinimized ?? false,
|
|
6
|
+
author: c.author,
|
|
7
|
+
authorType: c.authorType ?? "Unknown",
|
|
8
|
+
body: c.body,
|
|
9
|
+
url: c.url,
|
|
10
|
+
createdAtUnix: c.createdAtUnix ?? 0,
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
id: "",
|
|
16
|
+
isMinimized: false,
|
|
17
|
+
author: thread.author,
|
|
18
|
+
authorType: thread.authorType ?? "Unknown",
|
|
19
|
+
body: thread.body,
|
|
20
|
+
url: thread.url ?? "",
|
|
21
|
+
createdAtUnix: thread.createdAtUnix ?? 0,
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
export function threadTranscriptBody(thread) {
|
|
26
|
+
if (!thread.comments || thread.comments.length === 0)
|
|
27
|
+
return thread.body;
|
|
28
|
+
return threadComments(thread)
|
|
29
|
+
.map((c) => `${c.id}\n${c.body}`)
|
|
30
|
+
.join("\n\n--- thread comment ---\n\n");
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/bin/types.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
/** Shared type definitions for the shepherd CLI. */
|
|
2
2
|
export * from "./types/github.mjs";
|
|
3
|
+
export * from "./types/review-thread.mjs";
|
|
4
|
+
export * from "./types/agent-thread.mjs";
|
|
5
|
+
export * from "./types/check-annotations.mjs";
|
|
3
6
|
export * from "./types/report.mjs";
|
|
4
7
|
export * from "./types/iterate.mjs";
|
package/package.json
CHANGED
|
@@ -39,5 +39,5 @@ Poll dispatcher for iterating a PR to completion.
|
|
|
39
39
|
|
|
40
40
|
4. **Stop conditions (terminal states):**
|
|
41
41
|
- Stop when the CLI emits `[CANCEL]` (ready-delay completed, or PR merged/closed).
|
|
42
|
-
- Stop when the CLI emits `[ESCALATE]`, including `stall-timeout` for repeated unchanged CI failures.
|
|
42
|
+
- Stop when the CLI emits `[ESCALATE]`, including `stall-timeout` for repeated unchanged CI failures or CI that never starts.
|
|
43
43
|
- **Do NOT merge the pull request** unless the human has explicitly requested or allowed it.
|