pr-shepherd 0.46.2 → 0.46.3

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
3
  "description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
4
- "version": "0.46.2",
4
+ "version": "0.46.3",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
@@ -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 logExcerpt = jobInfo?.jobId
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)
@@ -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 startupFailureChecks = result.checkSuitesComplete
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);
@@ -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" &&
@@ -185,13 +194,19 @@ export async function handleFixCode(ctx) {
185
194
  checks.some((check) => (check.annotations?.length ?? 0) > 0) ||
186
195
  failingAgentChecks.some((check) => belongsToActiveWorkflowRun(check) || !checkRequiresHumanFollowUp(check));
187
196
  if (manualFollowUpChecks.length > 0 && !hasAutonomousWork) {
197
+ const exhaustedAttempts = manualFollowUpChecks.filter((check) => check.runAttempt !== undefined && check.runAttempt > 1);
198
+ const checkSuggestion = exhaustedAttempts.length > 0
199
+ ? `GitHub reports a later workflow attempt (${exhaustedAttempts
200
+ .map((check) => `${check.runId ?? check.name}: attempt ${check.runAttempt}`)
201
+ .join(", ")}), so Shepherd's single rerun allowance is exhausted. Use the included evidence to handle the repeated failure manually before resuming.`
202
+ : buildEscalateSuggestion(["check-follow-up-unavailable"]);
188
203
  const checkEscalateBase = {
189
204
  triggers: ["check-follow-up-unavailable"],
190
205
  unresolvedThreads: [],
191
206
  ambiguousComments: [],
192
207
  changesRequestedReviews,
193
208
  checks: manualFollowUpChecks,
194
- suggestion: buildEscalateSuggestion(["check-follow-up-unavailable"]),
209
+ suggestion: checkSuggestion,
195
210
  };
196
211
  return {
197
212
  ...base,
@@ -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();
@@ -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 }),
@@ -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`. */
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.46.2",
3
+ "version": "0.46.3",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
5
5
  "keywords": [
6
6
  "automation",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.46.2",
3
+ "version": "0.46.3",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for Codex.",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "pr-shepherd": {
4
4
  "command": "npx",
5
- "args": ["--yes", "--package", "pr-shepherd@0.46.2", "pr-shepherd-mcp"]
5
+ "args": ["--yes", "--package", "pr-shepherd@0.46.3", "pr-shepherd-mcp"]
6
6
  }
7
7
  }
8
8
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "pr-shepherd": {
3
3
  "command": "npx",
4
- "args": ["--yes", "--package", "pr-shepherd@0.46.2", "pr-shepherd-mcp"]
4
+ "args": ["--yes", "--package", "pr-shepherd@0.46.3", "pr-shepherd-mcp"]
5
5
  }
6
6
  }
@@ -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 this from `repositoryPermission`. Run the printed command at most once. A run still in progress, an `ACTION_REQUIRED` run (paused pending manual workflow approval — a rerun cannot grant that approval), or a check whose runId does not resolve to a GitHub Actions workflow never gets `[rerun authorized]`, regardless of role. 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.
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