pr-shepherd 0.44.1 → 0.45.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.
Files changed (95) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +4 -4
  3. package/bin/checks/conclusions.d.mts +24 -1
  4. package/bin/checks/conclusions.mjs +39 -0
  5. package/bin/checks/triage.d.mts +3 -2
  6. package/bin/checks/triage.mjs +44 -15
  7. package/bin/cli/api-usage-formatter.d.mts +3 -0
  8. package/bin/cli/api-usage-formatter.mjs +36 -0
  9. package/bin/cli/error-format.d.mts +13 -0
  10. package/bin/cli/error-format.mjs +41 -0
  11. package/bin/cli/fix-formatter.mjs +12 -1
  12. package/bin/cli/iterate-activity-formatter.d.mts +2 -0
  13. package/bin/cli/iterate-activity-formatter.mjs +26 -0
  14. package/bin/cli/iterate-formatter.mjs +11 -28
  15. package/bin/cli/iterate-instructions.mjs +14 -1
  16. package/bin/cli/iterate-lean.mjs +1 -0
  17. package/bin/cli/mutate-formatter.mjs +2 -0
  18. package/bin/commands/check-annotations.d.mts +2 -1
  19. package/bin/commands/check-annotations.mjs +7 -7
  20. package/bin/commands/check.mjs +6 -4
  21. package/bin/commands/iterate/api-usage.d.mts +2 -0
  22. package/bin/commands/iterate/api-usage.mjs +42 -0
  23. package/bin/commands/iterate/check-instructions.d.mts +2 -2
  24. package/bin/commands/iterate/check-instructions.mjs +27 -8
  25. package/bin/commands/iterate/fix-code.mjs +42 -8
  26. package/bin/commands/iterate/index.mjs +6 -3
  27. package/bin/commands/iterate/merge-state.mjs +5 -2
  28. package/bin/commands/iterate/render.d.mts +1 -1
  29. package/bin/commands/iterate/render.mjs +9 -5
  30. package/bin/commands/iterate/run.d.mts +2 -0
  31. package/bin/commands/iterate/run.mjs +8 -0
  32. package/bin/commands/mark-files-as-viewed.mjs +8 -0
  33. package/bin/commands/poll-run.d.mts +2 -0
  34. package/bin/commands/poll-run.mjs +8 -0
  35. package/bin/commands/poll.d.mts +1 -2
  36. package/bin/commands/poll.mjs +29 -10
  37. package/bin/comments/rate-limit.d.mts +4 -0
  38. package/bin/comments/rate-limit.mjs +6 -0
  39. package/bin/config/load.d.mts +6 -1
  40. package/bin/config/load.mjs +33 -1
  41. package/bin/config.json +6 -1
  42. package/bin/github/api-telemetry-aggregate.d.mts +32 -0
  43. package/bin/github/api-telemetry-aggregate.mjs +84 -0
  44. package/bin/github/api-telemetry.d.mts +13 -0
  45. package/bin/github/api-telemetry.mjs +128 -0
  46. package/bin/github/check-annotations.d.mts +14 -1
  47. package/bin/github/check-annotations.mjs +29 -2
  48. package/bin/github/errors.d.mts +2 -0
  49. package/bin/github/errors.mjs +2 -0
  50. package/bin/github/gql/batch-pr-page.gql +8 -0
  51. package/bin/github/gql/batch-pr.gql +8 -0
  52. package/bin/github/gql/check-run-annotations.gql +8 -0
  53. package/bin/github/gql/commit-check-contexts.gql +8 -0
  54. package/bin/github/gql/get-pr-body.gql +8 -0
  55. package/bin/github/gql/get-pr-head-sha.gql +8 -0
  56. package/bin/github/gql/pr-number-by-branch.gql +8 -0
  57. package/bin/github/gql/review-thread-comments.gql +8 -0
  58. package/bin/github/gql/suggestion-threads.gql +8 -0
  59. package/bin/github/graphql-http.mjs +63 -8
  60. package/bin/github/http-auth.d.mts +9 -1
  61. package/bin/github/http-auth.mjs +35 -14
  62. package/bin/github/http-intermediate.d.mts +9 -0
  63. package/bin/github/http-intermediate.mjs +20 -0
  64. package/bin/github/http-request.d.mts +1 -1
  65. package/bin/github/http-request.mjs +1 -1
  66. package/bin/github/http-utils.d.mts +6 -0
  67. package/bin/github/http-utils.mjs +10 -1
  68. package/bin/github/rest-http.d.mts +16 -1
  69. package/bin/github/rest-http.mjs +71 -8
  70. package/bin/github/rest-text.mjs +41 -5
  71. package/bin/index.mjs +2 -1
  72. package/bin/log/session.d.mts +11 -0
  73. package/bin/log/session.mjs +22 -0
  74. package/bin/mcp/server.mjs +16 -2
  75. package/bin/quota-warning.d.mts +2 -0
  76. package/bin/quota-warning.mjs +6 -0
  77. package/bin/state/graphql-quota-claims.d.mts +2 -0
  78. package/bin/state/graphql-quota-claims.mjs +61 -0
  79. package/bin/state/graphql-quota-policy.d.mts +17 -0
  80. package/bin/state/graphql-quota-policy.mjs +43 -0
  81. package/bin/state/graphql-quota-warnings.d.mts +6 -0
  82. package/bin/state/graphql-quota-warnings.mjs +105 -0
  83. package/bin/state/rest-cache.d.mts +48 -0
  84. package/bin/state/rest-cache.mjs +91 -0
  85. package/bin/types/api-usage.d.mts +31 -0
  86. package/bin/types/api-usage.mjs +1 -0
  87. package/bin/types/iterate.d.mts +4 -1
  88. package/bin/types/report.d.mts +2 -0
  89. package/bin/types.d.mts +1 -0
  90. package/bin/types.mjs +1 -0
  91. package/package.json +1 -1
  92. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  93. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  94. package/plugins/pr-shepherd/.mcp.json +1 -1
  95. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +14 -9
@@ -0,0 +1,105 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { resolveStateBase } from "./base.mjs";
5
+ import { SAFE_SEGMENT } from "../util/path-segment.mjs";
6
+ import { getWorktreeKey } from "../util/worktree.mjs";
7
+ import { claimWarning } from "./graphql-quota-claims.mjs";
8
+ import { evaluateGraphqlQuotaWarning, } from "./graphql-quota-policy.mjs";
9
+ const pendingStateUpdates = new Map();
10
+ const sessionStates = new Map();
11
+ export async function evaluateWorktreeGraphqlQuotaWarning(key, bands, sample, persist, now = Date.now() / 1000) {
12
+ if (bands.length === 0)
13
+ return undefined;
14
+ const path = await warningStatePath(key);
15
+ if (path === undefined) {
16
+ const sessionKey = `${key.owner}/${key.repo}`;
17
+ if (!persist) {
18
+ return evaluateGraphqlQuotaWarning(bands, sample, sessionStates.get(sessionKey) ?? null, now)
19
+ .warning;
20
+ }
21
+ return serializeStateUpdate(`session:${sessionKey}`, async () => {
22
+ const evaluated = evaluateGraphqlQuotaWarning(bands, sample, sessionStates.get(sessionKey) ?? null, now);
23
+ sessionStates.set(sessionKey, evaluated.state);
24
+ return evaluated.warning;
25
+ });
26
+ }
27
+ if (!persist) {
28
+ const previous = await readState(path);
29
+ return evaluateGraphqlQuotaWarning(bands, sample, previous, now).warning;
30
+ }
31
+ return serializeStateUpdate(path, async () => {
32
+ const previous = await readState(path);
33
+ const evaluated = evaluateGraphqlQuotaWarning(bands, sample, previous, now);
34
+ const warning = evaluated.warning !== undefined &&
35
+ (await claimWarning(path, evaluated.warning, now, evaluated.state.rearmEpoch))
36
+ ? evaluated.warning
37
+ : undefined;
38
+ await writeState(path, evaluated.state);
39
+ return warning;
40
+ });
41
+ }
42
+ async function serializeStateUpdate(key, update) {
43
+ const previous = pendingStateUpdates.get(key) ?? Promise.resolve();
44
+ const result = previous.then(update);
45
+ const completion = result.then(() => undefined, () => undefined);
46
+ pendingStateUpdates.set(key, completion);
47
+ void completion.finally(() => {
48
+ if (pendingStateUpdates.get(key) === completion) {
49
+ pendingStateUpdates.delete(key);
50
+ }
51
+ });
52
+ return result;
53
+ }
54
+ async function warningStatePath(key) {
55
+ if (!SAFE_SEGMENT.test(key.owner) || !SAFE_SEGMENT.test(key.repo)) {
56
+ throw new Error(`Invalid repo key segments: ${key.owner}/${key.repo}`);
57
+ }
58
+ let worktreeKey;
59
+ try {
60
+ worktreeKey = await getWorktreeKey();
61
+ }
62
+ catch {
63
+ return undefined;
64
+ }
65
+ return join(resolveStateBase(), `${key.owner}-${key.repo}`, "worktrees", `${worktreeKey}-graphql-quota-warnings.json`);
66
+ }
67
+ async function readState(path) {
68
+ try {
69
+ const parsed = JSON.parse(await readFile(path, "utf8"));
70
+ if (typeof parsed.resource !== "string" ||
71
+ !Number.isFinite(parsed.limit) ||
72
+ !Number.isFinite(parsed.lastRemaining) ||
73
+ !Number.isFinite(parsed.resetAt) ||
74
+ !Array.isArray(parsed.warnedThresholds)) {
75
+ return null;
76
+ }
77
+ return parsed;
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
83
+ async function writeState(path, state) {
84
+ let tmp;
85
+ try {
86
+ await mkdir(dirname(path), { recursive: true });
87
+ tmp = `${path}.${randomUUID()}.tmp`;
88
+ await writeFile(tmp, JSON.stringify(state), "utf8");
89
+ await rename(tmp, path);
90
+ tmp = undefined;
91
+ }
92
+ catch {
93
+ // Best-effort warning state must never fail the command.
94
+ }
95
+ finally {
96
+ if (tmp !== undefined) {
97
+ try {
98
+ await unlink(tmp);
99
+ }
100
+ catch {
101
+ // Best-effort cleanup.
102
+ }
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Cross-tick GitHub API response cache — reduces redundant requests across
3
+ * `pr-shepherd` poll ticks.
4
+ *
5
+ * Two entry kinds, both keyed by a caller-chosen logical `name` (hashed to a
6
+ * filesystem-safe filename, mirroring seen-comments.mts):
7
+ *
8
+ * - "etag" entries back conditional REST requests (`If-None-Match`). A 200
9
+ * response overwrites the entry; a 304 leaves it untouched. Used for list
10
+ * endpoints whose content can change from tick to tick (job lists, run
11
+ * lists).
12
+ * - "derived" entries cache a value computed from a response that is
13
+ * immutable once identified by its key — job-log excerpts once a job is
14
+ * terminal, check-run annotations once the check is COMPLETED. No ETag
15
+ * applies; the cache is keyed on an immutable identity instead.
16
+ *
17
+ * Entries live under `$PR_SHEPHERD_STATE_DIR/<owner>-<repo>/<pr>/rest-cache/`
18
+ * and are removed for free when `pr-shepherd clean` deletes the PR's state
19
+ * directory — there is no separate pruning routine.
20
+ */
21
+ export interface StateKey {
22
+ owner: string;
23
+ repo: string;
24
+ pr: number;
25
+ }
26
+ export interface EtagCacheEntry {
27
+ kind: "etag";
28
+ name: string;
29
+ etag: string;
30
+ body: unknown;
31
+ storedAt: number;
32
+ headSha?: string;
33
+ }
34
+ export interface DerivedCacheEntry<T = unknown> {
35
+ kind: "derived";
36
+ name: string;
37
+ value: T;
38
+ storedAt: number;
39
+ headSha?: string;
40
+ }
41
+ export declare function loadEtagEntry(key: StateKey, name: string): Promise<EtagCacheEntry | null>;
42
+ export declare function storeEtagEntry(key: StateKey, name: string, fields: {
43
+ etag: string;
44
+ body: unknown;
45
+ headSha?: string;
46
+ }): Promise<void>;
47
+ export declare function loadDerived<T = unknown>(key: StateKey, name: string): Promise<DerivedCacheEntry<T> | null>;
48
+ export declare function storeDerived<T = unknown>(key: StateKey, name: string, value: T, headSha?: string): Promise<void>;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Cross-tick GitHub API response cache — reduces redundant requests across
3
+ * `pr-shepherd` poll ticks.
4
+ *
5
+ * Two entry kinds, both keyed by a caller-chosen logical `name` (hashed to a
6
+ * filesystem-safe filename, mirroring seen-comments.mts):
7
+ *
8
+ * - "etag" entries back conditional REST requests (`If-None-Match`). A 200
9
+ * response overwrites the entry; a 304 leaves it untouched. Used for list
10
+ * endpoints whose content can change from tick to tick (job lists, run
11
+ * lists).
12
+ * - "derived" entries cache a value computed from a response that is
13
+ * immutable once identified by its key — job-log excerpts once a job is
14
+ * terminal, check-run annotations once the check is COMPLETED. No ETag
15
+ * applies; the cache is keyed on an immutable identity instead.
16
+ *
17
+ * Entries live under `$PR_SHEPHERD_STATE_DIR/<owner>-<repo>/<pr>/rest-cache/`
18
+ * and are removed for free when `pr-shepherd clean` deletes the PR's state
19
+ * directory — there is no separate pruning routine.
20
+ */
21
+ import { readFile, writeFile, rename, unlink, mkdir } from "node:fs/promises";
22
+ import { dirname } from "node:path";
23
+ import { createHash, randomUUID } from "node:crypto";
24
+ import { resolvePrStatePath } from "./base.mjs";
25
+ export async function loadEtagEntry(key, name) {
26
+ const entry = await readEntry(key, name);
27
+ return entry?.kind === "etag" ? entry : null;
28
+ }
29
+ export async function storeEtagEntry(key, name, fields) {
30
+ await writeEntry(key, name, {
31
+ kind: "etag",
32
+ name,
33
+ etag: fields.etag,
34
+ body: fields.body,
35
+ storedAt: Date.now(),
36
+ ...(fields.headSha !== undefined && { headSha: fields.headSha }),
37
+ });
38
+ }
39
+ export async function loadDerived(key, name) {
40
+ const entry = await readEntry(key, name);
41
+ return entry?.kind === "derived" ? entry : null;
42
+ }
43
+ export async function storeDerived(key, name, value, headSha) {
44
+ await writeEntry(key, name, {
45
+ kind: "derived",
46
+ name,
47
+ value,
48
+ storedAt: Date.now(),
49
+ ...(headSha !== undefined && { headSha }),
50
+ });
51
+ }
52
+ // ---------------------------------------------------------------------------
53
+ // Helpers
54
+ // ---------------------------------------------------------------------------
55
+ async function readEntry(key, name) {
56
+ try {
57
+ const raw = await readFile(resolvePath(key, name), "utf8");
58
+ return JSON.parse(raw);
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ async function writeEntry(key, name, entry) {
65
+ let tmp;
66
+ try {
67
+ const path = resolvePath(key, name);
68
+ await mkdir(dirname(path), { recursive: true });
69
+ tmp = `${path}.${randomUUID()}.tmp`;
70
+ await writeFile(tmp, JSON.stringify(entry), "utf8");
71
+ await rename(tmp, path);
72
+ tmp = undefined;
73
+ }
74
+ catch {
75
+ // best-effort — a failed cache write just means the next tick re-fetches
76
+ }
77
+ finally {
78
+ if (tmp !== undefined) {
79
+ try {
80
+ await unlink(tmp);
81
+ }
82
+ catch {
83
+ // best-effort cleanup
84
+ }
85
+ }
86
+ }
87
+ }
88
+ function resolvePath(key, name) {
89
+ const hash = createHash("sha256").update(name, "utf8").digest("hex");
90
+ return resolvePrStatePath(key, "rest-cache", `${hash}.json`);
91
+ }
@@ -0,0 +1,31 @@
1
+ export interface ApiResourceUsage {
2
+ resource: string;
3
+ requestCount: number;
4
+ limit: number;
5
+ used?: number;
6
+ remaining: number;
7
+ resetAt: number;
8
+ }
9
+ export interface GraphqlApiUsage extends ApiResourceUsage {
10
+ /** Exact sum reported by rateLimit.cost for GraphQL queries in this command. */
11
+ measuredQueryCost: number;
12
+ /** Requests without an exact cost, principally GraphQL mutations. */
13
+ unmeasuredRequestCount: number;
14
+ /** Exact sum reported by rateLimit.nodeCount for measured GraphQL queries. */
15
+ nodeCount: number;
16
+ }
17
+ export interface ApiUsage {
18
+ credentialSources: string[];
19
+ graphql?: GraphqlApiUsage;
20
+ rest?: ApiResourceUsage[];
21
+ }
22
+ export interface GraphqlQuotaWarning {
23
+ resource: "graphql";
24
+ thresholdPercent: number;
25
+ remaining: number;
26
+ limit: number;
27
+ used?: number;
28
+ resetAt: number;
29
+ pollIntervalMinutes: number;
30
+ pollTimeoutMinutes: number;
31
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -5,6 +5,7 @@ import type { MergeRequirements } from "./merge-requirements.mts";
5
5
  import type { EscalateDetails } from "./escalate.mts";
6
6
  import type { MergeCommandPlan } from "./merge-action.mts";
7
7
  import type { ProtectedRun } from "./protected-run.mts";
8
+ import type { ApiUsage, GraphqlQuotaWarning } from "./api-usage.mts";
8
9
  export type ShepherdAction = "wait" | "fix_code" | "mark_ready" | "merge" | "cancel" | "escalate";
9
10
  export interface IterateResultSummary {
10
11
  passing: number;
@@ -41,6 +42,8 @@ export interface IterateResultBase {
41
42
  supersededNames?: string[];
42
43
  activity?: PrActivitySummary;
43
44
  mergeQueue?: import("./merge-queue.mts").MergeQueueReport;
45
+ apiUsage?: ApiUsage;
46
+ quotaWarning?: GraphqlQuotaWarning;
44
47
  }
45
48
  interface IterateResultWait extends IterateResultBase {
46
49
  action: "wait";
@@ -129,12 +132,12 @@ export interface IterateCommandOptions extends GlobalOptions {
129
132
  noAutoMarkReady?: boolean;
130
133
  /** Legacy no-op retained for API compatibility; workflow runs are never cancelled. */
131
134
  noAutoCancelActionable?: boolean;
132
- /** Override stall timeout seconds. Defaults to config.iterate.stallTimeoutMinutes * 60. */
133
135
  stallTimeoutSeconds?: number;
134
136
  /** Legacy per-invocation no-op retained for API compatibility. */
135
137
  neverCancelRuns?: string[];
136
138
  persistSeen?: boolean;
137
139
  /** Shepherd through readiness and emit the exact merge/queue command when ready. */
138
140
  merge?: boolean;
141
+ deferQuotaWarning?: boolean;
139
142
  }
140
143
  export {};
@@ -136,6 +136,8 @@ export interface AgentCheck {
136
136
  /** One-line status text shown in the GitHub UI (e.g. "67.68% of diff hit (target 85.00%)"). */
137
137
  summary?: string;
138
138
  logExcerpt?: string;
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
+ rerunCommand?: string;
139
141
  annotations?: CheckAnnotation[];
140
142
  annotationOnly?: true;
141
143
  scope?: "merge_group";
package/bin/types.d.mts CHANGED
@@ -12,3 +12,4 @@ export * from "./types/merge-requirements.mts";
12
12
  export * from "./types/merge-action.mts";
13
13
  export * from "./types/escalate.mts";
14
14
  export * from "./types/merge-queue.mts";
15
+ export * from "./types/api-usage.mts";
package/bin/types.mjs CHANGED
@@ -12,3 +12,4 @@ export * from "./types/merge-requirements.mjs";
12
12
  export * from "./types/merge-action.mjs";
13
13
  export * from "./types/escalate.mjs";
14
14
  export * from "./types/merge-queue.mjs";
15
+ export * from "./types/api-usage.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.44.1",
3
+ "version": "0.45.0",
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.44.1",
3
+ "version": "0.45.0",
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.44.1", "pr-shepherd-mcp"]
5
+ "args": ["--yes", "--package", "pr-shepherd@0.45.0", "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.44.1", "pr-shepherd-mcp"]
4
+ "args": ["--yes", "--package", "pr-shepherd@0.45.0", "pr-shepherd-mcp"]
5
5
  }
6
6
  }
@@ -28,7 +28,7 @@ mechanics every tick. Apply the referenced playbook in full whenever a step poin
28
28
  ### Suggestion patches
29
29
 
30
30
  - Run one plural `build-suggestion-patches` command with a repeated `--thread-id … --message … [--description …]` group for every marked thread in displayed order.
31
- - The CLI only builds patches. Apply, stage, and commit the returned patches in order. Push only when authorization has been established outside Shepherd; GitHub viewer fields cannot verify the local Git credential.
31
+ - The CLI only builds patches. Apply, stage, and commit the returned patches in order. This command does not itself check push authorization follow the `iterate`/`fix_code` output's own commit/push instruction for whether to push now or hand off: it reflects whether GitHub confirms the viewer can push to the PR head branch.
32
32
  - The command builds from the fetched PR head and accepts a clean local descendant only when the complete ordered patch stream passes `git apply --check`.
33
33
  - If the command refuses because a suggestion is unsafe or no longer applies, inspect the current source, the displayed replacement block, and reviewer intent before editing manually. Do not apply a stale numeric range blindly or retry unchanged input.
34
34
  - A returned patch was checked against the then-current worktree. If it later fails, re-inspect the worktree because it changed after validation.
@@ -40,14 +40,19 @@ Match each failure's `[conclusion: …]` tag under `## Failing checks` to a rule
40
40
 
41
41
  More specific rows win over the general "GitHub Actions failure" row — check conclusion first.
42
42
 
43
- | Tag / kind | Do |
44
- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
45
- | GitHub Actions failure (has a run ID, not `CANCELLED`/`STARTUP_FAILURE`) | Read the included log excerpt if one is rendered. If it is missing or insufficient, hand off the displayed run ID/URL; Shepherd cannot verify authorization for another Actions read or mutation. |
46
- | Transient infrastructure failure | Record the diagnosis. Shepherd does not recommend a rerun because GitHub exposes no exact viewer capability for that workflow-run action. |
47
- | Real test or build failure | Apply a code fix — do not rerun. |
48
- | `[conclusion: CANCELLED]` | No log excerpt is rendered. Hand off the displayed metadata; Shepherd cannot verify authorization for another Actions read or mutation. |
49
- | `[conclusion: STARTUP_FAILURE]` | No log excerpt is rendered. Hand off the displayed metadata; Shepherd cannot verify authorization for another Actions read or mutation. |
50
- | `external` (no run ID, has a URL) | Preserve the URL in the handoff. Shepherd does not recommend opening it because it cannot verify the current viewer's access to the external system. |
43
+ 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`. This confirms the account's role, not the granular scope of whatever credential actually runs `gh`; if the printed `rerun:` command still fails when run, do not retry it — hand off the displayed metadata instead, the same as an unauthorized check. No tag/command means Shepherd could not verify the account-level capability at all; hand off instead of guessing. 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.
44
+
45
+ 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.
46
+
47
+ | Tag / kind | Do |
48
+ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
49
+ | GitHub Actions failure (has a run ID, not `CANCELLED`/`STARTUP_FAILURE`) | Read the included log excerpt if one is rendered. If it is missing or insufficient: run the `rerun:` command if `[rerun authorized]`, then iterate again once it completes; otherwise hand off the displayed run ID/URL. |
50
+ | Transient infrastructure failure | Run the `rerun:` command if `[rerun authorized]`, then iterate again once it completes. Otherwise record the diagnosis and hand off Shepherd cannot verify rerun authorization. |
51
+ | Real test or build failure | Apply a code fix — do not rerun, even if `[rerun authorized]` is shown. |
52
+ | `[conclusion: CANCELLED]` | No log excerpt is rendered. Run the `rerun:` command if `[rerun authorized]`, then iterate again once it completes; otherwise hand off the displayed metadata. |
53
+ | `[conclusion: STARTUP_FAILURE]` | No log excerpt is rendered. Run the `rerun:` command if `[rerun authorized]`, then iterate again once it completes; otherwise hand off the displayed metadata. |
54
+ | `[conclusion: ACTION_REQUIRED]` | Never carries `[rerun authorized]`. Hand off — the run needs a maintainer's manual workflow approval on GitHub, which a rerun cannot grant. |
55
+ | `external` (no run ID, has a URL) | Preserve the URL in the handoff. Shepherd does not recommend opening it because it cannot verify the current viewer's access to the external system. |
51
56
 
52
57
  ### Review-mutation mechanics
53
58