pr-shepherd 1.0.0 → 1.1.1

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.
Files changed (103) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/checks/classify.d.mts +3 -1
  3. package/bin/checks/classify.mjs +8 -1
  4. package/bin/cli/poll-summary-formatter.mjs +3 -0
  5. package/bin/cli/rule-auto-resolve-lines.d.mts +3 -0
  6. package/bin/cli/rule-auto-resolve-lines.mjs +12 -0
  7. package/bin/cli/stack-layer-view.d.mts +2 -1
  8. package/bin/cli/stack-overview.mjs +3 -0
  9. package/bin/cli/triage-formatter.mjs +5 -1
  10. package/bin/commands/check-activity.d.mts +8 -0
  11. package/bin/commands/check-activity.mjs +26 -0
  12. package/bin/commands/check-annotations.mjs +2 -1
  13. package/bin/commands/check-status.d.mts +6 -1
  14. package/bin/commands/check-status.mjs +9 -4
  15. package/bin/commands/check-wait-detectors.mjs +11 -0
  16. package/bin/commands/check.mjs +10 -3
  17. package/bin/commands/iterate/escalate.mjs +4 -2
  18. package/bin/commands/poll-summary-explicit-instructions.d.mts +1 -1
  19. package/bin/commands/poll-summary-explicit-instructions.mjs +9 -7
  20. package/bin/commands/poll-summary-instructions.d.mts +2 -2
  21. package/bin/commands/poll-summary-instructions.mjs +16 -15
  22. package/bin/commands/poll-summary-signature.d.mts +6 -0
  23. package/bin/commands/poll-summary-signature.mjs +19 -2
  24. package/bin/commands/poll-summary.mjs +22 -14
  25. package/bin/commands/ready-delay.d.mts +1 -0
  26. package/bin/commands/ready-delay.mjs +8 -3
  27. package/bin/commands/ready-mergeability.d.mts +1 -1
  28. package/bin/commands/ready-mergeability.mjs +2 -2
  29. package/bin/commands/rule-auto-resolve-format.d.mts +7 -0
  30. package/bin/commands/rule-auto-resolve-format.mjs +11 -4
  31. package/bin/commands/rule-auto-resolve.d.mts +14 -2
  32. package/bin/commands/stack-drain.d.mts +1 -1
  33. package/bin/commands/stack-drain.mjs +3 -3
  34. package/bin/commands/stack-work.d.mts +4 -1
  35. package/bin/commands/stack-work.mjs +10 -2
  36. package/bin/commands/tick.mjs +1 -0
  37. package/bin/comments/thread-visibility.d.mts +6 -0
  38. package/bin/comments/thread-visibility.mjs +30 -15
  39. package/bin/github/batch-parse-suites.d.mts +14 -0
  40. package/bin/github/batch-parse-suites.mjs +4 -1
  41. package/bin/github/batch-parser-helpers.mjs +5 -0
  42. package/bin/github/batch-raw-types.d.mts +1 -0
  43. package/bin/github/check-annotation-cache.d.mts +5 -0
  44. package/bin/github/check-annotation-cache.mjs +6 -3
  45. package/bin/github/fingerprint.mjs +4 -0
  46. package/bin/github/github-fetch.d.mts +6 -2
  47. package/bin/github/github-fetch.mjs +37 -2
  48. package/bin/github/github-timeouts.d.mts +8 -0
  49. package/bin/github/github-timeouts.mjs +8 -0
  50. package/bin/github/gql/batch-pr-page.gql +1 -0
  51. package/bin/github/gql/batch-pr.gql +1 -0
  52. package/bin/github/gql/commit-check-contexts.gql +1 -0
  53. package/bin/github/gql/poll-summary-check-contexts.gql +1 -0
  54. package/bin/github/http-utils.mjs +3 -0
  55. package/bin/github/poll-summary-annotation-probe.d.mts +5 -0
  56. package/bin/github/poll-summary-annotation-probe.mjs +17 -3
  57. package/bin/github/poll-summary-annotations.d.mts +14 -0
  58. package/bin/github/poll-summary-annotations.mjs +75 -0
  59. package/bin/github/poll-summary-blocking-reviewer.d.mts +2 -0
  60. package/bin/github/poll-summary-blocking-reviewer.mjs +9 -0
  61. package/bin/github/poll-summary-checks.d.mts +1 -0
  62. package/bin/github/poll-summary-checks.mjs +21 -2
  63. package/bin/github/poll-summary-command.d.mts +7 -1
  64. package/bin/github/poll-summary-command.mjs +29 -0
  65. package/bin/github/poll-summary-fingerprint.d.mts +7 -0
  66. package/bin/github/poll-summary-fingerprint.mjs +7 -0
  67. package/bin/github/poll-summary-projector.d.mts +3 -1
  68. package/bin/github/poll-summary-projector.mjs +15 -25
  69. package/bin/github/poll-summary-queue-removal.d.mts +2 -0
  70. package/bin/github/poll-summary-queue-removal.mjs +11 -0
  71. package/bin/github/poll-summary-raw.d.mts +1 -0
  72. package/bin/github/poll-summary-readiness.mjs +3 -0
  73. package/bin/github/poll-summary-review-parity.d.mts +24 -0
  74. package/bin/github/poll-summary-review-parity.mjs +82 -0
  75. package/bin/github/poll-summary-review.d.mts +11 -2
  76. package/bin/github/poll-summary-review.mjs +87 -59
  77. package/bin/github/poll-summary-route.mjs +2 -0
  78. package/bin/github/poll-summary-row-error.d.mts +16 -0
  79. package/bin/github/poll-summary-row-error.mjs +74 -0
  80. package/bin/github/poll-summary-rule-auto-resolve.d.mts +32 -0
  81. package/bin/github/poll-summary-rule-auto-resolve.mjs +119 -0
  82. package/bin/github/poll-summary-rule-handoff.d.mts +7 -0
  83. package/bin/github/poll-summary-rule-handoff.mjs +20 -0
  84. package/bin/github/poll-summary-rule-items.d.mts +20 -0
  85. package/bin/github/poll-summary-rule-items.mjs +28 -0
  86. package/bin/github/poll-summary-unreported.d.mts +7 -0
  87. package/bin/github/poll-summary-unreported.mjs +16 -0
  88. package/bin/github/poll-summary.mjs +15 -7
  89. package/bin/github/rest-check-read.mjs +3 -1
  90. package/bin/github/rest-reader-core.mjs +13 -2
  91. package/bin/github/rest-summary-read.mjs +12 -1
  92. package/bin/github/rest-text.mjs +3 -2
  93. package/bin/github/stack-handoff-page.mjs +1 -0
  94. package/bin/reporters/agent.mjs +2 -0
  95. package/bin/types/github.d.mts +8 -0
  96. package/bin/types/poll-summary.d.mts +23 -0
  97. package/bin/types/report.d.mts +5 -1
  98. package/package.json +2 -2
  99. package/plugins/pr-shepherd/.claude-plugin/plugin.json +1 -1
  100. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  101. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  102. package/plugins/pr-shepherd/.mcp.json +1 -1
  103. package/plugins/pr-shepherd/skills/pr-shepherd/references/ci-failure-triage.md +1 -0
@@ -3,8 +3,9 @@ import { recordRestIdentity } from "./rest-identities.mjs";
3
3
  import { readRest as rest } from "./rest-reader-core.mjs";
4
4
  export async function readRestCommitChecks(oid, repo, pr = 0) {
5
5
  const prefix = restRepoPath(repo);
6
- const checks = await readRestPages(`${prefix}/commits/${encodeURIComponent(oid)}/check-runs?filter=latest`, (body) => restCollection(body, "check_runs"));
6
+ // Suites first: a COMPLETED suite read before the runs means a run still not COMPLETED is stuck.
7
7
  const suites = await readRestPages(`${prefix}/commits/${encodeURIComponent(oid)}/check-suites`, (body) => restCollection(body, "check_suites"));
8
+ const checks = await readRestPages(`${prefix}/commits/${encodeURIComponent(oid)}/check-runs?filter=latest`, (body) => restCollection(body, "check_runs"));
8
9
  const statuses = await readRestPages(`${prefix}/commits/${encodeURIComponent(oid)}/statuses`);
9
10
  const workflowRuns = await readRestPages(`${prefix}/actions/runs?head_sha=${encodeURIComponent(oid)}`, (body) => restCollection(body, "workflow_runs"));
10
11
  for (const suite of suites.nodes) {
@@ -64,6 +65,7 @@ export async function readRestCommitChecks(oid, repo, pr = 0) {
64
65
  annotations: { totalCount: check.output.annotations_count, nodes: [] },
65
66
  checkSuite: suite
66
67
  ? {
68
+ status: suite.status.toUpperCase(),
67
69
  createdAt: suite.created_at,
68
70
  updatedAt: suite.updated_at,
69
71
  workflowRun: run
@@ -101,7 +101,9 @@ function nextRestPage(link, original) {
101
101
  if (!match)
102
102
  malformedRest(`invalid pagination link for ${original}`);
103
103
  const url = new URL(match[1], "https://api.github.com");
104
- if (url.origin !== "https://api.github.com" || url.pathname !== original.split("?")[0])
104
+ const originalPath = original.split("?")[0];
105
+ if (url.origin !== "https://api.github.com" ||
106
+ !samePaginationResource(url.pathname, originalPath))
105
107
  malformedRest(`unsafe pagination link for ${original}`);
106
108
  if (url.searchParams.get("per_page") !== "100")
107
109
  malformedRest(`changed page size for ${original}`);
@@ -113,7 +115,16 @@ function nextRestPage(link, original) {
113
115
  JSON.stringify(source.searchParams.getAll(key)))
114
116
  malformedRest(`changed pagination filter for ${original}`);
115
117
  }
116
- return `${url.pathname}${url.search}`;
118
+ // Rebuild on the requested path so the repository id in a /repositories/{id} link is never trusted.
119
+ return `${originalPath}${url.search}`;
120
+ }
121
+ /** GitHub may link `/repos/{owner}/{repo}/…` pages through `/repositories/{id}/…`. */
122
+ function samePaginationResource(linkPath, originalPath) {
123
+ if (linkPath === originalPath)
124
+ return true;
125
+ const linkSuffix = /^\/repositories\/\d+(\/.*)$/.exec(linkPath)?.[1];
126
+ const originalSuffix = /^\/repos\/[^/]+\/[^/]+(\/.*)$/.exec(originalPath)?.[1];
127
+ return linkSuffix !== undefined && linkSuffix === originalSuffix;
117
128
  }
118
129
  /** A pull request whose mergeability GitHub is still computing must be re-read, never replayed. */
119
130
  function isSettledBody(body) {
@@ -99,7 +99,7 @@ export function restSummary(data, pull, feedback, checks, baseRef) {
99
99
  nodes: checks.suites.map((suite) => ({
100
100
  status: suite.status.toUpperCase(),
101
101
  conclusion: suite.conclusion?.toUpperCase() ?? null,
102
- workflowRun: checks.workflowRuns.find((run) => run.check_suite_id === suite.id) ?? null,
102
+ workflowRun: restSuiteWorkflowRun(checks.workflowRuns.find((run) => run.check_suite_id === suite.id)),
103
103
  })),
104
104
  },
105
105
  },
@@ -110,3 +110,14 @@ export function restSummary(data, pull, feedback, checks, baseRef) {
110
110
  transportUnavailable: data.transportUnavailable,
111
111
  };
112
112
  }
113
+ /** The GraphQL `workflowRun` shape, so summary readers see one suite format on either transport. */
114
+ function restSuiteWorkflowRun(run) {
115
+ if (!run)
116
+ return null;
117
+ return {
118
+ databaseId: run.id,
119
+ event: run.event,
120
+ url: run.html_url,
121
+ workflow: { name: run.name },
122
+ };
123
+ }
@@ -2,6 +2,7 @@ import { appendEntry, nextEntry } from "../log/log-file.mjs";
2
2
  import { formatRequestEntry, formatResponseEntry } from "../log/session.mjs";
3
3
  import { GitHubRequestError } from "./errors.mjs";
4
4
  import { githubFetch } from "./github-fetch.mjs";
5
+ import { GITHUB_LOG_TIMEOUT_MS } from "./github-timeouts.mjs";
5
6
  import { makeAuthHeaders } from "./http-auth.mjs";
6
7
  import { requestWithTokenRetry } from "./http-request.mjs";
7
8
  import { parseRateLimit, parseRetryAfter, redactUrl, sanitizeBody, } from "./http-utils.mjs";
@@ -19,7 +20,7 @@ export async function restText(path, onRateLimit) {
19
20
  const auth = await makeAuthHeaders();
20
21
  authSource = auth.source;
21
22
  credentialFingerprint = auth.fingerprint;
22
- return githubFetch(url, { method: "GET", headers: auth.headers, redirect: "manual" });
23
+ return githubFetch(url, { method: "GET", headers: auth.headers, redirect: "manual" }, GITHUB_LOG_TIMEOUT_MS);
23
24
  }, t0, (response, durationMs) => recordIntermediateResponse({
24
25
  n,
25
26
  kind: "restText",
@@ -105,7 +106,7 @@ async function followRestTextRedirect(res, entry) {
105
106
  const logUrl = redactUrl(location);
106
107
  appendEntry(formatRequestEntry({ n: n2, kind: "restText", method: "GET", url: logUrl }));
107
108
  const t1 = performance.now();
108
- const redirectRes = await githubFetch(location);
109
+ const redirectRes = await githubFetch(location, undefined, GITHUB_LOG_TIMEOUT_MS);
109
110
  appendEntry(formatResponseEntry({
110
111
  n: n2,
111
112
  kind: "restText",
@@ -90,6 +90,7 @@ export function batchResponseFromSummary(layer, source, runs) {
90
90
  ...(run.annotations && { annotations: run.annotations }),
91
91
  checkSuite: suite
92
92
  ? {
93
+ ...(suite.status && { status: suite.status }),
93
94
  ...(suite.createdAt && { createdAt: suite.createdAt }),
94
95
  ...(suite.updatedAt && { updatedAt: suite.updatedAt }),
95
96
  workflowRun: workflowRun
@@ -59,6 +59,8 @@ export function toAgentCheck(c) {
59
59
  runId: c.runId,
60
60
  detailsUrl: c.detailsUrl,
61
61
  conclusion: c.conclusion,
62
+ ...(c.status !== "COMPLETED" &&
63
+ c.suiteStatus !== undefined && { status: c.status, suiteStatus: c.suiteStatus }),
62
64
  ...(c.runAttempt !== undefined && c.runAttempt > 1 && { runAttempt: c.runAttempt }),
63
65
  ...(c.workflowName !== undefined && { workflowName: c.workflowName }),
64
66
  ...(c.jobName !== undefined && { jobName: c.jobName }),
@@ -57,9 +57,17 @@ export interface CheckRun {
57
57
  summary?: string;
58
58
  /** Workflow display name for GitHub Actions check runs, when GraphQL exposes it. */
59
59
  workflowName?: string;
60
+ /**
61
+ * Raw status of the check suite this run belongs to. Absent when GitHub did not report one
62
+ * (status contexts, synthetic checks, or a transport that cannot read it). A `COMPLETED`
63
+ * suite with a run that is not `COMPLETED` is a stuck check.
64
+ */
65
+ suiteStatus?: string;
60
66
  workflowId?: string;
61
67
  /** True when GraphQL reported at least one CheckRun annotation. Omitted when false. */
62
68
  hasAnnotations?: boolean;
69
+ /** Annotation total GraphQL reported for this CheckRun. Internal: validates cached lists. */
70
+ annotationCount?: number;
63
71
  /**
64
72
  * Every annotation on this run, when BatchPr's `annotations(first: 1)` page already held all
65
73
  * of them. Internal: lets the check path skip `CheckRunAnnotationsBatch`; never rendered.
@@ -3,6 +3,7 @@ import type { MergeableState, MergeStateStatus, ReviewDecision } from "./github.
3
3
  import type { MergeQueueRemovalStatus } from "./merge-requirements.mts";
4
4
  import type { ShepherdAction } from "./iterate.mts";
5
5
  import type { NextCheck } from "./next-check.mts";
6
+ import type { AutoMinimizedItem } from "./report.mts";
6
7
  export interface PollSummaryChecks {
7
8
  passing?: number;
8
9
  failing?: number;
@@ -12,11 +13,26 @@ export interface PollSummaryChecks {
12
13
  ignored?: number;
13
14
  superseded?: number;
14
15
  incomplete?: true;
16
+ /**
17
+ * Non-SUCCESS checks with annotations the one-PR check has not shown yet (or that could not be
18
+ * read). Only set on a row that would otherwise claim ready; it routes to `triage`.
19
+ */
20
+ annotated?: number;
15
21
  /** Required contexts with no check run and no status context. Omitted when empty. */
16
22
  unreportedRequired?: string[];
17
23
  /** A relevant Actions workflow suite has not completed. */
18
24
  actionsWorkflowInProgress?: true;
19
25
  }
26
+ /** The one-PR `ruleAutoResolve` content, with threads reduced to what a row needs: ID and link. */
27
+ export interface PollSummaryRuleAutoResolve {
28
+ summary: string;
29
+ threads?: Array<{
30
+ id: string;
31
+ url?: string;
32
+ }>;
33
+ minimized?: AutoMinimizedItem[];
34
+ errors?: string[];
35
+ }
20
36
  export interface PollSummaryReview {
21
37
  comments?: number;
22
38
  reviews?: number;
@@ -56,6 +72,8 @@ export interface PollSummaryItem {
56
72
  /** Conservative routing hint; the selected one-PR poll makes the authoritative decision. */
57
73
  action: ShepherdAction;
58
74
  reasons: string[];
75
+ /** Raw message of the error that stopped this PR's summary read (`summary-read-failed`). */
76
+ error?: string;
59
77
  state: "OPEN" | "CLOSED" | "MERGED" | "UNKNOWN";
60
78
  mergeable: MergeableState;
61
79
  mergeStateStatus: MergeStateStatus;
@@ -72,6 +90,11 @@ export interface PollSummaryItem {
72
90
  remainingSeconds?: number;
73
91
  checks?: PollSummaryChecks;
74
92
  review?: PollSummaryReview;
93
+ /**
94
+ * Rule-driven auto-resolve this tick performed or failed, in the one-PR `ruleAutoResolve`
95
+ * shape. Kept out of `review` so a one-tick event does not churn the status signature.
96
+ */
97
+ ruleAutoResolve?: PollSummaryRuleAutoResolve;
75
98
  stack?: PollSummaryStack;
76
99
  pollCommand?: string;
77
100
  /**
@@ -1,4 +1,4 @@
1
- import type { AuthorType, ReviewThread, PrComment, Review, MergeStatusResult, ViewerAuthorization, CheckConclusion, SuggestionBlock } from "./github.mts";
1
+ import type { AuthorType, ReviewThread, PrComment, Review, MergeStatusResult, ViewerAuthorization, CheckConclusion, CheckStatus, SuggestionBlock } from "./github.mts";
2
2
  import type { ClassifiedCheck, RelatedFailedJob, TriagedCheck } from "./check-classification.mts";
3
3
  import type { AgentThreadComment } from "./agent-thread.mts";
4
4
  import type { CheckAnnotation } from "./check-annotations.mts";
@@ -196,6 +196,10 @@ export interface AgentCheck {
196
196
  detailsUrl: string | null;
197
197
  /** Raw GitHub check conclusion; may be null for some completed checks from upstream data. */
198
198
  conclusion: CheckConclusion;
199
+ /** Raw check status, present only for a stuck check: not `COMPLETED` while its suite is. */
200
+ status?: CheckStatus;
201
+ /** Raw check-suite status (`COMPLETED`), present only with `status` for a stuck check. */
202
+ suiteStatus?: string;
199
203
  /** Workflow display name (e.g. `"CI"`). Populated on a best-effort basis when available from the jobs API. */
200
204
  workflowName?: string;
201
205
  /** Name of the matched job (e.g. `"tests (ubuntu)"`). Distinct from check name for matrix builds. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
5
5
  "keywords": [
6
6
  "automation",
@@ -91,7 +91,7 @@
91
91
  "husky": "^9.1.7",
92
92
  "knip": "^6.14.1",
93
93
  "marked": "^18.0.11",
94
- "oxfmt": "^0.72.0",
94
+ "oxfmt": "^0.73.0",
95
95
  "oxlint": "^1.60.0",
96
96
  "typescript": "^7.0.2",
97
97
  "vitest": "^5.0.0"
@@ -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": "1.0.0",
4
+ "version": "1.1.1",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
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@1.0.0", "pr-shepherd-mcp"]
5
+ "args": ["--yes", "--package", "pr-shepherd@1.1.1", "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@1.0.0", "pr-shepherd-mcp"]
4
+ "args": ["--yes", "--package", "pr-shepherd@1.1.1", "pr-shepherd-mcp"]
5
5
  }
6
6
  }
@@ -13,6 +13,7 @@ Apply when a step says `Playbook: "CI failure triage"`. Use the excerpts and tag
13
13
  - Actions failure with a run id: read the excerpt. Fix a real test or build failure in code and never rerun it, even with a printed `rerun:`. Run a printed `rerun:` only for a transient cause; without one, finish the other work. Then iterate without waiting.
14
14
  - An empty excerpt is not evidence of a transient failure. A gate-job excerpt naming failed jobs (for example `test-playwright: failure`) is test-failure evidence even without a stack trace: read the `Other failed jobs in this run` tails first, and only when a named job's tail is absent or truncated run `gh run view <runId> --log-failed -R <owner/repo>`. Rerun only when those logs show a transient cause.
15
15
  - `CANCELLED` or `STARTUP_FAILURE` (no excerpt): run a printed `rerun:`, else finish the other work; iterate without waiting.
16
+ - `[status: …, suite: COMPLETED]` (stuck run): the workflow finished but this check never completed, so there is nothing to fix in code. Run a printed `rerun:`, else finish the other work; iterate without waiting.
16
17
  - `ACTION_REQUIRED`: finish the other work and iterate. Shepherd escalates if manual approval is still required.
17
18
  - `external` with a URL: inspect the provider or reproduce locally, fix, and iterate. The URL alone is not `[ESCALATE]`.
18
19
  - `(no runId)` without a URL: keep the metadata. Shepherd escalates when nothing else remains.