pr-shepherd 0.26.1 → 0.26.2

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.26.1",
4
+ "version": "0.26.2",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
@@ -20,7 +20,7 @@ export function classifyChecks(checks) {
20
20
  const config = loadConfig();
21
21
  const relevantEvents = new Set(config.checks.ciTriggerEvents);
22
22
  const isIgnored = buildIgnoreMatcher(config.ignoreChecks ?? []);
23
- return checks.filter((c) => !isIgnored(c.name)).map((c) => classify(c, relevantEvents));
23
+ return checks.map((c) => isIgnored(c.name) ? { ...c, category: "ignored" } : classify(c, relevantEvents));
24
24
  }
25
25
  function buildIgnoreMatcher(patterns) {
26
26
  if (patterns.length === 0)
@@ -51,7 +51,7 @@ function classify(check, relevantEvents) {
51
51
  }
52
52
  /** Compute a high-level CI verdict from a list of classified checks. */
53
53
  export function getCiVerdict(classified) {
54
- const relevant = classified.filter((c) => c.category !== "filtered" && c.category !== "skipped");
54
+ const relevant = classified.filter((c) => c.category !== "filtered" && c.category !== "skipped" && c.category !== "ignored");
55
55
  const anyInProgress = relevant.some((c) => c.category === "in_progress");
56
56
  const anyFailing = relevant.some((c) => c.category === "failing");
57
57
  // When there are no relevant checks (e.g. docs-only PR where all checks are filtered/skipped),
@@ -59,5 +59,6 @@ export function getCiVerdict(classified) {
59
59
  const allPassed = !anyInProgress && !anyFailing;
60
60
  const hasChecks = relevant.length > 0;
61
61
  const filteredNames = classified.filter((c) => c.category === "filtered").map((c) => c.name);
62
- return { allPassed, hasChecks, anyInProgress, anyFailing, filteredNames };
62
+ const ignoredNames = Array.from(new Set(classified.filter((c) => c.category === "ignored").map((c) => c.name)));
63
+ return { allPassed, hasChecks, anyInProgress, anyFailing, filteredNames, ignoredNames };
63
64
  }
@@ -112,6 +112,10 @@ export function formatIterateResult(result, opts) {
112
112
  const headerLines = [heading, "", baseLine, summaryLine];
113
113
  if (requiredLine)
114
114
  headerLines.push(requiredLine);
115
+ if (result.ignoredNames && result.ignoredNames.length > 0) {
116
+ const names = result.ignoredNames.map((n) => "`" + n + "`").join(", ");
117
+ headerLines.push(`**ignored** ${names}`);
118
+ }
115
119
  const activityLine = formatActivityLine(result);
116
120
  if (activityLine)
117
121
  headerLines.push(activityLine);
@@ -133,6 +137,10 @@ export function formatIterateResult(result, opts) {
133
137
  const cancelHeaderLines = [`${heading} — ${result.reason}`, "", baseLine, summaryLine];
134
138
  if (requiredLine)
135
139
  cancelHeaderLines.push(requiredLine);
140
+ if (result.ignoredNames && result.ignoredNames.length > 0) {
141
+ const ignoredStr = result.ignoredNames.map((n) => "`" + n + "`").join(", ");
142
+ cancelHeaderLines.push(`**ignored** ${ignoredStr}`);
143
+ }
136
144
  if (activityLine)
137
145
  cancelHeaderLines.push(activityLine);
138
146
  return joinSections([
@@ -55,6 +55,9 @@ export function projectIterateLean(result, opts) {
55
55
  ...((result.inProgressChecks?.length ?? 0) > 0 && {
56
56
  inProgressChecks: result.inProgressChecks,
57
57
  }),
58
+ ...((result.ignoredNames?.length ?? 0) > 0 && {
59
+ ignoredNames: result.ignoredNames,
60
+ }),
58
61
  };
59
62
  switch (result.action) {
60
63
  case "wait":
@@ -13,13 +13,18 @@ export function computeStatus(verdict, unresolvedThreads, unresolvedComments, me
13
13
  // is BLOCKED (review pending, insufficient approvals, branch-protection rule, etc.).
14
14
  // Requires hasChecks so that a PR with zero relevant checks (CI never started, or all
15
15
  // filtered/skipped) doesn't prematurely trigger READY before any check has reported.
16
+ // Exception: UNSTABLE with ignored checks — UNSTABLE means only non-required checks are
17
+ // pending/failing, and if those are all ignored the handoff is safe even with no other checks.
18
+ // BLOCKED is excluded from the ignoredNames extension: BLOCKED can mean required checks haven't
19
+ // started, and handing off prematurely there risks a broken merge attempt.
16
20
  // blockingBotReviewInProgress is still excluded — a bot review is shepherd's problem, not a hand-off.
21
+ const hasRelevantPassingChecks = verdict.hasChecks || (mergeStatus.status === "UNSTABLE" && verdict.ignoredNames.length > 0);
17
22
  if (verdict.allPassed &&
18
- verdict.hasChecks &&
23
+ hasRelevantPassingChecks &&
19
24
  unresolvedThreads === 0 &&
20
25
  unresolvedComments === 0 &&
21
26
  changesRequestedReviews === 0 &&
22
- mergeStatus.status === "BLOCKED" &&
27
+ (mergeStatus.status === "BLOCKED" || mergeStatus.status === "UNSTABLE") &&
23
28
  !mergeStatus.blockingBotReviewInProgress) {
24
29
  return "READY";
25
30
  }
@@ -124,6 +124,7 @@ export async function runCheck(opts) {
124
124
  filtered,
125
125
  filteredNames: verdict.filteredNames,
126
126
  blockedByFilteredCheck,
127
+ ...(verdict.ignoredNames.length > 0 && { ignoredNames: verdict.ignoredNames }),
127
128
  },
128
129
  threads: {
129
130
  actionable: threadVisibility.activeThreads,
@@ -50,6 +50,7 @@ export async function runIterate(opts) {
50
50
  branchProtection: report.branchProtection,
51
51
  checks: buildRelevantChecks(report),
52
52
  inProgressChecks: buildActiveChecks(report),
53
+ ...(report.checks.ignoredNames?.length ? { ignoredNames: report.checks.ignoredNames } : {}),
53
54
  activity: report.activity,
54
55
  action: "cancel",
55
56
  reason: report.mergeStatus.state === "MERGED" ? "merged" : "closed",
@@ -93,6 +94,7 @@ export async function runIterate(opts) {
93
94
  branchProtection: report.branchProtection,
94
95
  checks: buildRelevantChecks(report),
95
96
  inProgressChecks: buildActiveChecks(report),
97
+ ...(report.checks.ignoredNames?.length ? { ignoredNames: report.checks.ignoredNames } : {}),
96
98
  activity: report.activity,
97
99
  };
98
100
  if (readyState.shouldCancel) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.26.1",
3
+ "version": "0.26.2",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
5
5
  "license": "MIT",
6
6
  "author": "Jonathan Ong",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.26.1",
3
+ "version": "0.26.2",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for Codex.",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",