pr-shepherd 0.51.0 → 0.52.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 +25 -17
- package/bin/checks/log-excerpt.d.mts +1 -0
- package/bin/checks/log-excerpt.mjs +192 -0
- package/bin/checks/triage.mjs +2 -121
- package/bin/cli/iterate-lean.mjs +0 -3
- package/bin/cli/poll-summary-emitter.mjs +2 -3
- package/bin/cli/poll-summary-formatter.mjs +9 -2
- package/bin/commands/check.mjs +1 -0
- package/bin/commands/iterate/escalate.mjs +0 -13
- package/bin/commands/iterate/fix-code.d.mts +2 -0
- package/bin/commands/iterate/fix-code.mjs +14 -42
- package/bin/commands/iterate/index.mjs +140 -3
- package/bin/commands/iterate/merge-state.mjs +33 -18
- package/bin/commands/iterate/parent-first.d.mts +9 -0
- package/bin/commands/iterate/parent-first.mjs +57 -0
- package/bin/commands/iterate/stale-ancestry.d.mts +22 -0
- package/bin/commands/iterate/stale-ancestry.mjs +37 -0
- package/bin/commands/poll-summary-instructions.mjs +179 -99
- package/bin/commands/poll-summary.mjs +28 -4
- package/bin/exit-codes.d.mts +2 -0
- package/bin/exit-codes.mjs +2 -0
- package/bin/github/batch-parsers.mjs +1 -0
- package/bin/github/batch-raw-rules.d.mts +3 -0
- package/bin/github/gql/poll-summary-fragment.gql +54 -0
- package/bin/github/gql/pr-merge-policy.gql +3 -0
- package/bin/github/poll-summary-fingerprint.d.mts +8 -0
- package/bin/github/poll-summary-fingerprint.mjs +28 -0
- package/bin/github/poll-summary-projector.mjs +67 -14
- package/bin/github/poll-summary-queue-removal.d.mts +5 -0
- package/bin/github/poll-summary-queue-removal.mjs +16 -0
- package/bin/github/poll-summary-raw.d.mts +33 -0
- package/bin/github/poll-summary-readiness.d.mts +6 -0
- package/bin/github/poll-summary-readiness.mjs +25 -0
- package/bin/github/poll-summary.d.mts +3 -0
- package/bin/github/poll-summary.mjs +5 -0
- package/bin/state/ready-receipts.d.mts +45 -0
- package/bin/state/ready-receipts.mjs +86 -0
- package/bin/types/escalate.d.mts +2 -3
- package/bin/types/github.d.mts +2 -0
- package/bin/types/poll-summary.d.mts +12 -2
- package/bin/types/report.d.mts +2 -0
- package/package.json +4 -4
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- package/plugins/pr-shepherd/.mcp.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +3 -3
- package/bin/state/bot-cr-seen.d.mts +0 -51
- package/bin/state/bot-cr-seen.mjs +0 -100
package/README.md
CHANGED
|
@@ -36,7 +36,10 @@ Each tick returns exactly one action:
|
|
|
36
36
|
- `FIX_CODE` — agent work is required; complete it, push when needed, then continue polling. Push access to the PR head branch is a usage precondition.
|
|
37
37
|
- `MERGE` — run the emitted head-pinned auto-merge or queue command. Ordinary merges include a plain-merge fallback; queue merges include a GraphQL enqueue fallback. GitHub is authoritative for the result and reports any authorization failure.
|
|
38
38
|
- `CANCEL` — stop polling because the PR merged, closed, or completed its ready-delay.
|
|
39
|
-
- `ESCALATE` — stop polling until a human provides direction.
|
|
39
|
+
- `ESCALATE` — stop polling until a human provides direction. Native stacks reach this only after their autonomous one-PR sessions are exhausted.
|
|
40
|
+
|
|
41
|
+
Native-stack summaries additionally use stack-level `SHEPHERD`: run the listed one-PR sessions,
|
|
42
|
+
then recheck the stack. It is not a per-PR `FIX_CODE` action.
|
|
40
43
|
|
|
41
44
|
Example shape:
|
|
42
45
|
|
|
@@ -79,7 +82,7 @@ Conversations Resolved: No [Not Required]
|
|
|
79
82
|
9. `[FIX_CODE]` is non-terminal: if you changed code, commit and push to the PR head branch, then run review mutations using the pushed commit SHA and iterate immediately with the same options; without code changes, complete the authorized review mutations and iterate immediately.
|
|
80
83
|
```
|
|
81
84
|
|
|
82
|
-
See [docs/actions.md](docs/actions.md) for the complete output contract and [docs/escalations.md](docs/escalations.md) for the exact finite human-handoff boundary. Iterate/poll PR outcomes use exit codes `0` and `10`–`
|
|
85
|
+
See [docs/actions.md](docs/actions.md) for the complete output contract and [docs/escalations.md](docs/escalations.md) for the exact finite human-handoff boundary. Iterate/poll PR outcomes use exit codes `0` and `10`–`16`; command and GitHub failures use `sysexits.h` codes — [docs/exit-codes.md](docs/exit-codes.md).
|
|
83
86
|
|
|
84
87
|
## Workflow Assumptions
|
|
85
88
|
|
|
@@ -129,7 +132,7 @@ pr-shepherd 42 # poll until non-WAIT or timeout
|
|
|
129
132
|
pr-shepherd 42 --interval 60s --timeout 270s
|
|
130
133
|
pr-shepherd 42 --quiet-status # print only changed WAIT status snapshots
|
|
131
134
|
pr-shepherd 42 --until-terminal # continue through WAIT/MARK_READY until work or terminal state
|
|
132
|
-
pr-shepherd 42 --debounce 5m # wait 5m after first FIX_CODE, then return one batched tick
|
|
135
|
+
pr-shepherd 42 --debounce 5m # wait 5m after first FIX_CODE or stack SHEPHERD, then return one batched tick
|
|
133
136
|
pr-shepherd 42 --ready-delay 15m
|
|
134
137
|
pr-shepherd 42 --merge # request head-pinned auto-merge/queue; GitHub reports the result
|
|
135
138
|
pr-shepherd iterate 42 # single tick
|
|
@@ -139,20 +142,25 @@ pr-shepherd 42 43 44 # summarize an explicit same-repository s
|
|
|
139
142
|
pr-shepherd --stack 43 # summarize every PR in a native GitHub stack
|
|
140
143
|
```
|
|
141
144
|
|
|
142
|
-
Multi-PR and `--stack` polling use compact, read-only GraphQL summaries. They return when
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
145
|
+
Multi-PR and `--stack` polling use compact, read-only GraphQL summaries. They return when work is
|
|
146
|
+
needed, every selected PR is complete, the bounded timeout expires, or `--until-terminal` crosses a
|
|
147
|
+
configured GraphQL quota-warning band. Explicit PR sets give each actionable row an exact single-PR
|
|
148
|
+
`pollCommand`, so independent rows can proceed before the next aggregate poll.
|
|
149
|
+
|
|
150
|
+
Native-stack rows are ordered bottom-to-top. `--stack` never performs a mutation itself; only
|
|
151
|
+
`--stack --merge` can emit a complete-stack merge command for the agent. An unready layer (draft, missing a READY
|
|
152
|
+
receipt, conflicting, failing, or stale) returns stack-level `SHEPHERD` with one-PR Shepherd instructions for
|
|
153
|
+
the affected layers. A draft or other unready lower layer marks every higher open layer with
|
|
154
|
+
`blockedByPr`; review and CI sessions on independent layers may proceed concurrently, but an upper
|
|
155
|
+
draft cannot transition to ready until every lower layer has its READY receipt. A queued stack
|
|
156
|
+
returns `WAIT`. A terminal READY or fully merged stack returns `CANCEL`. Closed or unverified
|
|
157
|
+
topology returns `ESCALATE` for human direction after any other shepherdable PRs are handled;
|
|
158
|
+
until then, `SHEPHERD` remains the immediate action and lists the human blockers too.
|
|
159
|
+
|
|
160
|
+
With `--stack --merge`, a fully reconciled and READY stack returns `MERGE` with a `gh stack merge`
|
|
161
|
+
command for the agent to run, checking and installing the optional `github/gh-stack` extension first
|
|
162
|
+
when necessary. It then rechecks until every layer merges and returns `CANCEL`. API and MCP
|
|
163
|
+
aggregate calls perform one summary tick and leave recurrence to the caller.
|
|
156
164
|
|
|
157
165
|
Polling defaults can be set under `poll` in `.pr-shepherdrc.yml`: `intervalSeconds`, `timeoutSeconds`, `debounceSeconds`, and `quietStatus`. Explicit flags override configuration, including `--no-quiet-status` when a shared config enables quiet output. Quiet status remains off by default.
|
|
158
166
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function buildLogExcerpt(raw: string): string | undefined;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { loadConfig } from "../config/load.mjs";
|
|
2
|
+
const LOG_EXCERPT_CONTEXT_LINES = 16;
|
|
3
|
+
const LOG_EXCERPT_TAIL_LINES = 28;
|
|
4
|
+
const LOG_EXCERPT_MAX_CHARS = 4_000;
|
|
5
|
+
const TRUNCATED_MARK = "[truncated]";
|
|
6
|
+
const ANSI_SGR_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
7
|
+
const STEP_GROUP_RE = /^##\[group\](?:Run |Post )/;
|
|
8
|
+
const GROUP_RE = /^##\[group\]/;
|
|
9
|
+
const ENDGROUP_RE = /^##\[endgroup\]$/;
|
|
10
|
+
const POST_JOB_RE = /^(?:Post job cleanup\.?|Cleaning up orphan processes)$/;
|
|
11
|
+
export function buildLogExcerpt(raw) {
|
|
12
|
+
const ignorePatterns = compileIgnoreLogLinePatterns();
|
|
13
|
+
const prepared = raw
|
|
14
|
+
.split(/\r?\n/)
|
|
15
|
+
.map(cleanLogLine)
|
|
16
|
+
.filter((line) => line.trim() !== "");
|
|
17
|
+
if (prepared.length === 0)
|
|
18
|
+
return undefined;
|
|
19
|
+
const isolated = isolateFailedStep(prepared);
|
|
20
|
+
const lines = isolated.lines
|
|
21
|
+
.map(stripGroupMarkers)
|
|
22
|
+
.filter((line) => line.trim() !== "" && !isNoiseLine(line, ignorePatterns));
|
|
23
|
+
if (lines.length === 0)
|
|
24
|
+
return undefined;
|
|
25
|
+
const aggregateExcerpt = buildAggregateJobResultsExcerpt(lines);
|
|
26
|
+
if (aggregateExcerpt !== undefined)
|
|
27
|
+
return aggregateExcerpt;
|
|
28
|
+
if (isolated.isolated) {
|
|
29
|
+
const errorIndex = findLogExcerptAnchor(lines);
|
|
30
|
+
return truncateTail(lines.join("\n"), errorIndex === -1 ? undefined : lines[errorIndex]);
|
|
31
|
+
}
|
|
32
|
+
return boundFallbackExcerpt(lines);
|
|
33
|
+
}
|
|
34
|
+
function isolateFailedStep(lines) {
|
|
35
|
+
const postJob = lines.findIndex((line) => POST_JOB_RE.test(line));
|
|
36
|
+
const capped = postJob === -1 ? lines : lines.slice(0, postJob);
|
|
37
|
+
const anchor = findLogExcerptAnchor(capped);
|
|
38
|
+
const groupStart = findPrecedingStepGroup(capped, anchor === -1 ? capped.length - 1 : anchor);
|
|
39
|
+
if (groupStart === -1)
|
|
40
|
+
return { lines: capped, isolated: false };
|
|
41
|
+
const endgroup = findMatchingEndgroup(capped, groupStart);
|
|
42
|
+
const after = endgroup === -1 ? groupStart + 1 : endgroup + 1;
|
|
43
|
+
const next = findNextStepGroup(capped, after);
|
|
44
|
+
return { lines: capped.slice(after, next === -1 ? capped.length : next), isolated: true };
|
|
45
|
+
}
|
|
46
|
+
function findPrecedingStepGroup(lines, from) {
|
|
47
|
+
for (let i = Math.min(from, lines.length - 1); i >= 0; i--) {
|
|
48
|
+
if (STEP_GROUP_RE.test(lines[i] ?? ""))
|
|
49
|
+
return i;
|
|
50
|
+
}
|
|
51
|
+
return -1;
|
|
52
|
+
}
|
|
53
|
+
function findMatchingEndgroup(lines, groupStart) {
|
|
54
|
+
let depth = 0;
|
|
55
|
+
for (let i = groupStart; i < lines.length; i++) {
|
|
56
|
+
const line = lines[i] ?? "";
|
|
57
|
+
if (GROUP_RE.test(line))
|
|
58
|
+
depth++;
|
|
59
|
+
else if (ENDGROUP_RE.test(line) && --depth === 0)
|
|
60
|
+
return i;
|
|
61
|
+
}
|
|
62
|
+
return -1;
|
|
63
|
+
}
|
|
64
|
+
function findNextStepGroup(lines, from) {
|
|
65
|
+
let depth = 0;
|
|
66
|
+
for (let i = from; i < lines.length; i++) {
|
|
67
|
+
const line = lines[i] ?? "";
|
|
68
|
+
if (GROUP_RE.test(line)) {
|
|
69
|
+
if (depth === 0 && STEP_GROUP_RE.test(line))
|
|
70
|
+
return i;
|
|
71
|
+
depth++;
|
|
72
|
+
}
|
|
73
|
+
else if (ENDGROUP_RE.test(line) && depth > 0)
|
|
74
|
+
depth--;
|
|
75
|
+
}
|
|
76
|
+
return -1;
|
|
77
|
+
}
|
|
78
|
+
function boundFallbackExcerpt(lines) {
|
|
79
|
+
const errorIndex = findLogExcerptAnchor(lines);
|
|
80
|
+
if (errorIndex === -1) {
|
|
81
|
+
return truncateLogExcerpt(lines.slice(-LOG_EXCERPT_TAIL_LINES).join("\n"));
|
|
82
|
+
}
|
|
83
|
+
const start = Math.max(0, errorIndex - LOG_EXCERPT_CONTEXT_LINES);
|
|
84
|
+
const excerpt = lines.slice(start, Math.min(lines.length, errorIndex + LOG_EXCERPT_CONTEXT_LINES + 1));
|
|
85
|
+
return truncateAnchoredExcerpt(excerpt, errorIndex - start);
|
|
86
|
+
}
|
|
87
|
+
function findLogExcerptAnchor(lines) {
|
|
88
|
+
const explicitError = lines.findIndex((line) => line.includes("##[error]"));
|
|
89
|
+
if (explicitError !== -1)
|
|
90
|
+
return explicitError;
|
|
91
|
+
return lines.findIndex((line) => /\b(error|failed|cancelled)\b/i.test(line));
|
|
92
|
+
}
|
|
93
|
+
function buildAggregateJobResultsExcerpt(lines) {
|
|
94
|
+
const jobResults = extractJobResults(lines);
|
|
95
|
+
if (jobResults === undefined)
|
|
96
|
+
return undefined;
|
|
97
|
+
const failed = Object.entries(jobResults)
|
|
98
|
+
.map(([name, value]) => ({ name, result: extractJobResult(value) }))
|
|
99
|
+
.filter((entry) => entry.result !== undefined && !["success", "skipped"].includes(entry.result));
|
|
100
|
+
if (failed.length === 0)
|
|
101
|
+
return undefined;
|
|
102
|
+
return truncateLogExcerpt([
|
|
103
|
+
...lines.filter((line) => /required jobs failed|exit code \d+/i.test(line)),
|
|
104
|
+
"Job results (non-success):",
|
|
105
|
+
...failed.map((entry) => `${entry.name}: ${entry.result}`),
|
|
106
|
+
].join("\n"));
|
|
107
|
+
}
|
|
108
|
+
function extractJobResults(lines) {
|
|
109
|
+
const startIndex = lines.findIndex((line) => line.includes("Job results:"));
|
|
110
|
+
if (startIndex === -1)
|
|
111
|
+
return undefined;
|
|
112
|
+
const block = collectJsonBlock(lines, startIndex);
|
|
113
|
+
if (block === undefined)
|
|
114
|
+
return undefined;
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(block);
|
|
117
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
118
|
+
? parsed
|
|
119
|
+
: undefined;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function collectJsonBlock(lines, startIndex) {
|
|
126
|
+
const startLine = lines[startIndex] ?? "";
|
|
127
|
+
const objectStart = startLine.indexOf("{");
|
|
128
|
+
if (objectStart === -1)
|
|
129
|
+
return undefined;
|
|
130
|
+
const collected = [startLine.slice(objectStart)];
|
|
131
|
+
let depth = braceDepth(collected[0]);
|
|
132
|
+
for (let i = startIndex + 1; i < lines.length && depth > 0; i++) {
|
|
133
|
+
const line = lines[i] ?? "";
|
|
134
|
+
collected.push(line);
|
|
135
|
+
depth += braceDepth(line);
|
|
136
|
+
}
|
|
137
|
+
return depth === 0 ? collected.join("\n") : undefined;
|
|
138
|
+
}
|
|
139
|
+
function braceDepth(line) {
|
|
140
|
+
return [...line].reduce((depth, ch) => {
|
|
141
|
+
if (ch === "{")
|
|
142
|
+
return depth + 1;
|
|
143
|
+
if (ch === "}")
|
|
144
|
+
return depth - 1;
|
|
145
|
+
return depth;
|
|
146
|
+
}, 0);
|
|
147
|
+
}
|
|
148
|
+
function extractJobResult(value) {
|
|
149
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
150
|
+
return undefined;
|
|
151
|
+
const result = value.result;
|
|
152
|
+
return typeof result === "string" ? result : undefined;
|
|
153
|
+
}
|
|
154
|
+
function truncateLogExcerpt(text) {
|
|
155
|
+
if (text.length <= LOG_EXCERPT_MAX_CHARS)
|
|
156
|
+
return text;
|
|
157
|
+
return `${text.slice(0, LOG_EXCERPT_MAX_CHARS - `\n${TRUNCATED_MARK}`.length).trimEnd()}\n${TRUNCATED_MARK}`;
|
|
158
|
+
}
|
|
159
|
+
function truncateAnchoredExcerpt(lines, anchorIndex) {
|
|
160
|
+
const text = lines.join("\n");
|
|
161
|
+
if (text.length <= LOG_EXCERPT_MAX_CHARS)
|
|
162
|
+
return text;
|
|
163
|
+
return truncateLogExcerpt(`${TRUNCATED_MARK}\n${lines.slice(anchorIndex).join("\n")}`);
|
|
164
|
+
}
|
|
165
|
+
function truncateTail(text, keep) {
|
|
166
|
+
if (text.length <= LOG_EXCERPT_MAX_CHARS)
|
|
167
|
+
return text;
|
|
168
|
+
const head = `${TRUNCATED_MARK}\n`;
|
|
169
|
+
const slice = text.slice(-(LOG_EXCERPT_MAX_CHARS - head.length));
|
|
170
|
+
const nl = slice.indexOf("\n");
|
|
171
|
+
const tail = `${head}${nl === -1 ? slice : slice.slice(nl + 1)}`;
|
|
172
|
+
if (!keep || tail.includes(keep))
|
|
173
|
+
return tail;
|
|
174
|
+
const from = text.lastIndexOf(keep);
|
|
175
|
+
return from === -1 ? tail : truncateLogExcerpt(`${head}${text.slice(from)}`);
|
|
176
|
+
}
|
|
177
|
+
function compileIgnoreLogLinePatterns() {
|
|
178
|
+
return loadConfig().checks.ignoreLogLines.map((pattern) => new RegExp(pattern));
|
|
179
|
+
}
|
|
180
|
+
function isNoiseLine(line, patterns) {
|
|
181
|
+
return patterns.some((re) => re.test(line));
|
|
182
|
+
}
|
|
183
|
+
function cleanLogLine(line) {
|
|
184
|
+
return line
|
|
185
|
+
.replace(/^\uFEFF/, "")
|
|
186
|
+
.replace(ANSI_SGR_RE, "")
|
|
187
|
+
.replace(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\s*/, "")
|
|
188
|
+
.trimEnd();
|
|
189
|
+
}
|
|
190
|
+
function stripGroupMarkers(line) {
|
|
191
|
+
return line.replace(/##\[(?:group|endgroup)\]/g, "");
|
|
192
|
+
}
|
package/bin/checks/triage.mjs
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
import { restWithRateLimit, restText } from "../github/http.mjs";
|
|
3
3
|
import { loadDerived, storeDerived } from "../state/rest-cache.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { buildLogExcerpt } from "./log-excerpt.mjs";
|
|
5
5
|
const STARTUP_FAILURE_STATUS = "startup_failure";
|
|
6
|
-
const LOG_EXCERPT_CONTEXT_LINES = 16;
|
|
7
|
-
const LOG_EXCERPT_TAIL_LINES = 28;
|
|
8
|
-
const LOG_EXCERPT_MAX_CHARS = 4_000;
|
|
9
|
-
const TRUNCATED_SUFFIX = "\n[truncated]";
|
|
10
|
-
const ANSI_SGR_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
11
6
|
export function triageFailingChecks(failingChecks, repo, stateKey) {
|
|
12
7
|
const jobsCache = new Map();
|
|
13
8
|
return Promise.all(failingChecks.map((c) => triageCheck(c, repo, jobsCache, stateKey)));
|
|
@@ -163,7 +158,7 @@ function pickJobInfo(jobs, checkName) {
|
|
|
163
158
|
* never gets frozen into the cache.
|
|
164
159
|
*/
|
|
165
160
|
async function fetchJobLogExcerpt(jobId, repo, stateKey, cacheable = false) {
|
|
166
|
-
const cacheName = `joblog-${jobId}`;
|
|
161
|
+
const cacheName = `joblog-v2-${jobId}`;
|
|
167
162
|
if (stateKey && cacheable) {
|
|
168
163
|
const cached = await loadDerived(stateKey, cacheName);
|
|
169
164
|
if (cached)
|
|
@@ -181,117 +176,3 @@ async function fetchJobLogExcerpt(jobId, repo, stateKey, cacheable = false) {
|
|
|
181
176
|
return undefined;
|
|
182
177
|
}
|
|
183
178
|
}
|
|
184
|
-
function buildLogExcerpt(raw) {
|
|
185
|
-
const ignorePatterns = compileIgnoreLogLinePatterns();
|
|
186
|
-
const lines = raw
|
|
187
|
-
.split(/\r?\n/)
|
|
188
|
-
.map(cleanLogLine)
|
|
189
|
-
.filter((line) => line.trim() !== "" && !isNoiseLine(line, ignorePatterns));
|
|
190
|
-
if (lines.length === 0)
|
|
191
|
-
return undefined;
|
|
192
|
-
const aggregateExcerpt = buildAggregateJobResultsExcerpt(lines);
|
|
193
|
-
if (aggregateExcerpt !== undefined)
|
|
194
|
-
return aggregateExcerpt;
|
|
195
|
-
const errorIndex = findLogExcerptAnchor(lines);
|
|
196
|
-
if (errorIndex === -1)
|
|
197
|
-
return truncateLogExcerpt(lines.slice(-LOG_EXCERPT_TAIL_LINES).join("\n"));
|
|
198
|
-
const start = Math.max(0, errorIndex - LOG_EXCERPT_CONTEXT_LINES);
|
|
199
|
-
const excerpt = lines.slice(start, Math.min(lines.length, errorIndex + LOG_EXCERPT_CONTEXT_LINES + 1));
|
|
200
|
-
return truncateAnchoredExcerpt(excerpt, errorIndex - start);
|
|
201
|
-
}
|
|
202
|
-
function findLogExcerptAnchor(lines) {
|
|
203
|
-
const explicitError = lines.findIndex((line) => line.includes("##[error]"));
|
|
204
|
-
if (explicitError !== -1)
|
|
205
|
-
return explicitError;
|
|
206
|
-
return lines.findIndex((line) => /\b(error|failed|cancelled)\b/i.test(line));
|
|
207
|
-
}
|
|
208
|
-
function buildAggregateJobResultsExcerpt(lines) {
|
|
209
|
-
const jobResults = extractJobResults(lines);
|
|
210
|
-
if (jobResults === undefined)
|
|
211
|
-
return undefined;
|
|
212
|
-
const failed = Object.entries(jobResults)
|
|
213
|
-
.map(([name, value]) => ({ name, result: extractJobResult(value) }))
|
|
214
|
-
.filter((entry) => entry.result !== undefined && !["success", "skipped"].includes(entry.result));
|
|
215
|
-
if (failed.length === 0)
|
|
216
|
-
return undefined;
|
|
217
|
-
const output = [
|
|
218
|
-
...lines.filter((line) => /required jobs failed|exit code \d+/i.test(line)),
|
|
219
|
-
"Job results (non-success):",
|
|
220
|
-
...failed.map((entry) => `${entry.name}: ${entry.result}`),
|
|
221
|
-
];
|
|
222
|
-
return truncateLogExcerpt(output.join("\n"));
|
|
223
|
-
}
|
|
224
|
-
function extractJobResults(lines) {
|
|
225
|
-
const startIndex = lines.findIndex((line) => line.includes("Job results:"));
|
|
226
|
-
if (startIndex === -1)
|
|
227
|
-
return undefined;
|
|
228
|
-
const block = collectJsonBlock(lines, startIndex);
|
|
229
|
-
if (block === undefined)
|
|
230
|
-
return undefined;
|
|
231
|
-
try {
|
|
232
|
-
const parsed = JSON.parse(block);
|
|
233
|
-
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
234
|
-
? parsed
|
|
235
|
-
: undefined;
|
|
236
|
-
}
|
|
237
|
-
catch {
|
|
238
|
-
return undefined;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
function collectJsonBlock(lines, startIndex) {
|
|
242
|
-
const startLine = lines[startIndex] ?? "";
|
|
243
|
-
const objectStart = startLine.indexOf("{");
|
|
244
|
-
if (objectStart === -1)
|
|
245
|
-
return undefined;
|
|
246
|
-
const collected = [startLine.slice(objectStart)];
|
|
247
|
-
let depth = braceDepth(collected[0]);
|
|
248
|
-
for (let i = startIndex + 1; i < lines.length && depth > 0; i++) {
|
|
249
|
-
const line = lines[i] ?? "";
|
|
250
|
-
collected.push(line);
|
|
251
|
-
depth += braceDepth(line);
|
|
252
|
-
}
|
|
253
|
-
return depth === 0 ? collected.join("\n") : undefined;
|
|
254
|
-
}
|
|
255
|
-
function braceDepth(line) {
|
|
256
|
-
return [...line].reduce((depth, ch) => {
|
|
257
|
-
if (ch === "{")
|
|
258
|
-
return depth + 1;
|
|
259
|
-
if (ch === "}")
|
|
260
|
-
return depth - 1;
|
|
261
|
-
return depth;
|
|
262
|
-
}, 0);
|
|
263
|
-
}
|
|
264
|
-
function extractJobResult(value) {
|
|
265
|
-
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
266
|
-
return undefined;
|
|
267
|
-
const result = value.result;
|
|
268
|
-
return typeof result === "string" ? result : undefined;
|
|
269
|
-
}
|
|
270
|
-
function truncateLogExcerpt(text) {
|
|
271
|
-
if (text.length <= LOG_EXCERPT_MAX_CHARS)
|
|
272
|
-
return text;
|
|
273
|
-
return `${text.slice(0, LOG_EXCERPT_MAX_CHARS - TRUNCATED_SUFFIX.length).trimEnd()}${TRUNCATED_SUFFIX}`;
|
|
274
|
-
}
|
|
275
|
-
function truncateAnchoredExcerpt(lines, anchorIndex) {
|
|
276
|
-
const text = lines.join("\n");
|
|
277
|
-
if (text.length <= LOG_EXCERPT_MAX_CHARS)
|
|
278
|
-
return text;
|
|
279
|
-
return truncateLogExcerpt(`${TRUNCATED_SUFFIX.trim()}\n${lines.slice(anchorIndex).join("\n")}`);
|
|
280
|
-
}
|
|
281
|
-
// User-configured via `checks.ignoreLogLines` (regex source strings) — empty by
|
|
282
|
-
// default. What counts as noise varies by CI toolchain, so Shepherd ships no
|
|
283
|
-
// built-in patterns; a project opts in via `.pr-shepherdrc.yml`.
|
|
284
|
-
function compileIgnoreLogLinePatterns() {
|
|
285
|
-
return loadConfig().checks.ignoreLogLines.map((pattern) => new RegExp(pattern));
|
|
286
|
-
}
|
|
287
|
-
function isNoiseLine(line, patterns) {
|
|
288
|
-
return patterns.some((re) => re.test(line));
|
|
289
|
-
}
|
|
290
|
-
function cleanLogLine(line) {
|
|
291
|
-
return line
|
|
292
|
-
.replace(/^\uFEFF/, "")
|
|
293
|
-
.replace(ANSI_SGR_RE, "")
|
|
294
|
-
.replace(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\s*/, "")
|
|
295
|
-
.replace(/##\[(?:group|endgroup)\]/g, "")
|
|
296
|
-
.trimEnd();
|
|
297
|
-
}
|
package/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -183,9 +183,6 @@ export function projectIterateLean(result, opts) {
|
|
|
183
183
|
...(result.escalate.mergeQueueRemoval && {
|
|
184
184
|
mergeQueueRemoval: result.escalate.mergeQueueRemoval,
|
|
185
185
|
}),
|
|
186
|
-
...(result.escalate.stack && {
|
|
187
|
-
stack: result.escalate.stack,
|
|
188
|
-
}),
|
|
189
186
|
...(result.escalate.authorization &&
|
|
190
187
|
result.escalate.authorization.length > 0 && {
|
|
191
188
|
authorization: result.escalate.authorization,
|
|
@@ -12,13 +12,12 @@ function pollSummaryExitCode(result) {
|
|
|
12
12
|
if (result.nextAction) {
|
|
13
13
|
const stackExitCode = {
|
|
14
14
|
escalate: EXIT.ESCALATE,
|
|
15
|
-
|
|
15
|
+
shepherd: EXIT.SHEPHERD,
|
|
16
16
|
merge: EXIT.MERGE,
|
|
17
|
-
mark_ready: EXIT.MARK_READY,
|
|
18
17
|
wait: EXIT.WAIT,
|
|
19
18
|
cancel: EXIT.OK,
|
|
20
19
|
};
|
|
21
|
-
return stackExitCode[result.nextAction]
|
|
20
|
+
return stackExitCode[result.nextAction];
|
|
22
21
|
}
|
|
23
22
|
const actions = new Set(result.prs.map((item) => item.action));
|
|
24
23
|
if (actions.has("escalate"))
|
|
@@ -7,7 +7,7 @@ export function formatPollSummaryResult(result) {
|
|
|
7
7
|
const lines = [
|
|
8
8
|
`# Poll summary [${result.reason.toUpperCase()}]`,
|
|
9
9
|
"",
|
|
10
|
-
`**repo** \`${result.repo}\` · **selection** ${selection} · **mode** \`${result.mode}\`${result.nextAction ? ` · **next action** \`${result.nextAction}\`` : ""}`,
|
|
10
|
+
`**repo** \`${result.repo}\` · **selection** ${selection} · **mode** \`${result.mode}\`${result.stackMergeable !== undefined ? ` · **stack mergeable** \`${result.stackMergeable}\`` : ""}${result.nextAction ? ` · **next action** \`${result.nextAction}\`` : ""}`,
|
|
11
11
|
"",
|
|
12
12
|
"## Pull requests",
|
|
13
13
|
"",
|
|
@@ -42,11 +42,13 @@ function formatItem(item) {
|
|
|
42
42
|
? " · blocking reviewer `in progress`"
|
|
43
43
|
: "";
|
|
44
44
|
const readyDelay = item.remainingSeconds !== undefined ? ` · ready delay \`${item.remainingSeconds}s\`` : "";
|
|
45
|
+
const readyReceipt = item.readyReceipt ? " · Shepherd READY completion `verified`" : "";
|
|
46
|
+
const blockedBy = item.blockedByPr ? ` · stack blocked by PR #${item.blockedByPr}` : "";
|
|
45
47
|
const checks = item.checks;
|
|
46
48
|
const review = item.review;
|
|
47
49
|
return [
|
|
48
50
|
`- [PR #${item.pr}: ${escapeMarkdownText(item.title)}](${item.url}) [${item.action.toUpperCase()}]`,
|
|
49
|
-
` - state \`${item.state}\` · mergeable \`${item.mergeable}\` · merge \`${item.mergeStateStatus}\`${reviewDecision}${stateFlags}${blockingReviewer}${readyDelay}${stack}`,
|
|
51
|
+
` - state \`${item.state}\` · mergeable \`${item.mergeable}\` · merge \`${item.mergeStateStatus}\`${reviewDecision}${stateFlags}${blockingReviewer}${readyDelay}${readyReceipt}${blockedBy}${stack}`,
|
|
50
52
|
` - head \`${item.headRefName}\` at \`${item.headRefOid}\` · base \`${item.baseRefName}\``,
|
|
51
53
|
...(checks
|
|
52
54
|
? [` - checks: ${formatCounts(checks, checks.incomplete ? ", incomplete" : "")}`]
|
|
@@ -54,6 +56,11 @@ function formatItem(item) {
|
|
|
54
56
|
...(review
|
|
55
57
|
? [` - review: ${formatCounts(review, review.incomplete ? ", incomplete" : "")}`]
|
|
56
58
|
: []),
|
|
59
|
+
...(item.queueRemoval
|
|
60
|
+
? [
|
|
61
|
+
` - queue removal: reason \`${item.queueRemoval.reason ?? "UNKNOWN"}\` · at \`${item.queueRemoval.createdAtUnix}\`${item.queueRemoval.actor ? ` · actor \`@${item.queueRemoval.actor}\`` : ""}${item.queueRemoval.beforeCommitOid ? ` · commit \`${item.queueRemoval.beforeCommitOid}\`` : ""}${item.queueRemoval.beforeCommitParentOids?.length ? ` · parents \`${item.queueRemoval.beforeCommitParentOids.join(",")}\`` : ""}`,
|
|
62
|
+
]
|
|
63
|
+
: []),
|
|
57
64
|
` - reasons: ${item.reasons.map((reason) => `\`${reason}\``).join(", ")}`,
|
|
58
65
|
...(item.pollCommand ? [` - pollCommand: \`${item.pollCommand}\``] : []),
|
|
59
66
|
].join("\n");
|
package/bin/commands/check.mjs
CHANGED
|
@@ -249,6 +249,7 @@ export async function runCheck(opts) {
|
|
|
249
249
|
...(batchData.viewerAuthorization && { viewerAuthorization: batchData.viewerAuthorization }),
|
|
250
250
|
status,
|
|
251
251
|
baseBranch: batchData.baseRefName,
|
|
252
|
+
...(batchData.baseRefOid && { baseRefOid: batchData.baseRefOid }),
|
|
252
253
|
mergeStatus,
|
|
253
254
|
checks: {
|
|
254
255
|
passing: merged.passing,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { loadConfig } from "../../config/load.mjs";
|
|
2
|
-
import { inlineCode } from "../../util/markdown.mjs";
|
|
3
2
|
import { renderResolveCommand } from "./render.mjs";
|
|
4
3
|
function renderEscalateAuthor(item) {
|
|
5
4
|
return [`@${item.author}`, item.authorType, item.authorAssociation].filter(Boolean).join(" · ");
|
|
@@ -194,10 +193,6 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
|
|
|
194
193
|
if (removal.beforeCommitOid)
|
|
195
194
|
lines.push(`- queue commit: \`${removal.beforeCommitOid}\``);
|
|
196
195
|
}
|
|
197
|
-
if (escalate.stack) {
|
|
198
|
-
const s = escalate.stack;
|
|
199
|
-
lines.push("", "## GitHub stack", "", `- layer: \`${s.position}\` of \`${s.size}\` in stack \`${s.number}\``, `- stack base: ${inlineCode(s.baseRefName)}`);
|
|
200
|
-
}
|
|
201
196
|
if (escalate.authorization && escalate.authorization.length > 0) {
|
|
202
197
|
lines.push("");
|
|
203
198
|
lines.push("## Authorization");
|
|
@@ -231,10 +226,6 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
|
|
|
231
226
|
return lines.join("\n");
|
|
232
227
|
}
|
|
233
228
|
export function buildEscalateSuggestion(triggers, detail) {
|
|
234
|
-
if (triggers.includes("stacked-pr")) {
|
|
235
|
-
const selector = detail ?? "<pr>";
|
|
236
|
-
return `This PR belongs to a GitHub stack, so Shepherd will not emit a merge command. \`gh pr merge\` targets the PR's own base branch — for a mid-stack layer that is the unmerged parent branch, not the stack's base — and auto-merge is unsupported on stacked PRs. Merge from the GitHub stack UI, or run \`gh stack merge --squash ${selector}\` (requires the \`github/gh-stack\` extension — run \`gh extension install github/gh-stack\` first if it's not installed), which lands this PR and every unmerged layer below it.`;
|
|
237
|
-
}
|
|
238
229
|
if (triggers.includes("check-follow-up-unavailable")) {
|
|
239
230
|
return "One or more failing checks have no autonomous follow-up available. Use the displayed conclusion, run or URL, and included evidence to handle them manually.";
|
|
240
231
|
}
|
|
@@ -257,9 +248,5 @@ export function buildEscalateSuggestion(triggers, detail) {
|
|
|
257
248
|
const attempts = loadConfig().iterate.fixAttemptsPerThread;
|
|
258
249
|
return `The same thread(s) remain unresolved after their pending review commands were returned for ${attempts} FIX_CODE ticks. Automated iteration is paused for a manual decision.`;
|
|
259
250
|
}
|
|
260
|
-
if (triggers.includes("bot-cr-not-dismissed")) {
|
|
261
|
-
const ids = detail ? ` (review IDs: ${detail})` : "";
|
|
262
|
-
return `Bot CHANGES_REQUESTED review(s) remained undismissed past the stall window${ids}. The agent likely dropped \`--dismiss-review-ids\` from a prior apply command. Dismiss the review(s) manually (or re-run \`pr-shepherd apply review\` with the IDs) to unblock the PR.`;
|
|
263
|
-
}
|
|
264
251
|
return "Ambiguous state — automated handling cannot proceed safely. Inspect the PR and act manually.";
|
|
265
252
|
}
|
|
@@ -20,6 +20,8 @@ interface HandleFixCodeContext {
|
|
|
20
20
|
surfacedApprovals: Review[];
|
|
21
21
|
botUsernames: NormalizedBotUsernames;
|
|
22
22
|
ruleAutoResolveThreadIds?: string[];
|
|
23
|
+
/** Verified stack-repair guidance, when ancestry is stale. */
|
|
24
|
+
repairInstructions?: string[];
|
|
23
25
|
}
|
|
24
26
|
export declare function handleFixCode(ctx: HandleFixCodeContext): Promise<IterateResult>;
|
|
25
27
|
export {};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
import { readFixAttempts, writeFixAttempts, } from "../../state/fix-attempts.mjs";
|
|
3
|
-
import { readBotCrSeenState, writeBotCrSeenState, updateBotCrSeenState, } from "../../state/bot-cr-seen.mjs";
|
|
4
3
|
import { toAgentThread, toAgentComment, toAgentChecks } from "../../reporters/agent.mjs";
|
|
5
4
|
import { hashBody, markSeen } from "../../state/seen-comments.mjs";
|
|
6
5
|
import { checkEscalateTriggers, validateBaseBranch, buildEscalateSuggestion, buildEscalateHumanMessage, } from "./escalate.mjs";
|
|
@@ -17,15 +16,15 @@ import { formatPrUrl } from "../../pr-reference.mjs";
|
|
|
17
16
|
function checkRequiresHumanFollowUp(check) {
|
|
18
17
|
if (check.rerunCommand)
|
|
19
18
|
return false;
|
|
20
|
-
// Once GitHub advances beyond the original attempt, Shepherd's one autonomous rerun has
|
|
21
|
-
// already been consumed. Hand the repeated failure off even when logs are available; the
|
|
22
|
-
// human still receives that evidence in the escalation payload.
|
|
23
|
-
if (check.runAttempt !== undefined && check.runAttempt > 1)
|
|
24
|
-
return true;
|
|
25
19
|
if (check.conclusion === "ACTION_REQUIRED" ||
|
|
26
20
|
check.conclusion === "CANCELLED" ||
|
|
27
21
|
check.conclusion === "STARTUP_FAILURE")
|
|
28
22
|
return true;
|
|
23
|
+
// A later attempt cannot be rerun automatically, but its included log can still
|
|
24
|
+
// identify a code or configuration fix for the agent. Only a later failure with
|
|
25
|
+
// no actionable evidence needs a human handoff.
|
|
26
|
+
if (check.runAttempt !== undefined && check.runAttempt > 1)
|
|
27
|
+
return !check.logExcerpt?.trim();
|
|
29
28
|
// An external check's direct URL is actionable evidence: the agent can inspect the
|
|
30
29
|
// provider and/or reproduce the reported failure locally. Only a truly bare check
|
|
31
30
|
// has no autonomous investigation path.
|
|
@@ -68,7 +67,7 @@ function pendingReviewCommands(resolveCommand, resolveOnlyCommand) {
|
|
|
68
67
|
return Object.keys(pending).length > 0 ? pending : undefined;
|
|
69
68
|
}
|
|
70
69
|
export async function handleFixCode(ctx) {
|
|
71
|
-
const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, botUsernames, ruleAutoResolveThreadIds, } = ctx;
|
|
70
|
+
const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, botUsernames, ruleAutoResolveThreadIds, repairInstructions, } = ctx;
|
|
72
71
|
const prReference = formatPrUrl(report.repo, prNumber);
|
|
73
72
|
const failingChecks = report.checks.failing;
|
|
74
73
|
const annotatedExtra = checksWithActionableAnnotations(report).filter((c) => c.category !== "failing");
|
|
@@ -105,37 +104,6 @@ export async function handleFixCode(ctx) {
|
|
|
105
104
|
...(report.comments.minimizeIds ?? report.comments.actionable.map((comment) => comment.id)),
|
|
106
105
|
...reviewSummaryIds,
|
|
107
106
|
], changesRequestedReviewsForWork, checks, prReference, botUsernames, ruleAutoResolveThreadIds, report.viewerAuthorization, allThreads, resolveOtherHumanThreads);
|
|
108
|
-
const botCrReviews = report.changesRequestedReviews.filter((r) => (!isHumanAuthor(r) || isConfiguredBotAuthor(r, botUsernames)) &&
|
|
109
|
-
report.viewerAuthorization?.viewerCanAdminister === true);
|
|
110
|
-
const botCrStateKey = { owner: repoOwner, repo: repoName, pr: prNumber };
|
|
111
|
-
const previousBotCrState = await readBotCrSeenState(botCrStateKey);
|
|
112
|
-
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
113
|
-
const { next: nextBotCrState, staleIds: staleBotCrIds } = updateBotCrSeenState(previousBotCrState, botCrReviews, nowSeconds, stallTimeoutSeconds);
|
|
114
|
-
await writeBotCrSeenState(botCrStateKey, nextBotCrState);
|
|
115
|
-
if (staleBotCrIds.length > 0) {
|
|
116
|
-
const { resolveCommand, resolveOnlyCommand } = buildReviewCommands(toAgentChecks(failingChecks));
|
|
117
|
-
const pending = pendingReviewCommands(resolveCommand, resolveOnlyCommand);
|
|
118
|
-
const escalateBase = {
|
|
119
|
-
triggers: ["bot-cr-not-dismissed"],
|
|
120
|
-
unresolvedThreads: [...report.threads.actionable, ...report.threads.resolutionOnly].map(toAgentThread),
|
|
121
|
-
ambiguousComments: report.comments.actionable.map(toAgentComment),
|
|
122
|
-
changesRequestedReviews: report.changesRequestedReviews,
|
|
123
|
-
...(firstLookSummaries.length > 0 && { firstLookSummaries }),
|
|
124
|
-
...(editedSummaries.length > 0 && { editedSummaries }),
|
|
125
|
-
...(pending && { pendingReviewCommands: pending }),
|
|
126
|
-
suggestion: buildEscalateSuggestion(["bot-cr-not-dismissed"], staleBotCrIds.join(", ")),
|
|
127
|
-
};
|
|
128
|
-
return {
|
|
129
|
-
...base,
|
|
130
|
-
action: "escalate",
|
|
131
|
-
escalate: {
|
|
132
|
-
...escalateBase,
|
|
133
|
-
humanMessage: buildEscalateHumanMessage(escalateBase, prReference, {
|
|
134
|
-
merge: opts.merge,
|
|
135
|
-
}),
|
|
136
|
-
},
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
107
|
const escalateTriggers = countFixCodeAttempt
|
|
140
108
|
? checkEscalateTriggers(retryableActionableThreads, priorThreadAttempts)
|
|
141
109
|
: { triggers: [], thrashHistory: undefined };
|
|
@@ -216,8 +184,9 @@ export async function handleFixCode(ctx) {
|
|
|
216
184
|
const commentMinimizeIds = report.comments.minimizeIds ?? actionableComments.map((c) => c.id);
|
|
217
185
|
const belongsToActiveWorkflowRun = (check) => check.runId !== null && inProgressWorkflowRunIds.has(check.runId);
|
|
218
186
|
const manualFollowUpChecks = failingAgentChecks.filter((check) => !belongsToActiveWorkflowRun(check) && checkRequiresHumanFollowUp(check));
|
|
219
|
-
const exhaustedAttempts =
|
|
220
|
-
const
|
|
187
|
+
const exhaustedAttempts = failingAgentChecks.filter((check) => check.runAttempt !== undefined && check.runAttempt > 1);
|
|
188
|
+
const manualExhaustedAttempts = exhaustedAttempts.filter((check) => manualFollowUpChecks.includes(check));
|
|
189
|
+
const hasBehindBaseRecovery = isBehind && manualExhaustedAttempts.length > 0;
|
|
221
190
|
const hasAutonomousWork = hasConflicts ||
|
|
222
191
|
hasBehindBaseRecovery ||
|
|
223
192
|
threads.length > 0 ||
|
|
@@ -235,8 +204,8 @@ export async function handleFixCode(ctx) {
|
|
|
235
204
|
if (manualFollowUpChecks.length > 0 && !hasAutonomousWork) {
|
|
236
205
|
const { resolveCommand, resolveOnlyCommand } = buildReviewCommands(failingAgentChecks);
|
|
237
206
|
const pending = pendingReviewCommands(resolveCommand, resolveOnlyCommand);
|
|
238
|
-
const checkSuggestion =
|
|
239
|
-
? `GitHub reports a later workflow attempt (${
|
|
207
|
+
const checkSuggestion = manualExhaustedAttempts.length > 0
|
|
208
|
+
? `GitHub reports a later workflow attempt (${manualExhaustedAttempts
|
|
240
209
|
.map((check) => `${check.runId ?? check.name}: attempt ${check.runAttempt}`)
|
|
241
210
|
.join(", ")}), so Shepherd's single rerun allowance is exhausted. Use the included evidence to handle the repeated failure manually before resuming.`
|
|
242
211
|
: buildEscalateSuggestion(["check-follow-up-unavailable"]);
|
|
@@ -303,6 +272,9 @@ export async function handleFixCode(ctx) {
|
|
|
303
272
|
const firstLookThreads = report.threads.firstLook;
|
|
304
273
|
const firstLookComments = report.comments.firstLook;
|
|
305
274
|
const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prReference, cancelled.length, firstLookThreads, firstLookComments, firstLookSummaries, editedSummaries, inProgressRunIds, resolutionOnlyThreads, resolveOnlyCommand, behindBaseHint, isBehind, report.viewerAuthorization?.viewerCanUpdate === true, exhaustedAttempts.length > 0);
|
|
275
|
+
if (repairInstructions && repairInstructions.length > 0) {
|
|
276
|
+
instructions.unshift(...repairInstructions);
|
|
277
|
+
}
|
|
306
278
|
const prospectiveResult = {
|
|
307
279
|
...base,
|
|
308
280
|
baseBranch: baseLookup.branch,
|