pr-shepherd 0.46.2 → 0.46.4
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/bin/checks/triage.mjs +17 -4
- package/bin/cli/fix-formatter.mjs +2 -1
- package/bin/commands/check.mjs +2 -1
- package/bin/commands/iterate/check-instructions.d.mts +9 -0
- package/bin/commands/iterate/check-instructions.mjs +17 -0
- package/bin/commands/iterate/escalate.mjs +2 -1
- package/bin/commands/iterate/fix-code.mjs +19 -2
- package/bin/commands/iterate/render.d.mts +1 -1
- package/bin/commands/iterate/render.mjs +12 -4
- package/bin/commands/iterate/stall.mjs +1 -1
- package/bin/reporters/agent.mjs +1 -0
- package/bin/types/github.d.mts +2 -0
- package/bin/types/report.d.mts +2 -0
- package/package.json +1 -1
- 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/checks/triage.mjs
CHANGED
|
@@ -12,18 +12,18 @@ export function triageFailingChecks(failingChecks, repo, stateKey) {
|
|
|
12
12
|
return Promise.all(failingChecks.map((c) => triageCheck(c, repo, jobsCache, stateKey)));
|
|
13
13
|
}
|
|
14
14
|
async function triageCheck(check, repo, jobsCache, stateKey) {
|
|
15
|
-
if (check.runId === null ||
|
|
16
|
-
check.conclusion === "CANCELLED" ||
|
|
17
|
-
check.conclusion === "STARTUP_FAILURE") {
|
|
15
|
+
if (check.runId === null || check.conclusion === "STARTUP_FAILURE") {
|
|
18
16
|
return { ...check };
|
|
19
17
|
}
|
|
20
18
|
const jobs = await fetchJobs(check.runId, repo, jobsCache, stateKey);
|
|
21
19
|
const jobInfo = jobs ? pickJobInfo(jobs, check.name) : undefined;
|
|
22
|
-
const
|
|
20
|
+
const runAttempt = jobs ? pickRunAttempt(jobs) : undefined;
|
|
21
|
+
const logExcerpt = check.conclusion !== "CANCELLED" && jobInfo?.jobId
|
|
23
22
|
? await fetchJobLogExcerpt(jobInfo.jobId, repo, stateKey, jobInfo.jobConclusion != null)
|
|
24
23
|
: undefined;
|
|
25
24
|
return {
|
|
26
25
|
...check,
|
|
26
|
+
...(runAttempt !== undefined && { runAttempt }),
|
|
27
27
|
...(jobInfo?.workflowName !== undefined && { workflowName: jobInfo.workflowName }),
|
|
28
28
|
...(jobInfo?.jobName !== undefined && { jobName: jobInfo.jobName }),
|
|
29
29
|
...(jobInfo?.failedStep !== undefined && { failedStep: jobInfo.failedStep }),
|
|
@@ -72,6 +72,7 @@ async function fetchStartupFailureChecksUncached(repo, headSha, prNumber, stateK
|
|
|
72
72
|
}
|
|
73
73
|
function workflowRunToCheckRun(run) {
|
|
74
74
|
const summary = run.display_title?.trim() || undefined;
|
|
75
|
+
const runAttempt = normalizeRunAttempt(run.run_attempt);
|
|
75
76
|
return {
|
|
76
77
|
name: run.name?.trim() || `workflow run ${run.id}`,
|
|
77
78
|
status: "COMPLETED",
|
|
@@ -80,9 +81,21 @@ function workflowRunToCheckRun(run) {
|
|
|
80
81
|
detailsUrl: run.html_url,
|
|
81
82
|
event: run.event,
|
|
82
83
|
runId: String(run.id),
|
|
84
|
+
...(runAttempt !== undefined && { runAttempt }),
|
|
83
85
|
...(summary !== undefined && { summary }),
|
|
84
86
|
};
|
|
85
87
|
}
|
|
88
|
+
function normalizeRunAttempt(value) {
|
|
89
|
+
return Number.isSafeInteger(value) && (value ?? 0) > 0 ? value : undefined;
|
|
90
|
+
}
|
|
91
|
+
function pickRunAttempt(jobs) {
|
|
92
|
+
for (const job of jobs) {
|
|
93
|
+
const attempt = normalizeRunAttempt(job.run_attempt);
|
|
94
|
+
if (attempt !== undefined)
|
|
95
|
+
return attempt;
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
86
99
|
function runBelongsToPr(run, prNumber, headSha) {
|
|
87
100
|
return (run.pull_requests ?? []).some((pr) => pr.number === prNumber && (pr.head?.sha ?? headSha) === headSha);
|
|
88
101
|
}
|
|
@@ -63,12 +63,13 @@ export function formatFixCodeResult(header, result) {
|
|
|
63
63
|
? `external \`${ch.detailsUrl}\``
|
|
64
64
|
: "(no runId)";
|
|
65
65
|
const conclusionTag = ch.conclusion !== null ? ` [conclusion: ${ch.conclusion}]` : "";
|
|
66
|
+
const attemptTag = ch.runAttempt !== undefined ? ` [attempt: ${ch.runAttempt}]` : "";
|
|
66
67
|
const scopeTag = ch.scope
|
|
67
68
|
? ` [scope: ${ch.scope}${ch.commitOid ? `, commit: ${ch.commitOid}` : ""}]`
|
|
68
69
|
: "";
|
|
69
70
|
const rerunTag = ch.rerunCommand ? " [rerun authorized]" : "";
|
|
70
71
|
const lines = [
|
|
71
|
-
`- ${locator} — \`${workflowPrefix}${jobLabel}\`${conclusionTag}${scopeTag}${rerunTag}`,
|
|
72
|
+
`- ${locator} — \`${workflowPrefix}${jobLabel}\`${conclusionTag}${attemptTag}${scopeTag}${rerunTag}`,
|
|
72
73
|
];
|
|
73
74
|
if (ch.conclusion !== "CANCELLED") {
|
|
74
75
|
if (ch.failedStep)
|
package/bin/commands/check.mjs
CHANGED
|
@@ -40,7 +40,8 @@ export async function runCheck(opts) {
|
|
|
40
40
|
if (mergeStatus.state === "MERGED" || mergeStatus.state === "CLOSED") {
|
|
41
41
|
return buildTerminalReport(prNumber, repo, batchData, mergeStatus, mergeStatus.state);
|
|
42
42
|
}
|
|
43
|
-
const
|
|
43
|
+
const startupFailuresNeedAttempt = batchData.checks.some((check) => check.source === "startup_failure" && check.runAttempt === undefined);
|
|
44
|
+
const startupFailureChecks = result.checkSuitesComplete && !startupFailuresNeedAttempt
|
|
44
45
|
? []
|
|
45
46
|
: await fetchStartupFailureChecks(repo, batchData.headRefOid, prNumber, stateKey);
|
|
46
47
|
const allChecks = mergeStartupFailureChecks(batchData.checks, startupFailureChecks);
|
|
@@ -11,6 +11,15 @@ export declare function buildCrStaleClause(reviews: Review[]): string;
|
|
|
11
11
|
* discarding the rest of the user's config.
|
|
12
12
|
*/
|
|
13
13
|
export declare function buildBehindBaseHintInstruction(baseBranch: string, hint: string, isBehind: boolean): string[];
|
|
14
|
+
/**
|
|
15
|
+
* Give one branch-refresh recovery path after Shepherd's single workflow rerun has failed.
|
|
16
|
+
* The fetched PR base branch is raw context; the caller still owns repository-specific git
|
|
17
|
+
* mechanics and decides whether the base contains a relevant fix.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildRepeatedWorkflowBranchRecoveryInstructions(baseBranch: string, hasExhaustedWorkflowRerun: boolean, branch: {
|
|
20
|
+
isBehind: boolean;
|
|
21
|
+
hasConflicts: boolean;
|
|
22
|
+
}): string[];
|
|
14
23
|
/**
|
|
15
24
|
* Build the `Run the apply review: command` instruction. Steps stay here (not in the skill)
|
|
16
25
|
* whenever the *unmodified, as-printed* command is unsafe without them:
|
|
@@ -20,6 +20,23 @@ export function buildBehindBaseHintInstruction(baseBranch, hint, isBehind) {
|
|
|
20
20
|
return [];
|
|
21
21
|
return [`The branch is behind PR base branch \`${baseBranch}\`. ${trimmedHint} before pushing.`];
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Give one branch-refresh recovery path after Shepherd's single workflow rerun has failed.
|
|
25
|
+
* The fetched PR base branch is raw context; the caller still owns repository-specific git
|
|
26
|
+
* mechanics and decides whether the base contains a relevant fix.
|
|
27
|
+
*/
|
|
28
|
+
export function buildRepeatedWorkflowBranchRecoveryInstructions(baseBranch, hasExhaustedWorkflowRerun, branch) {
|
|
29
|
+
if (!hasExhaustedWorkflowRerun || (!branch.isBehind && !branch.hasConflicts))
|
|
30
|
+
return [];
|
|
31
|
+
const state = branch.hasConflicts ? "conflicts with" : "is behind";
|
|
32
|
+
const instructions = [
|
|
33
|
+
`The workflow rerun still fails while the branch ${state} PR base branch \`${baseBranch}\`. Inspect the current base branch for an existing fix before choosing a remediation.`,
|
|
34
|
+
];
|
|
35
|
+
instructions.push(branch.hasConflicts
|
|
36
|
+
? `Rebase or otherwise update the PR branch from \`${baseBranch}\` according to repository conventions, resolving conflicts as part of that update.`
|
|
37
|
+
: `Rebase or otherwise update the PR branch from \`${baseBranch}\` according to repository conventions.`);
|
|
38
|
+
return instructions;
|
|
39
|
+
}
|
|
23
40
|
/**
|
|
24
41
|
* Build the `Run the apply review: command` instruction. Steps stay here (not in the skill)
|
|
25
42
|
* whenever the *unmodified, as-printed* command is unsafe without them:
|
|
@@ -41,8 +41,9 @@ function renderEscalateCheck(check) {
|
|
|
41
41
|
const workflowPrefix = check.workflowName ? `${check.workflowName} › ` : "";
|
|
42
42
|
const jobLabel = check.jobName ?? check.name;
|
|
43
43
|
const conclusion = check.conclusion ?? "UNKNOWN";
|
|
44
|
+
const attempt = check.runAttempt !== undefined ? ` [attempt: ${check.runAttempt}]` : "";
|
|
44
45
|
const lines = [
|
|
45
|
-
`- ${renderCheckTarget(check)} — \`${workflowPrefix}${jobLabel}\` [conclusion: ${conclusion}]${renderCheckScope(check)}`,
|
|
46
|
+
`- ${renderCheckTarget(check)} — \`${workflowPrefix}${jobLabel}\` [conclusion: ${conclusion}]${attempt}${renderCheckScope(check)}`,
|
|
46
47
|
];
|
|
47
48
|
if (check.failedStep)
|
|
48
49
|
lines.push(` > failed step: ${check.failedStep}`);
|
|
@@ -17,6 +17,11 @@ import { formatPrUrl } from "../../pr-reference.mjs";
|
|
|
17
17
|
function checkRequiresHumanFollowUp(check) {
|
|
18
18
|
if (check.rerunCommand)
|
|
19
19
|
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;
|
|
20
25
|
if (check.conclusion === "ACTION_REQUIRED" ||
|
|
21
26
|
check.conclusion === "CANCELLED" ||
|
|
22
27
|
check.conclusion === "STARTUP_FAILURE")
|
|
@@ -138,9 +143,13 @@ export async function handleFixCode(ctx) {
|
|
|
138
143
|
const actionsRunIds = new Set(failingChecks.flatMap((c) => c.runId !== null && (c.source === "startup_failure" || c.workflowName !== undefined)
|
|
139
144
|
? [c.runId]
|
|
140
145
|
: []));
|
|
146
|
+
const initialAttemptRunIds = new Set(failingChecks.flatMap((c) => (c.runId !== null && c.runAttempt === 1 ? [c.runId] : [])));
|
|
141
147
|
const failingAgentChecks = toAgentChecks(failingChecks).map((c) => rerunAuthorized &&
|
|
142
148
|
c.runId &&
|
|
143
149
|
actionsRunIds.has(c.runId) &&
|
|
150
|
+
// GitHub increments run_attempt after every rerun. Recommend at most one rerun by limiting
|
|
151
|
+
// the command to the original attempt; missing attempt metadata is denied conservatively.
|
|
152
|
+
initialAttemptRunIds.has(c.runId) &&
|
|
144
153
|
// ACTION_REQUIRED means the run is paused pending manual workflow approval; rerunning does
|
|
145
154
|
// not grant that approval, so no rerun command applies.
|
|
146
155
|
c.conclusion !== "ACTION_REQUIRED" &&
|
|
@@ -171,7 +180,10 @@ export async function handleFixCode(ctx) {
|
|
|
171
180
|
const allCommentIds = [...commentMinimizeIds, ...reviewSummaryIds];
|
|
172
181
|
const belongsToActiveWorkflowRun = (check) => check.runId !== null && inProgressWorkflowRunIds.has(check.runId);
|
|
173
182
|
const manualFollowUpChecks = failingAgentChecks.filter((check) => !belongsToActiveWorkflowRun(check) && checkRequiresHumanFollowUp(check));
|
|
183
|
+
const exhaustedAttempts = manualFollowUpChecks.filter((check) => check.runAttempt !== undefined && check.runAttempt > 1);
|
|
184
|
+
const hasBehindBaseRecovery = isBehind && exhaustedAttempts.length > 0;
|
|
174
185
|
const hasAutonomousWork = hasConflicts ||
|
|
186
|
+
hasBehindBaseRecovery ||
|
|
175
187
|
threads.length > 0 ||
|
|
176
188
|
resolutionOnlyThreads.length > 0 ||
|
|
177
189
|
actionableComments.length > 0 ||
|
|
@@ -185,13 +197,18 @@ export async function handleFixCode(ctx) {
|
|
|
185
197
|
checks.some((check) => (check.annotations?.length ?? 0) > 0) ||
|
|
186
198
|
failingAgentChecks.some((check) => belongsToActiveWorkflowRun(check) || !checkRequiresHumanFollowUp(check));
|
|
187
199
|
if (manualFollowUpChecks.length > 0 && !hasAutonomousWork) {
|
|
200
|
+
const checkSuggestion = exhaustedAttempts.length > 0
|
|
201
|
+
? `GitHub reports a later workflow attempt (${exhaustedAttempts
|
|
202
|
+
.map((check) => `${check.runId ?? check.name}: attempt ${check.runAttempt}`)
|
|
203
|
+
.join(", ")}), so Shepherd's single rerun allowance is exhausted. Use the included evidence to handle the repeated failure manually before resuming.`
|
|
204
|
+
: buildEscalateSuggestion(["check-follow-up-unavailable"]);
|
|
188
205
|
const checkEscalateBase = {
|
|
189
206
|
triggers: ["check-follow-up-unavailable"],
|
|
190
207
|
unresolvedThreads: [],
|
|
191
208
|
ambiguousComments: [],
|
|
192
209
|
changesRequestedReviews,
|
|
193
210
|
checks: manualFollowUpChecks,
|
|
194
|
-
suggestion:
|
|
211
|
+
suggestion: checkSuggestion,
|
|
195
212
|
};
|
|
196
213
|
return {
|
|
197
214
|
...base,
|
|
@@ -242,7 +259,7 @@ export async function handleFixCode(ctx) {
|
|
|
242
259
|
}
|
|
243
260
|
const firstLookThreads = report.threads.firstLook;
|
|
244
261
|
const firstLookComments = report.comments.firstLook;
|
|
245
|
-
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);
|
|
262
|
+
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);
|
|
246
263
|
const prospectiveResult = {
|
|
247
264
|
...base,
|
|
248
265
|
baseBranch: baseLookup.branch,
|
|
@@ -2,4 +2,4 @@ import type { AgentThread, AgentComment, AgentCheck, Review, ResolveCommand, Fir
|
|
|
2
2
|
/** Render a resolve command as a shell snippet. Appends `--require-sha "$HEAD_SHA"` when set. */
|
|
3
3
|
export declare function renderResolveCommand(rc: ResolveCommand): string;
|
|
4
4
|
export declare function buildFixInstructions(threads: AgentThread[], actionableComments: AgentComment[], checks: AgentCheck[], changesRequestedReviews: Review[], baseBranch: string, resolveCommand: ResolveCommand, hasConflicts: boolean, prReference: string | number, cancelledCount: number, firstLookThreads?: FirstLookThread[], firstLookComments?: FirstLookComment[], firstLookSummaries?: Review[], editedSummaries?: Review[], inProgressRunIds?: string[], resolutionOnlyThreads?: ReviewThread[], resolveOnlyCommand?: ResolveCommand, behindBaseHint?: string, // iterate.behindBaseHint — see buildBehindBaseHintInstruction
|
|
5
|
-
isBehind?: boolean, viewerCanUpdate?: boolean): string[];
|
|
5
|
+
isBehind?: boolean, viewerCanUpdate?: boolean, hasExhaustedWorkflowRerun?: boolean): string[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { renderShellCommand } from "../../cli/runner.mjs";
|
|
2
|
-
import { buildFailingCheckInstructions, buildCrStaleClause, buildBehindBaseHintInstruction, buildResolveCommandInstruction, buildFixCompletionInstruction, } from "./check-instructions.mjs";
|
|
2
|
+
import { buildFailingCheckInstructions, buildCrStaleClause, buildBehindBaseHintInstruction, buildRepeatedWorkflowBranchRecoveryInstructions, buildResolveCommandInstruction, buildFixCompletionInstruction, } from "./check-instructions.mjs";
|
|
3
3
|
import { SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE, buildShepherdJournalInstruction, } from "../shepherd-journal.mjs";
|
|
4
4
|
import { isFailingAgentCheck } from "../../checks/conclusions.mjs";
|
|
5
5
|
import { buildCommitSuggestionInstruction } from "../commit-suggestion-instruction.mjs";
|
|
@@ -11,11 +11,16 @@ export function renderResolveCommand(rc) {
|
|
|
11
11
|
return renderShellCommand(parts);
|
|
12
12
|
}
|
|
13
13
|
export function buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseBranch, resolveCommand, hasConflicts, prReference, cancelledCount, firstLookThreads = [], firstLookComments = [], firstLookSummaries = [], editedSummaries = [], inProgressRunIds = [], resolutionOnlyThreads = [], resolveOnlyCommand, behindBaseHint = "", // iterate.behindBaseHint — see buildBehindBaseHintInstruction
|
|
14
|
-
isBehind = false, viewerCanUpdate = false) {
|
|
14
|
+
isBehind = false, viewerCanUpdate = false, hasExhaustedWorkflowRerun = false) {
|
|
15
15
|
const instructions = [];
|
|
16
16
|
const locatedThreads = threads.filter((thread) => thread.path !== null && thread.line !== null);
|
|
17
17
|
const unlocatedThreads = threads.filter((thread) => thread.path === null || thread.line === null);
|
|
18
18
|
const failingChecks = checks.filter((c) => isFailingAgentCheck(c));
|
|
19
|
+
const repeatedWorkflowBranchRecoveryInstructions = buildRepeatedWorkflowBranchRecoveryInstructions(baseBranch, hasExhaustedWorkflowRerun, {
|
|
20
|
+
isBehind,
|
|
21
|
+
hasConflicts,
|
|
22
|
+
});
|
|
23
|
+
const hasRepeatedWorkflowBranchRecovery = repeatedWorkflowBranchRecoveryInstructions.length > 0;
|
|
19
24
|
const hasAnnotations = checks.some((c) => (c.annotations?.length ?? 0) > 0);
|
|
20
25
|
const hasNonConflictHints = threads.length > 0 ||
|
|
21
26
|
failingChecks.length > 0 ||
|
|
@@ -41,7 +46,7 @@ isBehind = false, viewerCanUpdate = false) {
|
|
|
41
46
|
const sectionRef = actionableSections.length > 0 ? `under ${actionableSections.join(", ")}` : "above";
|
|
42
47
|
instructions.push(`Review each item ${sectionRef} and decide whether it needs a code change.`);
|
|
43
48
|
}
|
|
44
|
-
if (hasConflicts) {
|
|
49
|
+
if (hasConflicts && !hasRepeatedWorkflowBranchRecovery) {
|
|
45
50
|
instructions.push("The branch has merge conflicts (see `**branch**` above). Resolve them before committing.");
|
|
46
51
|
}
|
|
47
52
|
const firstLookTotal = firstLookThreads.length + firstLookComments.length;
|
|
@@ -76,7 +81,7 @@ isBehind = false, viewerCanUpdate = false) {
|
|
|
76
81
|
if (resolutionOnlyThreads.length > 0) {
|
|
77
82
|
instructions.push('Review the threads under `## Review threads to resolve` before running mutations. Use the generated commands as shown — see "Review-mutation routing" in the pr-shepherd skill for which flag applies to which ID.');
|
|
78
83
|
}
|
|
79
|
-
instructions.push(...buildFailingCheckInstructions(failingChecks));
|
|
84
|
+
instructions.push(...buildFailingCheckInstructions(failingChecks), ...repeatedWorkflowBranchRecoveryInstructions);
|
|
80
85
|
if (hasAnnotations) {
|
|
81
86
|
instructions.push("Inspect every referenced range under `## Check annotations` and apply any warranted change.");
|
|
82
87
|
}
|
|
@@ -90,6 +95,9 @@ isBehind = false, viewerCanUpdate = false) {
|
|
|
90
95
|
if (hasConflicts) {
|
|
91
96
|
instructions.push(`Commit any remaining conflict-resolution changes and push to the PR head branch${mutationSuffix}.`);
|
|
92
97
|
}
|
|
98
|
+
else if (hasRepeatedWorkflowBranchRecovery) {
|
|
99
|
+
instructions.push("Push the updated PR head branch before iterating again.");
|
|
100
|
+
}
|
|
93
101
|
else if (hasNonConflictHints) {
|
|
94
102
|
instructions.push("If you changed code, commit any remaining changes and push to the PR head branch, then run the remaining review mutations using the pushed commit SHA and iterate again with the same options. If you did not change code, do not commit and continue with the remaining steps.");
|
|
95
103
|
}
|
|
@@ -5,7 +5,7 @@ import { checksWithActionableAnnotations } from "../check-annotations.mjs";
|
|
|
5
5
|
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
6
6
|
function computeStallFingerprint(action, headSha, base, report, reviewSummaryIds) {
|
|
7
7
|
const checks = [
|
|
8
|
-
...report.checks.failing.map((f) => `failing:${f.name}:${f.conclusion}`),
|
|
8
|
+
...report.checks.failing.map((f) => `failing:${f.name}:${f.conclusion}:${f.runId ?? "no-run"}:${f.runAttempt ?? "unknown"}`),
|
|
9
9
|
...report.checks.inProgress.map((p) => `inProgress:${p.name}`),
|
|
10
10
|
].sort();
|
|
11
11
|
const threads = report.threads.actionable.map((t) => t.id).sort();
|
package/bin/reporters/agent.mjs
CHANGED
|
@@ -60,6 +60,7 @@ export function toAgentCheck(c) {
|
|
|
60
60
|
runId: c.runId,
|
|
61
61
|
detailsUrl: c.detailsUrl,
|
|
62
62
|
conclusion: c.conclusion,
|
|
63
|
+
...(c.runAttempt !== undefined && c.runAttempt > 1 && { runAttempt: c.runAttempt }),
|
|
63
64
|
...(c.workflowName !== undefined && { workflowName: c.workflowName }),
|
|
64
65
|
...(c.jobName !== undefined && { jobName: c.jobName }),
|
|
65
66
|
...(c.failedStep !== undefined && { failedStep: c.failedStep }),
|
package/bin/types/github.d.mts
CHANGED
|
@@ -30,6 +30,8 @@ export interface CheckRun {
|
|
|
30
30
|
detailsUrl: string;
|
|
31
31
|
event: string | null;
|
|
32
32
|
runId: string | null;
|
|
33
|
+
/** GitHub Actions workflow-run attempt number. Omitted for non-Actions or unavailable metadata. */
|
|
34
|
+
runAttempt?: number;
|
|
33
35
|
/** Commit scope that supplied this check. Omitted for ordinary PR-head checks. */
|
|
34
36
|
scope?: "merge_group";
|
|
35
37
|
/** Synthetic merge-group commit OID, when `scope` is `merge_group`. */
|
package/bin/types/report.d.mts
CHANGED
|
@@ -138,6 +138,8 @@ export interface AgentCheck {
|
|
|
138
138
|
logExcerpt?: string;
|
|
139
139
|
/** `gh run rerun` command, present only when the check has a runId and the viewer's repository role grants Actions rerun capability (WRITE+). */
|
|
140
140
|
rerunCommand?: string;
|
|
141
|
+
/** Workflow-run attempt number, surfaced only after the initial attempt. */
|
|
142
|
+
runAttempt?: number;
|
|
141
143
|
annotations?: CheckAnnotation[];
|
|
142
144
|
annotationOnly?: true;
|
|
143
145
|
scope?: "merge_group";
|
package/package.json
CHANGED
|
@@ -20,11 +20,11 @@ If the requested PR does not exist yet, review and commit the in-scope changes,
|
|
|
20
20
|
|
|
21
21
|
1. Parse an optional PR number, repository-qualified `owner/repo#N`, or GitHub PR URL and an optional `--merge` flag from `$ARGUMENTS`; otherwise let pr-shepherd infer the current branch PR. Reject any remaining argument. Follow the target repository's local `AGENTS.md` and `CLAUDE.md` standards while making changes.
|
|
22
22
|
|
|
23
|
-
2. For the CLI, convert supplied `owner/repo#N` to `https://github.com/owner/repo/pull/N`; otherwise pass the supplied URL or bare number unchanged, then run the poll command `pr-shepherd
|
|
23
|
+
2. For the CLI, convert supplied `owner/repo#N` to `https://github.com/owner/repo/pull/N`; otherwise pass the supplied URL or bare number unchanged, then run the canonical poll command `pr-shepherd [PR] --until-terminal`, omitting `[PR]` when none was supplied and appending `--merge` when requested. This command keeps ordinary `[WAIT]` and `[MARK_READY]` ticks inside the same invocation; it returns for agent-facing work, a quota warning, `[CANCEL]`, or `[ESCALATE]`. A qualified reference may name a fork or upstream repository: it is the GitHub target, while the current checkout continues to supply local git/config/rules context. Do not run `pr-shepherd iterate`. If the CLI is unavailable and the `iterate` MCP tool is available, first obtain a repository-qualified reference: use a supplied GitHub PR URL or `owner/repo#N` unchanged; for a bare number, run `gh pr view <number> --json url --jq .url`; when omitted, run `gh pr view --json url --jq .url`. If that does not produce one qualified PR reference, stop and report that MCP cannot safely determine the PR. Otherwise call `iterate` with that qualified reference, plus `merge: true` when `--merge` was supplied, and print its full result.
|
|
24
24
|
|
|
25
25
|
3. Print the full result and follow every returned `## Instructions` step exactly. For CLI output, run each printed mutation command when instructed. For MCP output, use MCP `apply` and `build_suggestion_patches` with the same qualified PR reference; do not run a shell `pr-shepherd apply` command.
|
|
26
26
|
|
|
27
|
-
4. After completing the returned instructions, repeat step 2 unless the action is `[CANCEL]` or `[ESCALATE]`, or the human directs you to stop. `
|
|
27
|
+
4. After completing the returned instructions, repeat step 2 with the same target and canonical options unless the action is `[CANCEL]` or `[ESCALATE]`, or the human directs you to stop. Preserve `--until-terminal` and any requested `--merge`; apply any polling-cadence adjustment printed by the CLI. Every other action is non-terminal: complete its instructions and rerun without asking whether to continue. `[FIX_CODE]` is always non-terminal, and only `[ESCALATE]` hands work to a human.
|
|
28
28
|
|
|
29
29
|
## Playbooks
|
|
30
30
|
|
|
@@ -46,7 +46,7 @@ Match each failure's `[conclusion: …]` tag under `## Failing checks` to a rule
|
|
|
46
46
|
|
|
47
47
|
More specific rows win over the general "GitHub Actions failure" row — check conclusion first.
|
|
48
48
|
|
|
49
|
-
A `[rerun authorized]` tag with a `rerun:` command means the viewer's repository role grants GitHub's Actions rerun capability (WRITE+) — Shepherd verified
|
|
49
|
+
A `[rerun authorized]` tag with a `rerun:` command means the viewer's repository role grants GitHub's Actions rerun capability (WRITE+) and GitHub reports the original workflow attempt — Shepherd verified these from `repositoryPermission` and `run_attempt`. Run the printed command at most once. Later attempts carry an `[attempt: N]` tag, never get another command, and return `[ESCALATE]` when no other autonomous work remains, even when a log excerpt exists. A run still in progress, an `ACTION_REQUIRED` run (paused pending manual workflow approval — a rerun cannot grant that approval), a check whose runId does not resolve to a GitHub Actions workflow, or a run whose attempt metadata is unavailable never gets `[rerun authorized]`. When a check has no autonomous follow-up and no other agent work remains, Shepherd returns `[ESCALATE]`; do not invent a handoff from a `[FIX_CODE]` result.
|
|
50
50
|
|
|
51
51
|
When several bullets share one runId (matrix jobs from the same run), the `rerun:` command is printed once, on the first bullet; every bullet for that runId still carries `[rerun authorized]` and is covered by that single command — do not run it more than once.
|
|
52
52
|
|