pr-shepherd 0.14.1 → 0.15.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.
@@ -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.14.1",
4
+ "version": "0.15.0",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
@@ -0,0 +1,22 @@
1
+ export function mergeStartupFailureChecks(checks, startupFailureChecks) {
2
+ const byRunId = new Map();
3
+ checks.forEach((check, index) => {
4
+ if (check.runId === null)
5
+ return;
6
+ const indices = byRunId.get(check.runId) ?? [];
7
+ indices.push(index);
8
+ byRunId.set(check.runId, indices);
9
+ });
10
+ const merged = [...checks];
11
+ const removed = new Set();
12
+ for (const startupFailure of startupFailureChecks) {
13
+ const indices = startupFailure.runId === null ? undefined : byRunId.get(startupFailure.runId);
14
+ if (indices === undefined) {
15
+ merged.push(startupFailure);
16
+ continue;
17
+ }
18
+ merged[indices[0]] = startupFailure;
19
+ indices.slice(1).forEach((index) => removed.add(index));
20
+ }
21
+ return merged.filter((_, index) => !removed.has(index));
22
+ }
@@ -1,10 +1,13 @@
1
1
  import { rest } from "../github/http.mjs";
2
+ const STARTUP_FAILURE_STATUS = "startup_failure";
2
3
  export function triageFailingChecks(failingChecks, repo) {
3
4
  const jobsCache = new Map();
4
5
  return Promise.all(failingChecks.map((c) => triageCheck(c, repo, jobsCache)));
5
6
  }
6
7
  async function triageCheck(check, repo, jobsCache) {
7
- if (check.runId === null || check.conclusion === "CANCELLED") {
8
+ if (check.runId === null ||
9
+ check.conclusion === "CANCELLED" ||
10
+ check.conclusion === "STARTUP_FAILURE") {
8
11
  return { ...check };
9
12
  }
10
13
  const jobs = await fetchJobs(check.runId, repo, jobsCache);
@@ -16,6 +19,49 @@ async function triageCheck(check, repo, jobsCache) {
16
19
  ...(jobInfo?.failedStep !== undefined && { failedStep: jobInfo.failedStep }),
17
20
  };
18
21
  }
22
+ export async function fetchStartupFailureChecks(repo, headSha, prNumber) {
23
+ try {
24
+ return await fetchStartupFailureChecksUncached(repo, headSha, prNumber);
25
+ }
26
+ catch (err) {
27
+ const msg = err instanceof Error ? err.message : String(err);
28
+ process.stderr.write(`pr-shepherd: startup-failure run fetch failed for PR #${prNumber} at ${headSha} (ignored): ${msg}\n`);
29
+ return [];
30
+ }
31
+ }
32
+ async function fetchStartupFailureChecksUncached(repo, headSha, prNumber) {
33
+ const { owner, name } = repo;
34
+ const perPage = 100;
35
+ const MAX_RUN_PAGES = 10;
36
+ const checks = [];
37
+ for (let page = 1; page <= MAX_RUN_PAGES; page++) {
38
+ const data = await rest("GET", `/repos/${owner}/${name}/actions/runs?head_sha=${encodeURIComponent(headSha)}&status=${STARTUP_FAILURE_STATUS}&per_page=${perPage}&page=${page}`);
39
+ checks.push(...data.workflow_runs
40
+ .filter((run) => runBelongsToPr(run, prNumber, headSha))
41
+ .map(workflowRunToCheckRun));
42
+ if (data.workflow_runs.length < perPage)
43
+ break;
44
+ if (page === MAX_RUN_PAGES) {
45
+ process.stderr.write(`pr-shepherd: startup-failure run pagination cap (${MAX_RUN_PAGES * perPage} runs) reached for ${headSha} — startup-failure detection may be incomplete\n`);
46
+ }
47
+ }
48
+ return checks;
49
+ }
50
+ function workflowRunToCheckRun(run) {
51
+ const summary = run.display_title?.trim() || undefined;
52
+ return {
53
+ name: run.name?.trim() || `workflow run ${run.id}`,
54
+ status: "COMPLETED",
55
+ conclusion: "STARTUP_FAILURE",
56
+ detailsUrl: run.html_url,
57
+ event: run.event,
58
+ runId: String(run.id),
59
+ ...(summary !== undefined && { summary }),
60
+ };
61
+ }
62
+ function runBelongsToPr(run, prNumber, headSha) {
63
+ return (run.pull_requests ?? []).some((pr) => pr.number === prNumber && (pr.head?.sha ?? headSha) === headSha);
64
+ }
19
65
  function fetchJobs(runId, repo, cache) {
20
66
  const cached = cache.get(runId);
21
67
  if (cached)
@@ -1,7 +1,8 @@
1
1
  import { fetchPrBatch } from "../github/batch.mjs";
2
2
  import { getRepoInfo, getCurrentPrNumber, getMergeableState } from "../github/client.mjs";
3
3
  import { classifyChecks, getCiVerdict } from "../checks/classify.mjs";
4
- import { triageFailingChecks } from "../checks/triage.mjs";
4
+ import { mergeStartupFailureChecks } from "../checks/startup-failures.mjs";
5
+ import { fetchStartupFailureChecks, triageFailingChecks } from "../checks/triage.mjs";
5
6
  import { getOutdatedThreads } from "../comments/outdated.mjs";
6
7
  import { autoResolveOutdated } from "../comments/resolve.mjs";
7
8
  import { deriveMergeStatus } from "../merge-status/derive.mjs";
@@ -28,7 +29,9 @@ export async function runCheck(opts) {
28
29
  mergeStateStatus: restState.mergeStateStatus ?? batchData.mergeStateStatus,
29
30
  };
30
31
  }
31
- const classifiedChecks = classifyChecks(batchData.checks);
32
+ const startupFailureChecks = await fetchStartupFailureChecks(repo, batchData.headRefOid, prNumber);
33
+ const allChecks = mergeStartupFailureChecks(batchData.checks, startupFailureChecks);
34
+ const classifiedChecks = classifyChecks(allChecks);
32
35
  const verdict = getCiVerdict(classifiedChecks);
33
36
  const passing = classifiedChecks.filter((c) => c.category === "passed");
34
37
  const failing = classifiedChecks.filter((c) => c.category === "failing");
@@ -0,0 +1,24 @@
1
+ export function buildFailingCheckInstructions(checks) {
2
+ const instructions = [];
3
+ const failedRunIdChecks = checks.filter((c) => c.runId && c.conclusion !== "CANCELLED" && c.conclusion !== "STARTUP_FAILURE");
4
+ const cancelledRunIdChecks = checks.filter((c) => c.runId && c.conclusion === "CANCELLED");
5
+ const startupFailureRunIdChecks = checks.filter((c) => c.runId && c.conclusion === "STARTUP_FAILURE");
6
+ const externalChecks = checks.filter((c) => !c.runId && c.detailsUrl);
7
+ const bareChecks = checks.filter((c) => !c.runId && !c.detailsUrl);
8
+ if (failedRunIdChecks.length > 0) {
9
+ instructions.push(`For each failing check under \`## Failing checks\` with a run ID and no \`[conclusion: CANCELLED]\` or \`[conclusion: STARTUP_FAILURE]\` tag: run \`gh run view <runId> --log-failed\` to fetch the failing job's log.`, `If the log shows a transient infrastructure failure (network timeout, runner setup crash, OOM kill): run \`gh run rerun <runId> --failed\`.`, `If the log shows a real test/build failure: apply a code fix.`);
10
+ }
11
+ if (cancelledRunIdChecks.length > 0) {
12
+ instructions.push(`For each \`[conclusion: CANCELLED]\` bullet under \`## Failing checks\`: the run was cancelled outside Shepherd's control (manual cancel, newer push, concurrency-group eviction). Run \`gh run rerun <runId>\` only if the cancellation looks unintended; otherwise treat it as resolved by the superseding run. Do NOT confuse these with IDs under \`## Cancelled runs\` — those were cancelled by Shepherd itself.`);
13
+ }
14
+ if (startupFailureRunIdChecks.length > 0) {
15
+ instructions.push(`For each \`[conclusion: STARTUP_FAILURE]\` bullet under \`## Failing checks\`: the workflow failed before jobs/logs were created. Run \`gh run view <runId>\` to inspect the run metadata, then run \`gh run rerun <runId>\` if the workflow should be attempted again.`);
16
+ }
17
+ if (externalChecks.length > 0) {
18
+ instructions.push(`For each bullet in \`## Failing checks\` starting with \`external\` (external status check): open the linked URL in a browser to inspect the failure — log tails are not available for external checks.`);
19
+ }
20
+ if (bareChecks.length > 0) {
21
+ instructions.push(`For each bullet in \`## Failing checks\` starting with \`(no runId)\`: there is no run or details URL to inspect. Escalate these to a human — they require manual investigation outside the pr-shepherd flow.`);
22
+ }
23
+ return instructions;
24
+ }
@@ -1,4 +1,5 @@
1
1
  import { buildPrShepherdCommand, renderShellCommand } from "../../cli/runner.mjs";
2
+ import { buildFailingCheckInstructions } from "./check-instructions.mjs";
2
3
  export const FIX_INSTRUCTION_STOP_AFTER_PUSH = "Stop this iteration — CI needs time to run on the new push before the next tick.";
3
4
  export const FIX_INSTRUCTION_STOP_BEFORE_NEXT_TICK = "Stop this iteration before the next tick.";
4
5
  export const FIX_INSTRUCTION_END_ITERATION = "End this iteration.";
@@ -41,24 +42,7 @@ export function buildFixInstructions(threads, actionableComments, checks, review
41
42
  if (resolutionOnlyThreads.length > 0) {
42
43
  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.`);
43
44
  }
44
- const cancelledRunIdChecks = checks.filter((c) => c.runId && c.conclusion === "CANCELLED");
45
- const failedRunIdChecks = checks.filter((c) => c.runId && c.conclusion !== "CANCELLED");
46
- const externalChecks = checks.filter((c) => !c.runId && c.detailsUrl);
47
- const bareChecks = checks.filter((c) => !c.runId && !c.detailsUrl);
48
- if (failedRunIdChecks.length > 0) {
49
- instructions.push(`For each failing check under \`## Failing checks\` with a run ID and no \`[conclusion: CANCELLED]\` tag: run \`gh run view <runId> --log-failed\` to fetch the failing job's log.`);
50
- instructions.push(`If the log shows a transient infrastructure failure (network timeout, runner setup crash, OOM kill): run \`gh run rerun <runId> --failed\`.`);
51
- instructions.push(`If the log shows a real test/build failure: apply a code fix.`);
52
- }
53
- if (cancelledRunIdChecks.length > 0) {
54
- instructions.push(`For each \`[conclusion: CANCELLED]\` bullet under \`## Failing checks\`: the run was cancelled outside Shepherd's control (manual cancel, newer push, concurrency-group eviction). Run \`gh run rerun <runId>\` only if the cancellation looks unintended; otherwise treat it as resolved by the superseding run. Do NOT confuse these with IDs under \`## Cancelled runs\` — those were cancelled by Shepherd itself.`);
55
- }
56
- if (externalChecks.length > 0) {
57
- instructions.push(`For each bullet in \`## Failing checks\` starting with \`external\` (external status check): open the linked URL in a browser to inspect the failure — log tails are not available for external checks.`);
58
- }
59
- if (bareChecks.length > 0) {
60
- instructions.push(`For each bullet in \`## Failing checks\` starting with \`(no runId)\`: there is no run or details URL to inspect. Escalate these to a human — they require manual investigation outside the pr-shepherd flow.`);
61
- }
45
+ instructions.push(...buildFailingCheckInstructions(checks));
62
46
  if (reviews.length > 0) {
63
47
  instructions.push(`For each bullet under \`## Changes-requested reviews\` above: read the review body and apply the requested changes.`);
64
48
  }
@@ -4,7 +4,7 @@
4
4
  * These strip fields that are always-false by the time items reach iterate
5
5
  * (isResolved, isOutdated, isMinimized, createdAtUnix) and check metadata the
6
6
  * monitor prompt never reads (event, status, category).
7
- * conclusion is preserved on AgentCheck so the formatter can branch on CANCELLED.
7
+ * conclusion is preserved on AgentCheck so the formatter can branch on run-level conclusions.
8
8
  * detailsUrl is preserved in AgentCheck as a fallback for external status checks.
9
9
  * The original domain types are preserved for check command output.
10
10
  */
@@ -35,7 +35,9 @@ export function buildCheckInstructions(report, opts) {
35
35
  const diagnosisHint = c.runId
36
36
  ? c.conclusion === "CANCELLED"
37
37
  ? `cancelled — if unintended, rerun with \`gh run rerun ${c.runId}\``
38
- : `run \`gh run view ${c.runId} --log-failed\`${stepHint} to diagnose — if transient, rerun with \`gh run rerun ${c.runId} --failed\`; otherwise apply a fix`
38
+ : c.conclusion === "STARTUP_FAILURE"
39
+ ? `startup failure — run \`gh run view ${c.runId}\` to inspect the workflow run; rerun with \`gh run rerun ${c.runId}\` if appropriate`
40
+ : `run \`gh run view ${c.runId} --log-failed\`${stepHint} to diagnose — if transient, rerun with \`gh run rerun ${c.runId} --failed\`; otherwise apply a fix`
39
41
  : c.detailsUrl
40
42
  ? `open the check details (${c.detailsUrl}) to diagnose the failure`
41
43
  : `no run or details URL available — escalate to a human`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.14.1",
3
+ "version": "0.15.0",
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.14.1",
3
+ "version": "0.15.0",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for Codex.",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",