pr-shepherd 0.55.1 → 0.56.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 (102) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +12 -9
  3. package/bin/api.d.mts +5 -0
  4. package/bin/api.mjs +12 -1
  5. package/bin/checks/job-log.d.mts +9 -0
  6. package/bin/checks/job-log.mjs +30 -0
  7. package/bin/checks/jobs-types.d.mts +14 -0
  8. package/bin/checks/jobs-types.mjs +7 -0
  9. package/bin/checks/related-jobs.d.mts +11 -0
  10. package/bin/checks/related-jobs.mjs +32 -0
  11. package/bin/checks/triage-budget.d.mts +17 -0
  12. package/bin/checks/triage-budget.mjs +49 -0
  13. package/bin/checks/triage.d.mts +6 -3
  14. package/bin/checks/triage.mjs +89 -61
  15. package/bin/cli/api-usage-formatter.mjs +2 -2
  16. package/bin/cli/fix-formatter.mjs +2 -0
  17. package/bin/cli/help-command-pages.d.mts +1 -1
  18. package/bin/cli/help-iterate-poll-pages.d.mts +1 -1
  19. package/bin/cli/help-iterate-poll-pages.mjs +1 -1
  20. package/bin/cli/help.d.mts +1 -1
  21. package/bin/cli/iterate-checks-formatter.mjs +2 -0
  22. package/bin/cli/iterate-instructions.mjs +1 -1
  23. package/bin/cli/related-jobs-format.d.mts +3 -0
  24. package/bin/cli/related-jobs-format.mjs +16 -0
  25. package/bin/commands/check-execution-context.d.mts +12 -0
  26. package/bin/commands/check-execution-context.mjs +38 -0
  27. package/bin/commands/check-fingerprint.mjs +14 -8
  28. package/bin/commands/check-unreported.d.mts +3 -2
  29. package/bin/commands/check-unreported.mjs +4 -4
  30. package/bin/commands/check.d.mts +2 -1
  31. package/bin/commands/check.mjs +21 -6
  32. package/bin/commands/commit-suggestion-instruction.mjs +2 -1
  33. package/bin/commands/iterate/check-instructions.d.mts +9 -5
  34. package/bin/commands/iterate/check-instructions.mjs +15 -35
  35. package/bin/commands/iterate/escalate.mjs +3 -0
  36. package/bin/commands/iterate/fix-code.mjs +6 -2
  37. package/bin/commands/iterate/helpers.mjs +1 -0
  38. package/bin/commands/iterate/index.mjs +20 -8
  39. package/bin/commands/iterate/native-stack-rebase.mjs +3 -2
  40. package/bin/commands/iterate/render.mjs +13 -5
  41. package/bin/commands/iterate/stale-ancestry.d.mts +2 -1
  42. package/bin/commands/iterate/stale-ancestry.mjs +3 -2
  43. package/bin/commands/iterate/unreported-required.mjs +1 -1
  44. package/bin/commands/playbook-pointer.d.mts +2 -0
  45. package/bin/commands/playbook-pointer.mjs +4 -0
  46. package/bin/commands/poll-quota.d.mts +2 -0
  47. package/bin/commands/poll-quota.mjs +18 -25
  48. package/bin/commands/poll-rate-limit-wait.mjs +13 -8
  49. package/bin/commands/poll-summary-instructions.mjs +1 -1
  50. package/bin/commands/ready-delay.d.mts +2 -0
  51. package/bin/commands/ready-delay.mjs +18 -0
  52. package/bin/commands/resolve-mutate.mjs +8 -9
  53. package/bin/commands/shepherd-journal.d.mts +1 -1
  54. package/bin/commands/shepherd-journal.mjs +3 -2
  55. package/bin/commands/stack-drain.mjs +2 -1
  56. package/bin/github/batch-raw-types.d.mts +2 -0
  57. package/bin/github/batch-receipt-evidence.d.mts +5 -0
  58. package/bin/github/batch-receipt-evidence.mjs +61 -0
  59. package/bin/github/batch.d.mts +4 -0
  60. package/bin/github/batch.mjs +42 -8
  61. package/bin/github/errors.d.mts +3 -0
  62. package/bin/github/errors.mjs +15 -6
  63. package/bin/github/gql/batch-pr-page.gql +1 -0
  64. package/bin/github/gql/batch-pr.gql +1 -0
  65. package/bin/github/gql/poll-summary-annotation-probe.gql +8 -0
  66. package/bin/github/gql/reply-thread-comments.gql +25 -0
  67. package/bin/github/gql/reply-thread-transcripts.gql +31 -0
  68. package/bin/github/merge-queue-checks.d.mts +2 -1
  69. package/bin/github/merge-queue-checks.mjs +18 -8
  70. package/bin/github/merge-target-rules.d.mts +2 -1
  71. package/bin/github/merge-target-rules.mjs +4 -4
  72. package/bin/github/poll-summary-annotation-probe.d.mts +2 -0
  73. package/bin/github/poll-summary-annotation-probe.mjs +7 -0
  74. package/bin/github/queries.d.mts +5 -0
  75. package/bin/github/queries.mjs +5 -0
  76. package/bin/github/rate-limit-kind.d.mts +13 -0
  77. package/bin/github/rate-limit-kind.mjs +25 -0
  78. package/bin/github/reply-thread-transcripts.d.mts +3 -0
  79. package/bin/github/reply-thread-transcripts.mjs +89 -0
  80. package/bin/github/rest-http.mjs +1 -0
  81. package/bin/github/rest-text.d.mts +2 -1
  82. package/bin/github/rest-text.mjs +5 -2
  83. package/bin/github/thread-comments.d.mts +5 -1
  84. package/bin/github/thread-comments.mjs +30 -6
  85. package/bin/mcp/server.mjs +32 -5
  86. package/bin/quota-warning.mjs +2 -2
  87. package/bin/reporters/agent.mjs +1 -0
  88. package/bin/threads/transcript.d.mts +1 -0
  89. package/bin/threads/transcript.mjs +4 -1
  90. package/bin/types/check-classification.d.mts +10 -0
  91. package/bin/types/report.d.mts +4 -1
  92. package/package.json +2 -2
  93. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  94. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  95. package/plugins/pr-shepherd/.mcp.json +1 -1
  96. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +57 -70
  97. package/plugins/pr-shepherd/skills/pr-shepherd/references/branch-update.md +9 -0
  98. package/plugins/pr-shepherd/skills/pr-shepherd/references/ci-failure-triage.md +21 -0
  99. package/plugins/pr-shepherd/skills/pr-shepherd/references/journal.md +7 -0
  100. package/plugins/pr-shepherd/skills/pr-shepherd/references/review-mutations.md +9 -0
  101. package/plugins/pr-shepherd/skills/pr-shepherd/references/stack-merge.md +7 -0
  102. package/plugins/pr-shepherd/skills/pr-shepherd/references/suggestion-patches.md +10 -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": "0.55.1",
4
+ "version": "0.56.0",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
package/README.md CHANGED
@@ -27,7 +27,7 @@ Full reference: [docs/README.md](docs/README.md). Feature matrix: [docs/features
27
27
 
28
28
  `pr-shepherd` moves deterministic PR orchestration into a local MCP server, with a CLI for shells and CI. Both interfaces fetch the same GitHub state, emit raw-enough context, and return a numbered plan for the calling agent to follow.
29
29
 
30
- The MCP server exposes canonical `iterate`, `apply`, and `build_suggestion_patches` tools. `apply` accepts ordered review mutations, file-view mutations, and journal entries; the deprecated singular suggestion tool remains temporarily as an adapter. Direct MCP calls require a repository-qualified `pr`: a GitHub PR URL or `owner/repo#N`; the explicit repository is the target for GitHub I/O, even when it differs from the local checkout. The CLI and programmatic API also retain bare-number and current-branch PR discovery. The shipped skills are thin dispatchers for those tools.
30
+ The MCP server exposes `iterate`, `apply`, `build_suggestion_patches`, `extract_journal`, and `get_journal`. `apply` accepts ordered review mutations, file-view mutations, and journal entries; the deprecated singular suggestion tool remains temporarily as an adapter. PR-targeted MCP calls require a repository-qualified `pr`: a GitHub PR URL or `owner/repo#N`; the explicit repository is the target for GitHub I/O, even when it differs from the local checkout. `extract_journal` takes a Markdown body string and performs no I/O. The CLI and programmatic API also retain bare-number and current-branch PR discovery. The shipped skills are thin dispatchers for those tools.
31
31
 
32
32
  Each tick returns exactly one action:
33
33
 
@@ -73,13 +73,12 @@ Conversations Resolved: No [Not Required]
73
73
 
74
74
  1. Review each item under `## Review threads` and `## Failing checks` and decide whether it needs a code change.
75
75
  2. Apply every warranted review fix in each file referenced above.
76
- 3. Triage every failure under `## Failing checks`. See "CI failure triage" in the pr-shepherd skill for read-only inspection rules.
77
- 4. If you changed code, commit any remaining changes and push to the PR head branch, then run review mutations using the pushed commit SHA and iterate immediately with the same options. If you did not change code, do not commit and continue.
78
- 5. Substitute any command placeholders and run the generated review mutations.
79
- 6. If you did not change code, replace `$HEAD_SHA` with `$(git rev-parse HEAD)`, which must equal the current remote PR head. If you changed code, commit and push to the PR head branch first, then replace `$HEAD_SHA` with the pushed commit SHA.
80
- 7. Replace `$DISMISS_MESSAGE` with one sentence describing what changed.
81
- 8. Run the `apply review:` command shown above. See "Review-mutation mechanics" in the pr-shepherd skill for dismiss-ID retention.
82
- 9. `[FIX_CODE]` is non-terminal: if you changed code, commit and push to the PR head branch, then run review mutations using the pushed commit SHA and iterate immediately with the same options; without code changes, complete the authorized review mutations and iterate immediately.
76
+ 3. Triage `## Failing checks`. Playbook: "CI failure triage".
77
+ 4. If you changed code, commit any remaining changes and push to the PR head branch. If you did not, do not commit.
78
+ 5. If you did not change code, replace `$HEAD_SHA` with `$(git rev-parse HEAD)` (it must equal the remote PR head). If you did, use the pushed SHA.
79
+ 6. Replace `$DISMISS_MESSAGE` with one sentence describing what changed.
80
+ 7. Run the `apply review:` command above. Playbook: "Review-mutation mechanics".
81
+ 8. `[FIX_CODE]` is non-terminal. Iterate immediately with the same options.
83
82
  ```
84
83
 
85
84
  See [docs/actions.md](docs/actions.md) for the complete output contract and [docs/escalations.md](docs/escalations.md) for the exact finite human-handoff boundary. Iterate/poll PR outcomes use exit codes `0` and `10`–`16`; command and GitHub failures use `sysexits.h` codes — [docs/exit-codes.md](docs/exit-codes.md).
@@ -123,7 +122,7 @@ Grok:
123
122
  /pr-shepherd 42
124
123
  ```
125
124
 
126
- MCP clients call `iterate` once per tick, then use `apply` for review/file/journal mutations and `build_suggestion_patches` for anchored suggestions. Every direct MCP call supplies the same repository-qualified PR reference. `iterate` returns the same structured action data as the CLI, including its review mutation arguments. The client owns recurrence, so this works consistently in Codex, Claude Code, Grok, and any other stdio MCP client.
125
+ MCP clients call `iterate` once per tick, then use `apply` for review/file/journal mutations and `build_suggestion_patches` for anchored suggestions. To read journal entries, call `extract_journal` with a body already in hand or `get_journal` with a repository-qualified PR reference. `iterate` returns the same structured action data as the CLI, including its review mutation arguments. The client owns recurrence, so this works consistently in Codex, Claude Code, Grok, and any other stdio MCP client.
127
126
 
128
127
  The CLI remains useful for shell workflows. Its canonical polling form is:
129
128
 
@@ -190,6 +189,10 @@ complete Markdown list item with LF line endings. It fails closed for malformed
190
189
  containers and ignores journal-shaped examples hidden in Markdown constructs. The full journal API,
191
190
  including append and reconciliation helpers, is documented in [docs/api.md](docs/api.md).
192
191
 
192
+ MCP clients can call `extract_journal({ body: prBody })` for the same pure result without writing a
193
+ file, or `get_journal({ pr: "owner/repo#123" })` to fetch a PR body through GraphQL and extract it.
194
+ Both return the typed extraction JSON in `structuredContent` and `content`; neither mutates the PR.
195
+
193
196
  For shell automation that already has a PR body, use the equivalent local-only command:
194
197
 
195
198
  ```sh
package/bin/api.d.mts CHANGED
@@ -2,6 +2,7 @@ import { type JournalResult } from "./commands/journal/index.mts";
2
2
  import { type MarkFilesAsViewedResult } from "./commands/mark-files-as-viewed.mts";
3
3
  import type { ResolveResult } from "./comments/resolve.mts";
4
4
  import type { BuildSuggestionPatchesResult, CommitSuggestionResult, IterateCommandOptions, IterateResult, PollSummaryResult } from "./types.mts";
5
+ import { type ShepherdJournalExtraction } from "./journal/index.mts";
5
6
  export interface CreatePrShepherdOptions {
6
7
  /** Working directory used for git, config, and classification-rule lookups. */
7
8
  cwd?: string;
@@ -81,12 +82,16 @@ export interface BuildSuggestionPatchesInput {
81
82
  pr?: PrReference;
82
83
  suggestions: SuggestionPatchInput[];
83
84
  }
85
+ export interface GetJournalInput {
86
+ pr: PrReference;
87
+ }
84
88
  export interface PrShepherd {
85
89
  iterate(input?: SingleIterateInput): Promise<IterateResult>;
86
90
  iterate(input: AggregateIterateInput): Promise<PollSummaryResult>;
87
91
  iterate(input: IterateInput): Promise<IterateResult | PollSummaryResult>;
88
92
  apply(input: ApplyInput): Promise<ApplyResult>;
89
93
  buildSuggestionPatches(input: BuildSuggestionPatchesInput): Promise<BuildSuggestionPatchesResult>;
94
+ getJournal(input: GetJournalInput): Promise<ShepherdJournalExtraction>;
90
95
  /** Compatibility adapter; prefer buildSuggestionPatches. */
91
96
  buildSuggestionPatch(input: BuildSuggestionPatchInput): Promise<CommitSuggestionResult>;
92
97
  }
package/bin/api.mjs CHANGED
@@ -10,7 +10,8 @@ import { runMarkFilesAsViewed, } from "./commands/mark-files-as-viewed.mjs";
10
10
  import { runResolveMutate } from "./commands/resolve-mutate.mjs";
11
11
  import { runWithExecutionCwd } from "./execution-context.mjs";
12
12
  import { parsePrReference, normalizeRepositoryIdentity, resolveParsedPrTarget, } from "./pr-reference.mjs";
13
- import { getRepoInfo } from "./github/client.mjs";
13
+ import { getPullRequestBody, getRepoInfo } from "./github/client.mjs";
14
+ import { extractShepherdJournal } from "./journal/index.mjs";
14
15
  /** Raised before any API mutation when an input cannot be validated. */
15
16
  export class PrShepherdValidationError extends Error {
16
17
  constructor(message) {
@@ -50,6 +51,16 @@ export function createPrShepherd(options = {}) {
50
51
  }
51
52
  return Object.freeze({
52
53
  iterate,
54
+ getJournal(input) {
55
+ return runWithExecutionCwd(cwd, async () => {
56
+ const { prNumber, targetRepository } = resolvePrReference(input.pr);
57
+ if (!prNumber)
58
+ throw new PrShepherdValidationError("getJournal requires a PR reference");
59
+ const { owner, name } = targetRepository ?? (await getRepoInfo());
60
+ const { body } = await getPullRequestBody(prNumber, owner, name);
61
+ return extractShepherdJournal(body);
62
+ });
63
+ },
53
64
  apply(input) {
54
65
  return runWithExecutionCwd(cwd, async () => {
55
66
  validateApplyInput(input);
@@ -0,0 +1,9 @@
1
+ import type { RepoInfo } from "../github/client.mts";
2
+ import { type StateKey } from "../state/rest-cache.mts";
3
+ import type { TriageBudget } from "./triage-budget.mts";
4
+ /**
5
+ * `cacheable` gates the cross-tick cache — only set once the matched job has
6
+ * a terminal conclusion, so an in-progress job's (possibly partial) log
7
+ * never gets frozen into the cache.
8
+ */
9
+ export declare function fetchJobLogExcerpt(jobId: number, repo: RepoInfo, stateKey?: StateKey, cacheable?: boolean, budget?: TriageBudget): Promise<string | undefined>;
@@ -0,0 +1,30 @@
1
+ import { restText } from "../github/http.mjs";
2
+ import { loadDerived, storeDerived } from "../state/rest-cache.mjs";
3
+ import { buildLogExcerpt } from "./log-excerpt.mjs";
4
+ /**
5
+ * `cacheable` gates the cross-tick cache — only set once the matched job has
6
+ * a terminal conclusion, so an in-progress job's (possibly partial) log
7
+ * never gets frozen into the cache.
8
+ */
9
+ export async function fetchJobLogExcerpt(jobId, repo, stateKey, cacheable = false, budget) {
10
+ const cacheName = `joblog-v2-${jobId}`;
11
+ if (stateKey && cacheable) {
12
+ const cached = await loadDerived(stateKey, cacheName);
13
+ if (cached)
14
+ return cached.value ?? undefined;
15
+ }
16
+ const { owner, name } = repo;
17
+ try {
18
+ if (!budget?.canScheduleOptional())
19
+ return undefined;
20
+ const excerpt = buildLogExcerpt(await restText(`/repos/${owner}/${name}/actions/jobs/${jobId}/logs`, (rateLimit) => budget?.observe(rateLimit)));
21
+ if (stateKey && cacheable) {
22
+ await storeDerived(stateKey, cacheName, excerpt ?? null);
23
+ }
24
+ return excerpt;
25
+ }
26
+ catch (error) {
27
+ budget?.observeError(error);
28
+ return undefined;
29
+ }
30
+ }
@@ -0,0 +1,14 @@
1
+ export interface ActionsJob {
2
+ id?: number;
3
+ name: string;
4
+ workflow_name?: string;
5
+ conclusion: string | null;
6
+ run_attempt?: number;
7
+ steps?: Array<{
8
+ name: string;
9
+ number: number;
10
+ conclusion: string | null;
11
+ }>;
12
+ }
13
+ /** Name of the first step with a non-success, non-skipped, non-neutral conclusion. */
14
+ export declare function pickFailedStep(job: ActionsJob): string | undefined;
@@ -0,0 +1,7 @@
1
+ /** Name of the first step with a non-success, non-skipped, non-neutral conclusion. */
2
+ export function pickFailedStep(job) {
3
+ return job.steps?.find((s) => s.conclusion !== null &&
4
+ s.conclusion !== "success" &&
5
+ s.conclusion !== "skipped" &&
6
+ s.conclusion !== "neutral")?.name;
7
+ }
@@ -0,0 +1,11 @@
1
+ import type { RepoInfo } from "../github/client.mts";
2
+ import type { StateKey } from "../state/rest-cache.mts";
3
+ import type { RelatedFailedJob } from "../types/check-classification.mts";
4
+ import { type ActionsJob } from "./jobs-types.mts";
5
+ import type { TriageBudget } from "./triage-budget.mts";
6
+ /**
7
+ * Other failed jobs in the same workflow run, excluding the matched job and any
8
+ * job already surfaced as its own failing check (`surfacedNames`).
9
+ */
10
+ export declare function pickRelatedFailedJobs(jobs: ActionsJob[], matchedJobId: number | undefined, surfacedNames: ReadonlySet<string>): ActionsJob[];
11
+ export declare function fetchRelatedJobs(jobs: ActionsJob[], repo: RepoInfo, stateKey?: StateKey, budget?: TriageBudget): Promise<RelatedFailedJob[]>;
@@ -0,0 +1,32 @@
1
+ import { fetchJobLogExcerpt } from "./job-log.mjs";
2
+ import { pickFailedStep } from "./jobs-types.mjs";
3
+ /** Max sibling failed jobs reported (and log-fetched) per workflow run. */
4
+ const MAX_RELATED_JOBS = 5;
5
+ const FAILED_JOB_CONCLUSIONS = new Set(["failure", "timed_out"]);
6
+ /**
7
+ * Other failed jobs in the same workflow run, excluding the matched job and any
8
+ * job already surfaced as its own failing check (`surfacedNames`).
9
+ */
10
+ export function pickRelatedFailedJobs(jobs, matchedJobId, surfacedNames) {
11
+ return jobs
12
+ .filter((j) => j.conclusion !== null &&
13
+ FAILED_JOB_CONCLUSIONS.has(j.conclusion) &&
14
+ (j.id === undefined || j.id !== matchedJobId) &&
15
+ !surfacedNames.has(j.name))
16
+ .slice(0, MAX_RELATED_JOBS);
17
+ }
18
+ export async function fetchRelatedJobs(jobs, repo, stateKey, budget) {
19
+ const logs = await Promise.all(jobs.map((job) => job.id === undefined
20
+ ? undefined
21
+ : fetchJobLogExcerpt(job.id, repo, stateKey, job.conclusion !== null, budget)));
22
+ return jobs.map((job, i) => {
23
+ const failedStep = pickFailedStep(job);
24
+ const logExcerpt = logs[i];
25
+ return {
26
+ name: job.name,
27
+ conclusion: (job.conclusion ?? "").toUpperCase(),
28
+ ...(failedStep !== undefined && { failedStep }),
29
+ ...(logExcerpt !== undefined && { logExcerpt }),
30
+ };
31
+ });
32
+ }
@@ -0,0 +1,17 @@
1
+ import type { RateLimitInfo } from "../github/client.mts";
2
+ /** One optional Actions-enrichment budget for the entire PR snapshot. */
3
+ export declare class TriageBudget {
4
+ private exhausted;
5
+ private secondaryError;
6
+ private omitted;
7
+ private omissionReported;
8
+ get canSchedule(): boolean;
9
+ get primaryExhausted(): boolean;
10
+ /** Call only when a needed optional request is about to be scheduled. */
11
+ canScheduleOptional(): boolean;
12
+ observe(rateLimit?: RateLimitInfo): void;
13
+ observeError(error: unknown): void;
14
+ throwIfSecondary(): void;
15
+ reportOmissionIfNeeded(): void;
16
+ private stopForPrimaryLimit;
17
+ }
@@ -0,0 +1,49 @@
1
+ import { pollRateLimitRetryAfterMs } from "../commands/poll-quota.mjs";
2
+ /** One optional Actions-enrichment budget for the entire PR snapshot. */
3
+ export class TriageBudget {
4
+ exhausted = false;
5
+ secondaryError;
6
+ omitted = false;
7
+ omissionReported = false;
8
+ get canSchedule() {
9
+ return !this.exhausted && this.secondaryError === undefined;
10
+ }
11
+ get primaryExhausted() {
12
+ return this.exhausted;
13
+ }
14
+ /** Call only when a needed optional request is about to be scheduled. */
15
+ canScheduleOptional() {
16
+ if (this.canSchedule)
17
+ return true;
18
+ if (this.exhausted)
19
+ this.omitted = true;
20
+ return false;
21
+ }
22
+ observe(rateLimit) {
23
+ if (rateLimit?.remaining === 0)
24
+ this.stopForPrimaryLimit();
25
+ }
26
+ observeError(error) {
27
+ const retry = pollRateLimitRetryAfterMs(error);
28
+ if (retry?.kind === "secondary") {
29
+ this.secondaryError ??= error;
30
+ }
31
+ else if (retry?.kind === "primary") {
32
+ this.stopForPrimaryLimit();
33
+ this.omitted = true;
34
+ }
35
+ }
36
+ throwIfSecondary() {
37
+ if (this.secondaryError !== undefined)
38
+ throw this.secondaryError;
39
+ }
40
+ reportOmissionIfNeeded() {
41
+ if (!this.omitted || this.omissionReported || this.secondaryError !== undefined)
42
+ return;
43
+ this.omissionReported = true;
44
+ process.stderr.write("pr-shepherd: REST core quota is exhausted; optional Actions job and log enrichment is incomplete\n");
45
+ }
46
+ stopForPrimaryLimit() {
47
+ this.exhausted = true;
48
+ }
49
+ }
@@ -1,5 +1,8 @@
1
1
  import type { CheckRun, ClassifiedCheck, TriagedCheck } from "../types.mts";
2
2
  import type { RepoInfo } from "../github/client.mts";
3
- import { type StateKey } from "../state/rest-cache.mts";
4
- export declare function triageFailingChecks(failingChecks: ClassifiedCheck[], repo: RepoInfo, stateKey?: StateKey): Promise<TriagedCheck[]>;
5
- export declare function fetchStartupFailureChecks(repo: RepoInfo, headSha: string, prNumber: number, stateKey?: StateKey): Promise<CheckRun[]>;
3
+ import type { StateKey } from "../state/rest-cache.mts";
4
+ import { TriageBudget } from "./triage-budget.mts";
5
+ export declare function triageFailingChecks(failingChecks: ClassifiedCheck[], repo: RepoInfo, stateKey?: StateKey, budget?: TriageBudget,
6
+ /** Non-failing checks (ignored, filtered, superseded, …) whose jobs must not resurface as related jobs. */
7
+ otherChecks?: ClassifiedCheck[]): Promise<TriagedCheck[]>;
8
+ export declare function fetchStartupFailureChecks(repo: RepoInfo, headSha: string, prNumber: number, stateKey?: StateKey, budget?: TriageBudget): Promise<CheckRun[]>;
@@ -1,22 +1,38 @@
1
1
  /* eslint-disable max-lines */
2
- import { restWithRateLimit, restText } from "../github/http.mjs";
3
- import { loadDerived, storeDerived } from "../state/rest-cache.mjs";
4
- import { buildLogExcerpt } from "./log-excerpt.mjs";
2
+ import { restWithRateLimit } from "../github/http.mjs";
3
+ import { fetchJobLogExcerpt } from "./job-log.mjs";
4
+ import { pickFailedStep } from "./jobs-types.mjs";
5
+ import { pickRelatedFailedJobs, fetchRelatedJobs } from "./related-jobs.mjs";
6
+ import { TriageBudget } from "./triage-budget.mjs";
7
+ import { mapPool } from "../util/pool.mjs";
5
8
  const STARTUP_FAILURE_STATUS = "startup_failure";
6
- export function triageFailingChecks(failingChecks, repo, stateKey) {
9
+ export async function triageFailingChecks(failingChecks, repo, stateKey, budget = new TriageBudget(),
10
+ /** Non-failing checks (ignored, filtered, superseded, …) whose jobs must not resurface as related jobs. */
11
+ otherChecks = []) {
7
12
  const jobsCache = new Map();
8
- return Promise.all(failingChecks.map((c) => triageCheck(c, repo, jobsCache, stateKey)));
13
+ const surfaced = surfacedNamesByRun([...failingChecks, ...otherChecks]);
14
+ const relatedOwners = firstCheckPerRun(failingChecks);
15
+ const checks = await mapPool(failingChecks, 4, (check) => triageCheck(check, repo, jobsCache, stateKey, budget, relatedOwners.has(check) ? surfaced.get(check.runId ?? "") : undefined));
16
+ budget.throwIfSecondary();
17
+ budget.reportOmissionIfNeeded();
18
+ return checks;
9
19
  }
10
- async function triageCheck(check, repo, jobsCache, stateKey) {
20
+ async function triageCheck(check, repo, jobsCache, stateKey, budget, relatedSurfaced) {
11
21
  if (check.runId === null || check.conclusion === "STARTUP_FAILURE") {
12
22
  return { ...check };
13
23
  }
14
- const jobs = await fetchJobs(check.runId, repo, jobsCache, stateKey);
24
+ if (!jobsCache.has(check.runId) && !budget?.canScheduleOptional()) {
25
+ return { ...check };
26
+ }
27
+ const jobs = await fetchJobs(check.runId, repo, jobsCache, stateKey, budget);
15
28
  const jobInfo = jobs ? pickJobInfo(jobs, check.name) : undefined;
16
29
  const runAttempt = jobs ? pickRunAttempt(jobs) : undefined;
17
30
  const logExcerpt = check.conclusion !== "CANCELLED" && jobInfo?.jobId
18
- ? await fetchJobLogExcerpt(jobInfo.jobId, repo, stateKey, jobInfo.jobConclusion != null)
31
+ ? await fetchJobLogExcerpt(jobInfo.jobId, repo, stateKey, jobInfo.jobConclusion != null, budget)
19
32
  : undefined;
33
+ const relatedJobs = jobs && relatedSurfaced && check.conclusion !== "CANCELLED"
34
+ ? await fetchRelatedJobs(pickRelatedFailedJobs(jobs, jobInfo?.jobId, relatedSurfaced), repo, stateKey, budget)
35
+ : [];
20
36
  return {
21
37
  ...check,
22
38
  ...(runAttempt !== undefined && { runAttempt }),
@@ -24,40 +40,79 @@ async function triageCheck(check, repo, jobsCache, stateKey) {
24
40
  ...(jobInfo?.jobName !== undefined && { jobName: jobInfo.jobName }),
25
41
  ...(jobInfo?.failedStep !== undefined && { failedStep: jobInfo.failedStep }),
26
42
  ...(logExcerpt !== undefined && { logExcerpt }),
43
+ ...(relatedJobs.length > 0 && { relatedJobs }),
27
44
  };
28
45
  }
29
- export async function fetchStartupFailureChecks(repo, headSha, prNumber, stateKey) {
46
+ function surfacedNamesByRun(checks) {
47
+ const byRun = new Map();
48
+ for (const check of checks) {
49
+ if (check.runId === null)
50
+ continue;
51
+ const names = byRun.get(check.runId) ?? new Set();
52
+ names.add(check.name);
53
+ byRun.set(check.runId, names);
54
+ }
55
+ return byRun;
56
+ }
57
+ /** Sibling failed jobs are reported once per run, on its first failing check. */
58
+ function firstCheckPerRun(checks) {
59
+ const seen = new Set();
60
+ const first = new Set();
61
+ for (const check of checks) {
62
+ if (check.runId === null || seen.has(check.runId))
63
+ continue;
64
+ if (check.conclusion === "CANCELLED" || check.conclusion === "STARTUP_FAILURE")
65
+ continue;
66
+ seen.add(check.runId);
67
+ first.add(check);
68
+ }
69
+ return first;
70
+ }
71
+ export async function fetchStartupFailureChecks(repo, headSha, prNumber, stateKey, budget = new TriageBudget()) {
30
72
  try {
31
- return await fetchStartupFailureChecksUncached(repo, headSha, prNumber, stateKey);
73
+ return await fetchStartupFailureChecksUncached(repo, headSha, prNumber, stateKey, budget);
32
74
  }
33
75
  catch (err) {
76
+ budget.observeError(err);
77
+ budget.throwIfSecondary();
78
+ if (budget.primaryExhausted)
79
+ return [];
34
80
  const msg = err instanceof Error ? err.message : String(err);
35
81
  process.stderr.write(`pr-shepherd: startup-failure run fetch failed for PR #${prNumber} at ${headSha} (ignored): ${msg}\n`);
36
82
  return [];
37
83
  }
38
84
  }
39
- async function fetchStartupFailureChecksUncached(repo, headSha, prNumber, stateKey) {
85
+ async function fetchStartupFailureChecksUncached(repo, headSha, prNumber, stateKey, budget) {
40
86
  const { owner, name } = repo;
41
87
  const perPage = 100;
42
88
  const MAX_RUN_PAGES = 10;
43
89
  const checks = [];
44
90
  for (let page = 1; page <= MAX_RUN_PAGES; page++) {
45
- const { data, rateLimit } = await restWithRateLimit("GET", `/repos/${owner}/${name}/actions/runs?head_sha=${encodeURIComponent(headSha)}&status=${STARTUP_FAILURE_STATUS}&per_page=${perPage}&page=${page}`, undefined, stateKey
46
- ? {
47
- conditional: {
48
- key: stateKey,
49
- name: `runs-startupfailure-${headSha}-p${page}`,
50
- headSha,
51
- },
52
- }
53
- : undefined);
91
+ if (!budget?.canScheduleOptional())
92
+ break;
93
+ let result;
94
+ try {
95
+ result = await restWithRateLimit("GET", `/repos/${owner}/${name}/actions/runs?head_sha=${encodeURIComponent(headSha)}&status=${STARTUP_FAILURE_STATUS}&per_page=${perPage}&page=${page}`, undefined, stateKey
96
+ ? {
97
+ conditional: {
98
+ key: stateKey,
99
+ name: `runs-startupfailure-${headSha}-p${page}`,
100
+ headSha,
101
+ },
102
+ }
103
+ : undefined);
104
+ }
105
+ catch (error) {
106
+ budget?.observeError(error);
107
+ if (budget?.primaryExhausted)
108
+ break;
109
+ throw error;
110
+ }
111
+ const { data, rateLimit } = result;
112
+ budget?.observe(rateLimit);
54
113
  checks.push(...data.workflow_runs
55
114
  .filter((run) => runBelongsToPr(run, prNumber, headSha))
56
115
  .map(workflowRunToCheckRun));
57
- if (rateLimit?.remaining === 0) {
58
- process.stderr.write(`pr-shepherd: REST rate limit remaining is 0 while listing startup-failure runs for ${headSha} — detection may be incomplete\n`);
59
- break;
60
- }
61
116
  if (data.workflow_runs.length < perPage)
62
117
  break;
63
118
  if (page === MAX_RUN_PAGES) {
@@ -95,15 +150,15 @@ function pickRunAttempt(jobs) {
95
150
  function runBelongsToPr(run, prNumber, headSha) {
96
151
  return (run.pull_requests ?? []).some((pr) => pr.number === prNumber && (pr.head?.sha ?? headSha) === headSha);
97
152
  }
98
- function fetchJobs(runId, repo, cache, stateKey) {
153
+ function fetchJobs(runId, repo, cache, stateKey, budget) {
99
154
  const cached = cache.get(runId);
100
155
  if (cached)
101
156
  return cached;
102
- const promise = fetchJobsUncached(runId, repo, stateKey);
157
+ const promise = fetchJobsUncached(runId, repo, stateKey, budget);
103
158
  cache.set(runId, promise);
104
159
  return promise;
105
160
  }
106
- async function fetchJobsUncached(runId, repo, stateKey) {
161
+ async function fetchJobsUncached(runId, repo, stateKey, budget) {
107
162
  const { owner, name } = repo;
108
163
  const perPage = 100;
109
164
  const MAX_JOB_PAGES = 20; // 2000 jobs max
@@ -111,6 +166,8 @@ async function fetchJobsUncached(runId, repo, stateKey) {
111
166
  const allJobs = [];
112
167
  try {
113
168
  for (let page = 1;; page++) {
169
+ if (!budget?.canScheduleOptional())
170
+ break;
114
171
  if (++pagesFetched > MAX_JOB_PAGES) {
115
172
  process.stderr.write(`pr-shepherd: job pagination cap (${MAX_JOB_PAGES * 100} jobs) reached for run ${runId} — triage may be incomplete\n`);
116
173
  break;
@@ -118,17 +175,15 @@ async function fetchJobsUncached(runId, repo, stateKey) {
118
175
  const { data, rateLimit } = await restWithRateLimit("GET", `/repos/${owner}/${name}/actions/runs/${runId}/jobs?filter=latest&per_page=${perPage}&page=${page}`, undefined, stateKey
119
176
  ? { conditional: { key: stateKey, name: `jobs-run-${runId}-p${page}` } }
120
177
  : undefined);
178
+ budget?.observe(rateLimit);
121
179
  allJobs.push(...data.jobs);
122
- if (rateLimit?.remaining === 0) {
123
- process.stderr.write(`pr-shepherd: REST rate limit remaining is 0 while listing jobs for run ${runId} — triage may be incomplete\n`);
124
- break;
125
- }
126
180
  if (data.jobs.length < perPage)
127
181
  break;
128
182
  }
129
183
  }
130
- catch {
131
- return undefined;
184
+ catch (error) {
185
+ budget?.observeError(error);
186
+ return budget?.primaryExhausted ? allJobs : undefined;
132
187
  }
133
188
  return allJobs;
134
189
  }
@@ -140,10 +195,7 @@ function pickJobInfo(jobs, checkName) {
140
195
  matchedJobs[0];
141
196
  if (!job)
142
197
  return undefined;
143
- const failedStep = job.steps?.find((s) => s.conclusion !== null &&
144
- s.conclusion !== "success" &&
145
- s.conclusion !== "skipped" &&
146
- s.conclusion !== "neutral")?.name;
198
+ const failedStep = pickFailedStep(job);
147
199
  return {
148
200
  ...(job.id !== undefined && { jobId: job.id }),
149
201
  workflowName: job.workflow_name,
@@ -152,27 +204,3 @@ function pickJobInfo(jobs, checkName) {
152
204
  jobConclusion: job.conclusion,
153
205
  };
154
206
  }
155
- /**
156
- * `cacheable` gates the cross-tick cache — only set once the matched job has
157
- * a terminal conclusion, so an in-progress job's (possibly partial) log
158
- * never gets frozen into the cache.
159
- */
160
- async function fetchJobLogExcerpt(jobId, repo, stateKey, cacheable = false) {
161
- const cacheName = `joblog-v2-${jobId}`;
162
- if (stateKey && cacheable) {
163
- const cached = await loadDerived(stateKey, cacheName);
164
- if (cached)
165
- return cached.value ?? undefined;
166
- }
167
- const { owner, name } = repo;
168
- try {
169
- const excerpt = buildLogExcerpt(await restText(`/repos/${owner}/${name}/actions/jobs/${jobId}/logs`));
170
- if (stateKey && cacheable) {
171
- await storeDerived(stateKey, cacheName, excerpt ?? null);
172
- }
173
- return excerpt;
174
- }
175
- catch {
176
- return undefined;
177
- }
178
- }
@@ -31,9 +31,9 @@ function recommendation(warning) {
31
31
  return "- Recommendation: keep polling pr-shepherd at the cadence above; both GraphQL and REST core are below their warning thresholds, so do not shift work between them. Do not substitute `gh pr checks` or `gh pr watch`";
32
32
  }
33
33
  if (warning.resource === "core") {
34
- return "- Recommendation: keep polling pr-shepherd at the cadence above; do not add incidental REST `gh` calls (`gh pr view`, `gh pr review`, `gh api`) while REST core is low. Do not substitute `gh pr checks` or `gh pr watch`";
34
+ return "- Recommendation: keep polling pr-shepherd at the cadence above; do not add incidental REST `gh api repos/OWNER/REPO/pulls/PR` calls while REST core is low. Do not substitute `gh pr checks` or `gh pr watch`";
35
35
  }
36
- return "- Recommendation: keep polling pr-shepherd at the cadence above; for incidental PR operations prefer REST `gh` (`gh pr view`, `gh pr review`, `gh api`); do not substitute `gh pr checks` or `gh pr watch`";
36
+ return "- Recommendation: keep polling pr-shepherd at the cadence above; for incidental PR reads use explicit REST endpoints such as `gh api repos/OWNER/REPO/pulls/PR`; do not substitute `gh pr checks` or `gh pr watch`";
37
37
  }
38
38
  export function formatApiUsage(usage) {
39
39
  if (usage === undefined)
@@ -6,6 +6,7 @@ import { renderThreadBullet, renderReviewBullet, renderThreadResolutionStatusTag
6
6
  import { BODY_TRUNCATE_MAX_CHARS } from "./body-truncate.mjs";
7
7
  import { numberInstructions } from "./iterate-instructions.mjs";
8
8
  import { renderCheckAnnotation, renderProtectedRun } from "./fix-formatter-extra.mjs";
9
+ import { renderRelatedJobLines } from "./related-jobs-format.mjs";
9
10
  import { isFailingAgentCheck } from "../checks/conclusions.mjs";
10
11
  import { renderMergeCommand } from "../commands/iterate/merge.mjs";
11
12
  import { partitionFixThreads } from "../commands/iterate/fix-instruction-threads.mjs";
@@ -81,6 +82,7 @@ export function formatFixCodeResult(header, result, opts = {}) {
81
82
  lines.push(` > ${ch.summary}`);
82
83
  if (ch.logExcerpt)
83
84
  lines.push(indentBlockquote(ch.logExcerpt, " "));
85
+ lines.push(...renderRelatedJobLines(ch.relatedJobs));
84
86
  }
85
87
  if (ch.rerunCommand && ch.runId && !seenRerunRunIds.has(ch.runId)) {
86
88
  seenRerunRunIds.add(ch.runId);
@@ -212,7 +212,7 @@ Flags:
212
212
 
213
213
  PR may be a number or GitHub pull request URL. When omitted, the current branch PR is inferred.
214
214
  Exit code: 0 on success; nonzero on failure (sysexits.h — see docs/exit-codes.md).`;
215
- readonly iterate: "pr-shepherd iterate\n\nRun one iterate tick for a pull request. The no-subcommand form polls; use this subcommand for a single tick.\nThe output contains one action and an action-specific ## Instructions section.\n\nUsage:\n pr-shepherd iterate [PR] [iterate-flags]\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Legacy no-op; workflow runs are never cancelled.\n --merge Shepherd through readiness, then emit a merge or merge-queue command.\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields.\n --help, -h Print this help and exit before GitHub, git, config, or log I/O.\n\nDurations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number is minutes; decimals are allowed only with an explicit unit (4.5m).\n\nActions:\n WAIT No immediate action; continue with the next poll.\n MARK_READY Draft PR was marked ready; continue with the next poll.\n READY Clean PR is inside the ready-delay. Wait out remainingSeconds, then poll again.\n FIX_CODE Agent action is required; follow the instructions, then continue polling.\n CANCEL Stop polling: merged/closed or ready-delay elapsed.\n ESCALATE Stop polling until a human provides direction.\n MERGE Run the emitted merge/queue command, then continue monitoring.\n\nExit codes:\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT or READY\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\n 15 MERGE\n A command/validation/GitHub failure exits with a sysexits.h code instead (see docs/exit-codes.md).";
215
+ readonly iterate: "pr-shepherd iterate\n\nRun one iterate tick for a pull request. The no-subcommand form polls; use this subcommand for a single tick.\nThe output contains one action and an action-specific ## Instructions section.\n\nUsage:\n pr-shepherd iterate [PR] [iterate-flags]\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Legacy no-op; workflow runs are never cancelled.\n --merge Shepherd through readiness, then emit a merge or merge-queue command.\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields.\n --help, -h Print this help and exit before GitHub, git, config, or log I/O.\n\nDurations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number is minutes; decimals are allowed only with an explicit unit (4.5m).\n\nActions:\n WAIT No immediate action; continue with the next poll.\n MARK_READY Draft PR was marked ready; continue with the next poll.\n READY Clean PR is inside the ready-delay. Schedule the rerun for when remainingSeconds elapses. Do not invent unrelated work. Already-owned later work may continue.\n FIX_CODE Agent action is required; follow the instructions, then continue polling.\n CANCEL Stop polling: merged/closed or ready-delay elapsed.\n ESCALATE Stop polling until a human provides direction.\n MERGE Run the emitted merge/queue command, then continue monitoring.\n\nExit codes:\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT or READY\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\n 15 MERGE\n A command/validation/GitHub failure exits with a sysexits.h code instead (see docs/exit-codes.md).";
216
216
  readonly poll: "pr-shepherd poll\n\nRun iterate repeatedly for one PR, or read compact summaries for an explicit PR set or native\nGitHub stack. Aggregate mode returns when any row needs work, every row is terminal, or timeout.\nPoll exits as soon as iterate returns READY, MARK_READY, CANCEL, or ESCALATE, or when timeout\nreturns the last WAIT result. FIX_CODE starts a --debounce settle window (default:\npoll.debounceSeconds; built-in 1m): poll keeps\niterating at --interval, then runs one more tick after the window and returns that result.\nWith --until-terminal or --merge, poll also continues through MARK_READY.\n\nUsage:\n pr-shepherd poll [PR ...] [poll-flags] [iterate-flags]\n pr-shepherd poll --stack PR [poll-flags] [iterate-flags]\n\nPoll flags:\n --stack PR Select all entries in PR's native GitHub stack, bottom to top.\n --interval <duration> Sleep between WAIT ticks. Bare number = seconds. Default: poll.intervalSeconds (built-in 60s). Stack and multi-PR polls multiply that by poll.stackIntervalFactor (built-in 2) unless this flag is set.\n --timeout <duration> Maximum wall-clock wait for WAIT ticks. Bare number = seconds. Default: poll.timeoutSeconds (built-in 4.5m).\n --debounce <duration> Settle window after first FIX_CODE or stack SHEPHERD before returning. Bare number = seconds. Default: poll.debounceSeconds (built-in 60s). 0 disables.\n --quiet-status Print only changed WAIT snapshots. Overrides poll.quietStatus.\n --no-quiet-status Print every WAIT snapshot. Overrides poll.quietStatus.\n --until-terminal Continue through WAIT/MARK_READY until READY/FIX_CODE/MERGE/CANCEL/ESCALATE or stack SHEPHERD.\n\nForwarded iterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Legacy no-op; workflow runs are never cancelled.\n --merge Shepherd through readiness, then emit a merge or merge-queue command.\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed per-tick lines.\n --help, -h Print this help and exit before GitHub, git, config, or log I/O.\n\nDurations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number uses each flag's default unit (seconds\nfor --interval/--timeout/--debounce, minutes for --ready-delay/--stall-timeout); decimals are allowed only with\nan explicit unit (4.5m).\nEach WAIT tick writes a stderr line naming what it is waiting on by default; poll.quietStatus can change that default, --quiet-status/--no-quiet-status override it, and --verbose emits detailed per-tick lines.\nFIX_CODE debounce writes a remaining-seconds line to stderr. --timeout does not cut an in-flight debounce short.\nWith --until-terminal, --timeout is ignored for WAIT ticks and polling continues until READY, FIX_CODE, MERGE, CANCEL, or ESCALATE. With --merge, --timeout still bounds WAIT ticks; it only continues through MARK_READY while polling remains within that timeout.\n\nExit codes: same as iterate (the final tick's action/reason decides the code).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT or READY (including a WAIT returned by --timeout)\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\n 15 MERGE\n 16 SHEPHERD (--stack only: run the listed one-PR sessions, then rerun the selector)\n A command/validation/GitHub failure exits with a sysexits.h code instead (see docs/exit-codes.md).";
217
217
  readonly clean: `pr-shepherd clean
218
218
 
@@ -1,4 +1,4 @@
1
- export declare const ITERATE_USAGE = "pr-shepherd iterate\n\nRun one iterate tick for a pull request. The no-subcommand form polls; use this subcommand for a single tick.\nThe output contains one action and an action-specific ## Instructions section.\n\nUsage:\n pr-shepherd iterate [PR] [iterate-flags]\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Legacy no-op; workflow runs are never cancelled.\n --merge Shepherd through readiness, then emit a merge or merge-queue command.\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields.\n --help, -h Print this help and exit before GitHub, git, config, or log I/O.\n\nDurations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number is minutes; decimals are allowed only with an explicit unit (4.5m).\n\nActions:\n WAIT No immediate action; continue with the next poll.\n MARK_READY Draft PR was marked ready; continue with the next poll.\n READY Clean PR is inside the ready-delay. Wait out remainingSeconds, then poll again.\n FIX_CODE Agent action is required; follow the instructions, then continue polling.\n CANCEL Stop polling: merged/closed or ready-delay elapsed.\n ESCALATE Stop polling until a human provides direction.\n MERGE Run the emitted merge/queue command, then continue monitoring.\n\nExit codes:\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT or READY\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\n 15 MERGE\n A command/validation/GitHub failure exits with a sysexits.h code instead (see docs/exit-codes.md).";
1
+ export declare const ITERATE_USAGE = "pr-shepherd iterate\n\nRun one iterate tick for a pull request. The no-subcommand form polls; use this subcommand for a single tick.\nThe output contains one action and an action-specific ## Instructions section.\n\nUsage:\n pr-shepherd iterate [PR] [iterate-flags]\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Legacy no-op; workflow runs are never cancelled.\n --merge Shepherd through readiness, then emit a merge or merge-queue command.\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields.\n --help, -h Print this help and exit before GitHub, git, config, or log I/O.\n\nDurations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number is minutes; decimals are allowed only with an explicit unit (4.5m).\n\nActions:\n WAIT No immediate action; continue with the next poll.\n MARK_READY Draft PR was marked ready; continue with the next poll.\n READY Clean PR is inside the ready-delay. Schedule the rerun for when remainingSeconds elapses. Do not invent unrelated work. Already-owned later work may continue.\n FIX_CODE Agent action is required; follow the instructions, then continue polling.\n CANCEL Stop polling: merged/closed or ready-delay elapsed.\n ESCALATE Stop polling until a human provides direction.\n MERGE Run the emitted merge/queue command, then continue monitoring.\n\nExit codes:\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT or READY\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\n 15 MERGE\n A command/validation/GitHub failure exits with a sysexits.h code instead (see docs/exit-codes.md).";
2
2
  export declare const POLL_USAGE = "pr-shepherd poll\n\nRun iterate repeatedly for one PR, or read compact summaries for an explicit PR set or native\nGitHub stack. Aggregate mode returns when any row needs work, every row is terminal, or timeout.\nPoll exits as soon as iterate returns READY, MARK_READY, CANCEL, or ESCALATE, or when timeout\nreturns the last WAIT result. FIX_CODE starts a --debounce settle window (default:\npoll.debounceSeconds; built-in 1m): poll keeps\niterating at --interval, then runs one more tick after the window and returns that result.\nWith --until-terminal or --merge, poll also continues through MARK_READY.\n\nUsage:\n pr-shepherd poll [PR ...] [poll-flags] [iterate-flags]\n pr-shepherd poll --stack PR [poll-flags] [iterate-flags]\n\nPoll flags:\n --stack PR Select all entries in PR's native GitHub stack, bottom to top.\n --interval <duration> Sleep between WAIT ticks. Bare number = seconds. Default: poll.intervalSeconds (built-in 60s). Stack and multi-PR polls multiply that by poll.stackIntervalFactor (built-in 2) unless this flag is set.\n --timeout <duration> Maximum wall-clock wait for WAIT ticks. Bare number = seconds. Default: poll.timeoutSeconds (built-in 4.5m).\n --debounce <duration> Settle window after first FIX_CODE or stack SHEPHERD before returning. Bare number = seconds. Default: poll.debounceSeconds (built-in 60s). 0 disables.\n --quiet-status Print only changed WAIT snapshots. Overrides poll.quietStatus.\n --no-quiet-status Print every WAIT snapshot. Overrides poll.quietStatus.\n --until-terminal Continue through WAIT/MARK_READY until READY/FIX_CODE/MERGE/CANCEL/ESCALATE or stack SHEPHERD.\n\nForwarded iterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Legacy no-op; workflow runs are never cancelled.\n --merge Shepherd through readiness, then emit a merge or merge-queue command.\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed per-tick lines.\n --help, -h Print this help and exit before GitHub, git, config, or log I/O.\n\nDurations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number uses each flag's default unit (seconds\nfor --interval/--timeout/--debounce, minutes for --ready-delay/--stall-timeout); decimals are allowed only with\nan explicit unit (4.5m).\nEach WAIT tick writes a stderr line naming what it is waiting on by default; poll.quietStatus can change that default, --quiet-status/--no-quiet-status override it, and --verbose emits detailed per-tick lines.\nFIX_CODE debounce writes a remaining-seconds line to stderr. --timeout does not cut an in-flight debounce short.\nWith --until-terminal, --timeout is ignored for WAIT ticks and polling continues until READY, FIX_CODE, MERGE, CANCEL, or ESCALATE. With --merge, --timeout still bounds WAIT ticks; it only continues through MARK_READY while polling remains within that timeout.\n\nExit codes: same as iterate (the final tick's action/reason decides the code).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT or READY (including a WAIT returned by --timeout)\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\n 15 MERGE\n 16 SHEPHERD (--stack only: run the listed one-PR sessions, then rerun the selector)\n A command/validation/GitHub failure exits with a sysexits.h code instead (see docs/exit-codes.md).";
3
3
  /** Public help page for the default PR polling invocation. */
4
4
  export declare const DEFAULT_USAGE: string;
@@ -21,7 +21,7 @@ Durations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number is minutes; decima
21
21
  Actions:
22
22
  WAIT No immediate action; continue with the next poll.
23
23
  MARK_READY Draft PR was marked ready; continue with the next poll.
24
- READY Clean PR is inside the ready-delay. Wait out remainingSeconds, then poll again.
24
+ READY Clean PR is inside the ready-delay. Schedule the rerun for when remainingSeconds elapses. Do not invent unrelated work. Already-owned later work may continue.
25
25
  FIX_CODE Agent action is required; follow the instructions, then continue polling.
26
26
  CANCEL Stop polling: merged/closed or ready-delay elapsed.
27
27
  ESCALATE Stop polling until a human provides direction.