pr-shepherd 0.44.1 → 0.46.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 (119) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +19 -8
  3. package/bin/checks/conclusions.d.mts +8 -1
  4. package/bin/checks/conclusions.mjs +12 -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 +22 -5
  12. package/bin/cli/help-command-pages.d.mts +13 -0
  13. package/bin/cli/help-command-pages.mjs +13 -0
  14. package/bin/cli/help-top-page.d.mts +1 -1
  15. package/bin/cli/help-top-page.mjs +2 -0
  16. package/bin/cli/help.d.mts +14 -1
  17. package/bin/cli/help.mjs +2 -0
  18. package/bin/cli/iterate-activity-formatter.d.mts +2 -0
  19. package/bin/cli/iterate-activity-formatter.mjs +26 -0
  20. package/bin/cli/iterate-checks-formatter.d.mts +2 -0
  21. package/bin/cli/iterate-checks-formatter.mjs +55 -0
  22. package/bin/cli/iterate-formatter.mjs +17 -29
  23. package/bin/cli/iterate-instructions.mjs +14 -1
  24. package/bin/cli/iterate-lean.mjs +5 -0
  25. package/bin/cli/journal-extract-handler.d.mts +2 -0
  26. package/bin/cli/journal-extract-handler.mjs +48 -0
  27. package/bin/cli/mutate-formatter.mjs +2 -0
  28. package/bin/cli/safe-body-file.d.mts +14 -0
  29. package/bin/cli/safe-body-file.mjs +42 -0
  30. package/bin/cli-parser.mjs +7 -0
  31. package/bin/commands/check-annotations.d.mts +2 -1
  32. package/bin/commands/check-annotations.mjs +7 -7
  33. package/bin/commands/check-status.mjs +4 -4
  34. package/bin/commands/check.mjs +31 -9
  35. package/bin/commands/iterate/api-usage.d.mts +2 -0
  36. package/bin/commands/iterate/api-usage.mjs +35 -0
  37. package/bin/commands/iterate/check-instructions.d.mts +1 -1
  38. package/bin/commands/iterate/check-instructions.mjs +13 -18
  39. package/bin/commands/iterate/classify.mjs +2 -1
  40. package/bin/commands/iterate/escalate.mjs +63 -9
  41. package/bin/commands/iterate/fix-code.mjs +110 -73
  42. package/bin/commands/iterate/index.mjs +8 -5
  43. package/bin/commands/iterate/merge-state.mjs +5 -2
  44. package/bin/commands/iterate/render.mjs +13 -6
  45. package/bin/commands/iterate/run.d.mts +2 -0
  46. package/bin/commands/iterate/run.mjs +8 -0
  47. package/bin/commands/mark-files-as-viewed.mjs +8 -0
  48. package/bin/commands/poll-run.d.mts +2 -0
  49. package/bin/commands/poll-run.mjs +8 -0
  50. package/bin/commands/poll.d.mts +1 -2
  51. package/bin/commands/poll.mjs +26 -10
  52. package/bin/comments/rate-limit.d.mts +4 -0
  53. package/bin/comments/rate-limit.mjs +6 -0
  54. package/bin/comments/review-visibility.d.mts +1 -1
  55. package/bin/comments/review-visibility.mjs +3 -2
  56. package/bin/comments/thread-visibility.d.mts +1 -1
  57. package/bin/comments/thread-visibility.mjs +9 -3
  58. package/bin/config/load.d.mts +6 -1
  59. package/bin/config/load.mjs +33 -1
  60. package/bin/config.json +6 -1
  61. package/bin/github/api-telemetry-aggregate.d.mts +32 -0
  62. package/bin/github/api-telemetry-aggregate.mjs +84 -0
  63. package/bin/github/api-telemetry.d.mts +13 -0
  64. package/bin/github/api-telemetry.mjs +128 -0
  65. package/bin/github/check-annotations.d.mts +14 -1
  66. package/bin/github/check-annotations.mjs +29 -2
  67. package/bin/github/client.d.mts +1 -1
  68. package/bin/github/client.mjs +1 -1
  69. package/bin/github/errors.d.mts +2 -0
  70. package/bin/github/errors.mjs +2 -0
  71. package/bin/github/gql/batch-pr-page.gql +8 -0
  72. package/bin/github/gql/batch-pr.gql +8 -0
  73. package/bin/github/gql/check-run-annotations.gql +8 -0
  74. package/bin/github/gql/commit-check-contexts.gql +8 -0
  75. package/bin/github/gql/get-pr-body.gql +8 -0
  76. package/bin/github/gql/get-pr-head-sha.gql +8 -0
  77. package/bin/github/gql/pr-number-by-branch.gql +8 -0
  78. package/bin/github/gql/review-thread-comments.gql +8 -0
  79. package/bin/github/gql/suggestion-threads.gql +8 -0
  80. package/bin/github/graphql-http.mjs +63 -8
  81. package/bin/github/http-auth.d.mts +9 -1
  82. package/bin/github/http-auth.mjs +35 -14
  83. package/bin/github/http-intermediate.d.mts +9 -0
  84. package/bin/github/http-intermediate.mjs +20 -0
  85. package/bin/github/http-request.d.mts +1 -1
  86. package/bin/github/http-request.mjs +1 -1
  87. package/bin/github/http-utils.d.mts +6 -0
  88. package/bin/github/http-utils.mjs +10 -1
  89. package/bin/github/rest-http.d.mts +16 -1
  90. package/bin/github/rest-http.mjs +71 -8
  91. package/bin/github/rest-text.mjs +41 -5
  92. package/bin/index.mjs +2 -1
  93. package/bin/log/session.d.mts +11 -0
  94. package/bin/log/session.mjs +22 -0
  95. package/bin/mcp/server.mjs +16 -2
  96. package/bin/pr-reference.d.mts +1 -1
  97. package/bin/pr-reference.mjs +1 -1
  98. package/bin/quota-warning.d.mts +2 -0
  99. package/bin/quota-warning.mjs +6 -0
  100. package/bin/state/graphql-quota-claims.d.mts +2 -0
  101. package/bin/state/graphql-quota-claims.mjs +61 -0
  102. package/bin/state/graphql-quota-policy.d.mts +17 -0
  103. package/bin/state/graphql-quota-policy.mjs +43 -0
  104. package/bin/state/graphql-quota-warnings.d.mts +6 -0
  105. package/bin/state/graphql-quota-warnings.mjs +105 -0
  106. package/bin/state/rest-cache.d.mts +48 -0
  107. package/bin/state/rest-cache.mjs +91 -0
  108. package/bin/types/api-usage.d.mts +31 -0
  109. package/bin/types/api-usage.mjs +1 -0
  110. package/bin/types/escalate.d.mts +5 -3
  111. package/bin/types/iterate.d.mts +4 -1
  112. package/bin/types/report.d.mts +3 -1
  113. package/bin/types.d.mts +1 -0
  114. package/bin/types.mjs +1 -0
  115. package/package.json +2 -2
  116. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  117. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  118. package/plugins/pr-shepherd/.mcp.json +1 -1
  119. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +24 -13
@@ -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.44.1",
4
+ "version": "0.46.0",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
package/README.md CHANGED
@@ -31,8 +31,8 @@ Each tick returns exactly one action:
31
31
 
32
32
  - `WAIT` — no immediate action; continue with the next poll.
33
33
  - `MARK_READY` — the CLI converted an eligible draft PR to ready; continue polling.
34
- - `FIX_CODE` — agent work is required; complete it, then continue polling.
35
- - `MERGE` — run the emitted auto-merge command only when GitHub reports `viewerCanEnableAutoMerge`; queue enrollment otherwise hands off for authorization.
34
+ - `FIX_CODE` — agent work is required; complete it, push when needed, then continue polling. Push access to the PR head branch is a usage precondition.
35
+ - `MERGE` — run the emitted auto-merge command only when GitHub reports `viewerCanEnableAutoMerge`; missing authorization returns `ESCALATE`.
36
36
  - `CANCEL` — stop polling because the PR merged, closed, or completed its ready-delay.
37
37
  - `ESCALATE` — stop polling until a human provides direction.
38
38
 
@@ -69,26 +69,26 @@ Conversations Resolved: No [Not Required]
69
69
  1. Review each item under `## Review threads` and `## Failing checks` and decide whether it needs a code change.
70
70
  2. Apply every warranted review fix in each file referenced above.
71
71
  3. Triage every failure under `## Failing checks`. See "CI failure triage" in the pr-shepherd skill for read-only inspection rules.
72
- 4. If you changed code, commit any remaining changes, then stop and hand off for a push whose authorization is established outside Shepherd; do not run review mutations or iterate until the remote PR head changes. If you did not change code, do not commit and continue.
72
+ 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 again with the same options. If you did not change code, do not commit and continue.
73
73
  5. Run the generated thread IDs unchanged. A latest comment beginning `<!-- pr-shepherd -->` is an earlier Shepherd reply: a marked viewer-authored human thread is emitted resolve-only when authorized, while a marked other-human thread is already acknowledged and has no further mutation.
74
- 6. If you did not change code, replace `$HEAD_SHA` with `$(git rev-parse HEAD)`, which must equal the current remote PR head. After changed code, wait for an authorized push and use its SHA.
74
+ 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.
75
75
  7. Replace `$DISMISS_MESSAGE` with one sentence describing what changed.
76
76
  8. Run the `apply review:` command shown above. See "Review-mutation mechanics" in the pr-shepherd skill for dismiss-ID retention.
77
- 9. `[FIX_CODE]` is conditional: after changed code, stop until an authorized push changes the remote PR head; without code changes, complete the authorized review mutations and iterate again.
77
+ 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 again with the same options; without code changes, complete the authorized review mutations and iterate again.
78
78
  ```
79
79
 
80
- See [docs/actions.md](docs/actions.md) for the complete output contract. Iterate/poll PR outcomes use exit codes `0` and `10`–`15`; command and GitHub failures use `sysexits.h` codes — [docs/exit-codes.md](docs/exit-codes.md).
80
+ 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`–`15`; command and GitHub failures use `sysexits.h` codes — [docs/exit-codes.md](docs/exit-codes.md).
81
81
 
82
82
  ## Workflow Assumptions
83
83
 
84
84
  This system is opinionated and works best with PRs that use required status checks and conversation resolution.
85
85
 
86
86
  - A human inline thread whose original comment has `viewerDidAuthor: true` is replied to and resolved when its latest comment is unmarked. An unmarked other-human inline thread remains reply-only; a marker-ended other-human thread is already acknowledged and receives no further mutation. Human items are never minimized.
87
- - Detected bots and configured `botUsernames` review threads are returned until resolved; bot/non-human threads, PR comments, and review summaries can be resolved or minimized when eligible. Review summaries are not minimized while known inline child threads from that review remain unresolved.
87
+ - Detected bots and configured `botUsernames` review threads are returned until resolved when the required mutation is authorized and the thread has a source location. Unauthorized or unlocated items are surfaced once and then marker-gated until edited. Bot/non-human threads, PR comments, and review summaries can be resolved or minimized when eligible. Review summaries are not minimized while known inline child threads from that review remain unresolved.
88
88
  - Shepherd identifies its own latest reply only when that comment begins `<!-- pr-shepherd -->`, not from author equality. A marked viewer-authored thread can be resolved without another reply as a retry.
89
89
  - Every review thread/comment/review summary is surfaced at least once, even if already outdated, resolved, or minimized; edited items re-surface through seen markers.
90
90
  - Draft PRs can be marked ready automatically when clean; disable with `actions.autoMarkReady: false` or `--no-auto-mark-ready`.
91
- - The CLI never performs git mutations. It may emit local commit guidance, but it does not recommend a push because GitHub viewer fields cannot verify the local Git credential.
91
+ - The CLI never performs git mutations itself it only emits commit/push instructions for the agent to run. Push access to the PR head is a usage precondition; GitHub viewer fields do not create a separate push-authorization handoff.
92
92
  - Every GitHub mutation is permission-aware. Shepherd uses raw viewer capability fields, omits unauthorized commands, and repeats authorization checks in direct `apply` commands. Missing capability data fails closed.
93
93
  - `build_suggestion_patches` turns one or more ordered GitHub suggestion threads into checked patches and commit metadata, but never edits the working tree or git history. Local HEAD may be ahead when the live PR head is its ancestor.
94
94
 
@@ -156,6 +156,17 @@ complete Markdown list item with LF line endings. It fails closed for malformed
156
156
  containers and ignores journal-shaped examples hidden in Markdown constructs. The full journal API,
157
157
  including append and reconciliation helpers, is documented in [docs/api.md](docs/api.md).
158
158
 
159
+ For shell automation that already has a PR body, use the equivalent local-only command:
160
+
161
+ ```sh
162
+ pr-shepherd journal extract --body-file pr-body.md
163
+ ```
164
+
165
+ It writes one JSON line containing the typed extraction result. It never reads GitHub credentials,
166
+ configuration, or Shepherd logs. On POSIX, its final body-file path entry must be a readable regular
167
+ file in a trusted parent directory; symlinks, FIFOs, and devices are rejected with exit code 66.
168
+ Unsupported platforms fail closed with that same exit code.
169
+
159
170
  ### Clean Local State
160
171
 
161
172
  `pr-shepherd` stores seen markers, fix-attempt counters, stall fingerprints, ready-delay markers, and logs under `$PR_SHEPHERD_STATE_DIR` (default `$TMPDIR/pr-shepherd-state`).
@@ -1,4 +1,11 @@
1
- import type { CheckConclusion } from "../types.mts";
1
+ import type { CheckConclusion, ViewerAuthorization } from "../types.mts";
2
+ /**
3
+ * True when the viewer's base-repo role grants GitHub's Actions rerun capability
4
+ * (`actions: write`, which rides with push access). The rerun runs against the base
5
+ * repo where the run lives, so `repositoryPermission` — not `headRepositoryPermission`,
6
+ * which covers fork push access — is the right field to gate on.
7
+ */
8
+ export declare function canRerunWorkflows(auth: ViewerAuthorization | undefined): boolean;
2
9
  /** Failing-check rows for formatter/instructions — excludes annotation-only carriers. */
3
10
  export declare function isFailingAgentCheck(check: {
4
11
  conclusion: CheckConclusion;
@@ -1,4 +1,16 @@
1
1
  const NON_FAILING_CONCLUSIONS = new Set(["SUCCESS", "SKIPPED", "NEUTRAL"]);
2
+ /** Repository roles GitHub grants `actions: write` to — the exact capability a workflow-run rerun needs. */
3
+ const RERUN_CAPABLE_PERMISSIONS = new Set(["WRITE", "MAINTAIN", "ADMIN"]);
4
+ /**
5
+ * True when the viewer's base-repo role grants GitHub's Actions rerun capability
6
+ * (`actions: write`, which rides with push access). The rerun runs against the base
7
+ * repo where the run lives, so `repositoryPermission` — not `headRepositoryPermission`,
8
+ * which covers fork push access — is the right field to gate on.
9
+ */
10
+ export function canRerunWorkflows(auth) {
11
+ const permission = auth?.repositoryPermission;
12
+ return permission != null && RERUN_CAPABLE_PERMISSIONS.has(permission);
13
+ }
2
14
  /** True for conclusions that belong under `## Failing checks` (not success/skipped/neutral). */
3
15
  function isFailingCheckConclusion(conclusion) {
4
16
  return conclusion == null || !NON_FAILING_CONCLUSIONS.has(conclusion);
@@ -1,4 +1,5 @@
1
1
  import type { CheckRun, ClassifiedCheck, TriagedCheck } from "../types.mts";
2
2
  import type { RepoInfo } from "../github/client.mts";
3
- export declare function triageFailingChecks(failingChecks: ClassifiedCheck[], repo: RepoInfo): Promise<TriagedCheck[]>;
4
- export declare function fetchStartupFailureChecks(repo: RepoInfo, headSha: string, prNumber: number): Promise<CheckRun[]>;
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[]>;
@@ -1,24 +1,27 @@
1
1
  /* eslint-disable max-lines */
2
2
  import { restWithRateLimit, restText } from "../github/http.mjs";
3
+ import { loadDerived, storeDerived } from "../state/rest-cache.mjs";
3
4
  const STARTUP_FAILURE_STATUS = "startup_failure";
4
5
  const LOG_EXCERPT_CONTEXT_LINES = 16;
5
6
  const LOG_EXCERPT_TAIL_LINES = 28;
6
7
  const LOG_EXCERPT_MAX_CHARS = 4_000;
7
8
  const TRUNCATED_SUFFIX = "\n[truncated]";
8
9
  const ANSI_SGR_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
9
- export function triageFailingChecks(failingChecks, repo) {
10
+ export function triageFailingChecks(failingChecks, repo, stateKey) {
10
11
  const jobsCache = new Map();
11
- return Promise.all(failingChecks.map((c) => triageCheck(c, repo, jobsCache)));
12
+ return Promise.all(failingChecks.map((c) => triageCheck(c, repo, jobsCache, stateKey)));
12
13
  }
13
- async function triageCheck(check, repo, jobsCache) {
14
+ async function triageCheck(check, repo, jobsCache, stateKey) {
14
15
  if (check.runId === null ||
15
16
  check.conclusion === "CANCELLED" ||
16
17
  check.conclusion === "STARTUP_FAILURE") {
17
18
  return { ...check };
18
19
  }
19
- const jobs = await fetchJobs(check.runId, repo, jobsCache);
20
+ const jobs = await fetchJobs(check.runId, repo, jobsCache, stateKey);
20
21
  const jobInfo = jobs ? pickJobInfo(jobs, check.name) : undefined;
21
- const logExcerpt = jobInfo?.jobId ? await fetchJobLogExcerpt(jobInfo.jobId, repo) : undefined;
22
+ const logExcerpt = jobInfo?.jobId
23
+ ? await fetchJobLogExcerpt(jobInfo.jobId, repo, stateKey, jobInfo.jobConclusion != null)
24
+ : undefined;
22
25
  return {
23
26
  ...check,
24
27
  ...(jobInfo?.workflowName !== undefined && { workflowName: jobInfo.workflowName }),
@@ -27,9 +30,9 @@ async function triageCheck(check, repo, jobsCache) {
27
30
  ...(logExcerpt !== undefined && { logExcerpt }),
28
31
  };
29
32
  }
30
- export async function fetchStartupFailureChecks(repo, headSha, prNumber) {
33
+ export async function fetchStartupFailureChecks(repo, headSha, prNumber, stateKey) {
31
34
  try {
32
- return await fetchStartupFailureChecksUncached(repo, headSha, prNumber);
35
+ return await fetchStartupFailureChecksUncached(repo, headSha, prNumber, stateKey);
33
36
  }
34
37
  catch (err) {
35
38
  const msg = err instanceof Error ? err.message : String(err);
@@ -37,13 +40,21 @@ export async function fetchStartupFailureChecks(repo, headSha, prNumber) {
37
40
  return [];
38
41
  }
39
42
  }
40
- async function fetchStartupFailureChecksUncached(repo, headSha, prNumber) {
43
+ async function fetchStartupFailureChecksUncached(repo, headSha, prNumber, stateKey) {
41
44
  const { owner, name } = repo;
42
45
  const perPage = 100;
43
46
  const MAX_RUN_PAGES = 10;
44
47
  const checks = [];
45
48
  for (let page = 1; page <= MAX_RUN_PAGES; page++) {
46
- const { data, rateLimit } = await restWithRateLimit("GET", `/repos/${owner}/${name}/actions/runs?head_sha=${encodeURIComponent(headSha)}&status=${STARTUP_FAILURE_STATUS}&per_page=${perPage}&page=${page}`);
49
+ 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
50
+ ? {
51
+ conditional: {
52
+ key: stateKey,
53
+ name: `runs-startupfailure-${headSha}-p${page}`,
54
+ headSha,
55
+ },
56
+ }
57
+ : undefined);
47
58
  checks.push(...data.workflow_runs
48
59
  .filter((run) => runBelongsToPr(run, prNumber, headSha))
49
60
  .map(workflowRunToCheckRun));
@@ -75,15 +86,15 @@ function workflowRunToCheckRun(run) {
75
86
  function runBelongsToPr(run, prNumber, headSha) {
76
87
  return (run.pull_requests ?? []).some((pr) => pr.number === prNumber && (pr.head?.sha ?? headSha) === headSha);
77
88
  }
78
- function fetchJobs(runId, repo, cache) {
89
+ function fetchJobs(runId, repo, cache, stateKey) {
79
90
  const cached = cache.get(runId);
80
91
  if (cached)
81
92
  return cached;
82
- const promise = fetchJobsUncached(runId, repo);
93
+ const promise = fetchJobsUncached(runId, repo, stateKey);
83
94
  cache.set(runId, promise);
84
95
  return promise;
85
96
  }
86
- async function fetchJobsUncached(runId, repo) {
97
+ async function fetchJobsUncached(runId, repo, stateKey) {
87
98
  const { owner, name } = repo;
88
99
  const perPage = 100;
89
100
  const MAX_JOB_PAGES = 20; // 2000 jobs max
@@ -95,7 +106,9 @@ async function fetchJobsUncached(runId, repo) {
95
106
  process.stderr.write(`pr-shepherd: job pagination cap (${MAX_JOB_PAGES * 100} jobs) reached for run ${runId} — triage may be incomplete\n`);
96
107
  break;
97
108
  }
98
- const { data, rateLimit } = await restWithRateLimit("GET", `/repos/${owner}/${name}/actions/runs/${runId}/jobs?filter=latest&per_page=${perPage}&page=${page}`);
109
+ const { data, rateLimit } = await restWithRateLimit("GET", `/repos/${owner}/${name}/actions/runs/${runId}/jobs?filter=latest&per_page=${perPage}&page=${page}`, undefined, stateKey
110
+ ? { conditional: { key: stateKey, name: `jobs-run-${runId}-p${page}` } }
111
+ : undefined);
99
112
  allJobs.push(...data.jobs);
100
113
  if (rateLimit?.remaining === 0) {
101
114
  process.stderr.write(`pr-shepherd: REST rate limit remaining is 0 while listing jobs for run ${runId} — triage may be incomplete\n`);
@@ -127,12 +140,28 @@ function pickJobInfo(jobs, checkName) {
127
140
  workflowName: job.workflow_name,
128
141
  jobName: job.name,
129
142
  failedStep,
143
+ jobConclusion: job.conclusion,
130
144
  };
131
145
  }
132
- async function fetchJobLogExcerpt(jobId, repo) {
146
+ /**
147
+ * `cacheable` gates the cross-tick cache — only set once the matched job has
148
+ * a terminal conclusion, so an in-progress job's (possibly partial) log
149
+ * never gets frozen into the cache.
150
+ */
151
+ async function fetchJobLogExcerpt(jobId, repo, stateKey, cacheable = false) {
152
+ const cacheName = `joblog-${jobId}`;
153
+ if (stateKey && cacheable) {
154
+ const cached = await loadDerived(stateKey, cacheName);
155
+ if (cached)
156
+ return cached.value ?? undefined;
157
+ }
133
158
  const { owner, name } = repo;
134
159
  try {
135
- return buildLogExcerpt(await restText(`/repos/${owner}/${name}/actions/jobs/${jobId}/logs`));
160
+ const excerpt = buildLogExcerpt(await restText(`/repos/${owner}/${name}/actions/jobs/${jobId}/logs`));
161
+ if (stateKey && cacheable) {
162
+ await storeDerived(stateKey, cacheName, excerpt ?? null);
163
+ }
164
+ return excerpt;
136
165
  }
137
166
  catch {
138
167
  return undefined;
@@ -0,0 +1,3 @@
1
+ import type { ApiUsage, GraphqlQuotaWarning } from "../types.mts";
2
+ export declare function formatQuotaWarning(warning: GraphqlQuotaWarning | undefined): string | null;
3
+ export declare function formatApiUsage(usage: ApiUsage | undefined): string | null;
@@ -0,0 +1,36 @@
1
+ function resetTime(resetAt) {
2
+ return new Date(resetAt * 1000).toISOString();
3
+ }
4
+ function formatResource(resource) {
5
+ const used = resource.used === undefined ? "" : ` · used ${resource.used}`;
6
+ return `- \`${resource.resource}\`: ${resource.remaining}/${resource.limit} remaining${used} · ${resource.requestCount} requests · resets ${resetTime(resource.resetAt)}`;
7
+ }
8
+ export function formatQuotaWarning(warning) {
9
+ if (warning === undefined)
10
+ return null;
11
+ const used = warning.used === undefined ? "" : ` · used ${warning.used}`;
12
+ return [
13
+ "## GitHub API quota warning",
14
+ "",
15
+ `- Resource: \`${warning.resource}\``,
16
+ `- Remaining: ${warning.remaining}/${warning.limit}${used}`,
17
+ `- Crossed threshold: ${warning.thresholdPercent}% remaining`,
18
+ `- Reset: ${resetTime(warning.resetAt)}`,
19
+ `- Recommended poll interval: ${warning.pollIntervalMinutes} minutes`,
20
+ `- Recommended bounded CLI timeout: ${warning.pollTimeoutMinutes} minutes`,
21
+ "- Recommendation: prefer non-GraphQL `gh` CLI commands for PR operations until the reset above, then resume pr-shepherd",
22
+ ].join("\n");
23
+ }
24
+ export function formatApiUsage(usage) {
25
+ if (usage === undefined)
26
+ return null;
27
+ const lines = ["## GitHub API usage", ""];
28
+ lines.push(`- Credential source: ${usage.credentialSources.map((source) => `\`${source}\``).join(", ")}`);
29
+ if (usage.graphql !== undefined) {
30
+ lines.push(formatResource(usage.graphql));
31
+ lines.push(`- GraphQL measured cost: ${usage.graphql.measuredQueryCost} · unmeasured requests: ${usage.graphql.unmeasuredRequestCount} · nodes: ${usage.graphql.nodeCount}`);
32
+ }
33
+ for (const resource of usage.rest ?? [])
34
+ lines.push(formatResource(resource));
35
+ return lines.join("\n");
36
+ }
@@ -0,0 +1,13 @@
1
+ /** Safe GitHub transport metadata suitable for user-facing structured error output. */
2
+ export declare function serializeGitHubRequestErrorDetails(err: unknown): {
3
+ rateLimit?: {
4
+ resource: string;
5
+ limit: number;
6
+ remaining: number;
7
+ used?: number | undefined;
8
+ resetAt: number;
9
+ } | undefined;
10
+ retryAfterSeconds?: number | undefined;
11
+ credential?: string | undefined;
12
+ } | undefined;
13
+ export declare function formatCliError(err: unknown): string;
@@ -0,0 +1,41 @@
1
+ import { GitHubRequestError } from "../github/errors.mjs";
2
+ /** Safe GitHub transport metadata suitable for user-facing structured error output. */
3
+ export function serializeGitHubRequestErrorDetails(err) {
4
+ if (!(err instanceof GitHubRequestError))
5
+ return undefined;
6
+ return {
7
+ ...(err.rateLimit !== undefined && {
8
+ rateLimit: {
9
+ resource: err.rateLimit.resource ?? "unknown",
10
+ limit: err.rateLimit.limit,
11
+ remaining: err.rateLimit.remaining,
12
+ ...(err.rateLimit.used !== undefined && { used: err.rateLimit.used }),
13
+ resetAt: err.rateLimit.resetAt,
14
+ },
15
+ }),
16
+ ...(err.retryAfterSeconds !== undefined && { retryAfterSeconds: err.retryAfterSeconds }),
17
+ ...(err.authSource !== undefined && { credential: err.authSource }),
18
+ };
19
+ }
20
+ export function formatCliError(err) {
21
+ const message = err instanceof Error ? err.message : String(err);
22
+ const githubDetails = serializeGitHubRequestErrorDetails(err);
23
+ if (githubDetails === undefined)
24
+ return message;
25
+ const details = [];
26
+ if (githubDetails.rateLimit !== undefined) {
27
+ details.push(`resource ${githubDetails.rateLimit.resource}`);
28
+ details.push(`remaining ${githubDetails.rateLimit.remaining}/${githubDetails.rateLimit.limit}`);
29
+ if (githubDetails.rateLimit.used !== undefined) {
30
+ details.push(`used ${githubDetails.rateLimit.used}`);
31
+ }
32
+ details.push(`reset ${new Date(githubDetails.rateLimit.resetAt * 1000).toISOString()}`);
33
+ }
34
+ if (githubDetails.retryAfterSeconds !== undefined) {
35
+ details.push(`retry after ${githubDetails.retryAfterSeconds}s`);
36
+ }
37
+ if (githubDetails.credential !== undefined) {
38
+ details.push(`credential ${githubDetails.credential}`);
39
+ }
40
+ return details.length === 0 ? message : `${message} (${details.join("; ")})`;
41
+ }
@@ -9,9 +9,11 @@ import { isFailingAgentCheck } from "../checks/conclusions.mjs";
9
9
  import { renderMergeCommand } from "../commands/iterate/merge.mjs";
10
10
  export function formatFixCodeResult(header, result) {
11
11
  const sections = [header];
12
- if (result.fix.threads.length > 0) {
13
- sections.push("## Review threads");
14
- for (const t of result.fix.threads) {
12
+ const renderThreads = (heading, threads) => {
13
+ if (threads.length === 0)
14
+ return;
15
+ sections.push(heading);
16
+ for (const t of threads) {
15
17
  const lineLabel = renderLineRange(t.startLine, t.line);
16
18
  const loc = t.path ? `\`${t.path}:${lineLabel}\`` : "(no location)";
17
19
  const heading = t.url ? `[threadId=${t.id}](${t.url})` : `\`threadId=${t.id}\``;
@@ -24,7 +26,11 @@ export function formatFixCodeResult(header, result) {
24
26
  sections.push(renderSuggestionBlock(t.suggestion, ""));
25
27
  }
26
28
  }
27
- }
29
+ };
30
+ const locatedThreads = result.fix.threads.filter((thread) => thread.path !== null && thread.line !== null);
31
+ const unlocatedThreads = result.fix.threads.filter((thread) => thread.path === null || thread.line === null);
32
+ renderThreads("## Review threads", locatedThreads);
33
+ renderThreads("## Unlocated review threads (logged once — no mutation)", unlocatedThreads);
28
34
  if (result.fix.resolutionOnlyThreads.length > 0) {
29
35
  sections.push("## Review threads to resolve");
30
36
  sections.push(result.fix.resolutionOnlyThreads
@@ -44,6 +50,10 @@ export function formatFixCodeResult(header, result) {
44
50
  const failingChecks = result.fix.checks.filter((ch) => isFailingAgentCheck(ch));
45
51
  if (failingChecks.length > 0) {
46
52
  sections.push("## Failing checks");
53
+ // Multiple failing CheckRuns (matrix jobs, etc.) can share one runId; the rerun command is
54
+ // identical for all of them, so print it once — after the first bullet for that runId —
55
+ // instead of repeating the same shell command on every row.
56
+ const seenRerunRunIds = new Set();
47
57
  const bullets = failingChecks.map((ch) => {
48
58
  const workflowPrefix = ch.workflowName ? `${ch.workflowName} › ` : "";
49
59
  const jobLabel = ch.jobName ? ch.jobName : ch.name;
@@ -56,7 +66,10 @@ export function formatFixCodeResult(header, result) {
56
66
  const scopeTag = ch.scope
57
67
  ? ` [scope: ${ch.scope}${ch.commitOid ? `, commit: ${ch.commitOid}` : ""}]`
58
68
  : "";
59
- const lines = [`- ${locator} \`${workflowPrefix}${jobLabel}\`${conclusionTag}${scopeTag}`];
69
+ const rerunTag = ch.rerunCommand ? " [rerun authorized]" : "";
70
+ const lines = [
71
+ `- ${locator} — \`${workflowPrefix}${jobLabel}\`${conclusionTag}${scopeTag}${rerunTag}`,
72
+ ];
60
73
  if (ch.conclusion !== "CANCELLED") {
61
74
  if (ch.failedStep)
62
75
  lines.push(` > ${ch.failedStep}`);
@@ -65,6 +78,10 @@ export function formatFixCodeResult(header, result) {
65
78
  if (ch.logExcerpt)
66
79
  lines.push(indentBlockquote(ch.logExcerpt, " "));
67
80
  }
81
+ if (ch.rerunCommand && ch.runId && !seenRerunRunIds.has(ch.runId)) {
82
+ seenRerunRunIds.add(ch.runId);
83
+ lines.push(` rerun: \`${ch.rerunCommand}\``);
84
+ }
68
85
  return lines.join("\n");
69
86
  });
70
87
  sections.push(bullets.join("\n\n"));
@@ -246,5 +246,18 @@ Flags:
246
246
  --help, -h Print this help and exit before any GitHub I/O.
247
247
 
248
248
  Exit code: 0 on success (including no-change no-op); nonzero on failure (sysexits.h — see docs/exit-codes.md).`;
249
+ readonly "journal extract": `pr-shepherd journal extract
250
+
251
+ Extract one visible, structurally valid Shepherd Journal from a local PR-body file.
252
+ This command performs no GitHub, configuration, or log I/O.
253
+
254
+ Usage:
255
+ pr-shepherd journal extract --body-file <path>
256
+
257
+ The output is exactly one JSON line: the typed result from extractShepherdJournal.
258
+ Malformed or unrecognized journal content is returned as {"ok":false,...} with exit code 0.
259
+ On POSIX, the final body-file path entry must be a readable regular file in a trusted parent directory;
260
+ symlinks, FIFOs, devices, and unreadable paths exit 66. Unsupported platforms fail closed with exit 66.
261
+ --help, -h Print this help and exit before any I/O.`;
249
262
  readonly "log-file": "pr-shepherd log-file\n\nPrint the per-worktree append-only debug log path for the current repository.\nThe log is created by the first non-help pr-shepherd command that initializes logging.\n\nUsage:\n pr-shepherd log-file [--format text|json]\n\nFlags:\n --format text|json Print a raw path or {\"path\": \"...\"} JSON. Default: text.\n --help, -h Print this help and exit before logging setup.\n\nEnvironment:\n PR_SHEPHERD_LOG_DISABLED=1 disables logging.\n PR_SHEPHERD_STATE_DIR overrides the base state directory.\n\nExit code: 0 on success; 1 if repository identity cannot be resolved.";
250
263
  };
@@ -249,5 +249,18 @@ Flags:
249
249
  --help, -h Print this help and exit before any GitHub I/O.
250
250
 
251
251
  Exit code: 0 on success (including no-change no-op); nonzero on failure (sysexits.h — see docs/exit-codes.md).`,
252
+ "journal extract": `pr-shepherd journal extract
253
+
254
+ Extract one visible, structurally valid Shepherd Journal from a local PR-body file.
255
+ This command performs no GitHub, configuration, or log I/O.
256
+
257
+ Usage:
258
+ pr-shepherd journal extract --body-file <path>
259
+
260
+ The output is exactly one JSON line: the typed result from extractShepherdJournal.
261
+ Malformed or unrecognized journal content is returned as {"ok":false,...} with exit code 0.
262
+ On POSIX, the final body-file path entry must be a readable regular file in a trusted parent directory;
263
+ symlinks, FIFOs, devices, and unreadable paths exit 66. Unsupported platforms fail closed with exit 66.
264
+ --help, -h Print this help and exit before any I/O.`,
252
265
  "log-file": LOG_FILE_USAGE,
253
266
  };
@@ -1 +1 @@
1
- export declare const TOP_USAGE = "pr-shepherd\n\nAutonomous PR CI monitor and review-comment resolver for agentic coding tools.\n\nUsage:\n pr-shepherd --version | -v\n pr-shepherd --help | -h\n pr-shepherd [PR] [poll-flags] [iterate-flags]\n pr-shepherd iterate [PR] [iterate-flags]\n pr-shepherd apply review [PR] [review-flags]\n pr-shepherd apply files [PR] [files...] [--tests] [--match REGEX]\n pr-shepherd apply journal [PR] <item> [--dry-run] [--format text|json]\n pr-shepherd build-suggestion-patches [PR] --thread-id ID --message MSG [groups...]\n pr-shepherd admin clean <pr|branch|current|repo|all> [value] [flags]\n pr-shepherd admin log-file [--format text|json]\n\nCommands:\n [PR] Poll until non-WAIT or timeout. This is the default command.\n iterate Run one iterate tick (single-tick alias).\n apply review Apply review-state mutations after fixes.\n apply files Select changed files; no viewed-state mutation is attempted.\n apply journal Append a list item to the Shepherd Journal details block of a PR body.\n build-suggestion-patches\n Convert ordered GitHub suggestion threads into patches and commit instructions.\n admin clean Remove pr-shepherd state files.\n admin log-file Print the per-worktree debug log path.\n\nPR argument:\n PR may be a number such as 42 or a GitHub pull request URL.\n When omitted, pr-shepherd infers the current branch's pull request.\n\nCommon flags:\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed poll-tick lines.\n --help, -h Print help and exit before any GitHub, git, config, or log I/O.\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\nPolling flags:\n --interval <duration> Delay between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Poll wall-clock cap for WAIT ticks. Bare number = seconds. Default: 4.5m.\n --debounce <duration> Settle window after first FIX_CODE before returning. Bare number = seconds. Default: 60s. 0 disables.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nClean variants:\n pr [number] Remove state for one PR. Defaults to current branch PR.\n branch [name] Remove state for a branch's PR. Defaults to current branch.\n current Alias for branch against the current branch.\n repo Remove all state for the current repository.\n all Remove all pr-shepherd state.\n\nExit codes: 0 done, 10-19 PR state, 64-78 shepherd failed (sysexits.h).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\nSee docs/exit-codes.md for the full sysexits.h error-code table.\n\nDuration examples: 30s, 4.5m, 1h. A bare number uses each flag's default unit (see above); decimals are allowed with an explicit unit (4.5m).\n\nRun 'pr-shepherd <command> --help' for command-specific details.";
1
+ export declare const TOP_USAGE = "pr-shepherd\n\nAutonomous PR CI monitor and review-comment resolver for agentic coding tools.\n\nUsage:\n pr-shepherd --version | -v\n pr-shepherd --help | -h\n pr-shepherd [PR] [poll-flags] [iterate-flags]\n pr-shepherd iterate [PR] [iterate-flags]\n pr-shepherd apply review [PR] [review-flags]\n pr-shepherd apply files [PR] [files...] [--tests] [--match REGEX]\n pr-shepherd apply journal [PR] <item> [--dry-run] [--format text|json]\n pr-shepherd journal extract --body-file <path>\n pr-shepherd build-suggestion-patches [PR] --thread-id ID --message MSG [groups...]\n pr-shepherd admin clean <pr|branch|current|repo|all> [value] [flags]\n pr-shepherd admin log-file [--format text|json]\n\nCommands:\n [PR] Poll until non-WAIT or timeout. This is the default command.\n iterate Run one iterate tick (single-tick alias).\n apply review Apply review-state mutations after fixes.\n apply files Select changed files; no viewed-state mutation is attempted.\n apply journal Append a list item to the Shepherd Journal details block of a PR body.\n journal extract Extract a validated Shepherd Journal from a local PR-body file as JSON.\n build-suggestion-patches\n Convert ordered GitHub suggestion threads into patches and commit instructions.\n admin clean Remove pr-shepherd state files.\n admin log-file Print the per-worktree debug log path.\n\nPR argument:\n PR may be a number such as 42 or a GitHub pull request URL.\n When omitted, pr-shepherd infers the current branch's pull request.\n\nCommon flags:\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed poll-tick lines.\n --help, -h Print help and exit before any GitHub, git, config, or log I/O.\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\nPolling flags:\n --interval <duration> Delay between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Poll wall-clock cap for WAIT ticks. Bare number = seconds. Default: 4.5m.\n --debounce <duration> Settle window after first FIX_CODE before returning. Bare number = seconds. Default: 60s. 0 disables.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nClean variants:\n pr [number] Remove state for one PR. Defaults to current branch PR.\n branch [name] Remove state for a branch's PR. Defaults to current branch.\n current Alias for branch against the current branch.\n repo Remove all state for the current repository.\n all Remove all pr-shepherd state.\n\nExit codes: 0 done, 10-19 PR state, 64-78 shepherd failed (sysexits.h).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\nSee docs/exit-codes.md for the full sysexits.h error-code table.\n\nDuration examples: 30s, 4.5m, 1h. A bare number uses each flag's default unit (see above); decimals are allowed with an explicit unit (4.5m).\n\nRun 'pr-shepherd <command> --help' for command-specific details.";
@@ -10,6 +10,7 @@ Usage:
10
10
  pr-shepherd apply review [PR] [review-flags]
11
11
  pr-shepherd apply files [PR] [files...] [--tests] [--match REGEX]
12
12
  pr-shepherd apply journal [PR] <item> [--dry-run] [--format text|json]
13
+ pr-shepherd journal extract --body-file <path>
13
14
  pr-shepherd build-suggestion-patches [PR] --thread-id ID --message MSG [groups...]
14
15
  pr-shepherd admin clean <pr|branch|current|repo|all> [value] [flags]
15
16
  pr-shepherd admin log-file [--format text|json]
@@ -20,6 +21,7 @@ Commands:
20
21
  apply review Apply review-state mutations after fixes.
21
22
  apply files Select changed files; no viewed-state mutation is attempted.
22
23
  apply journal Append a list item to the Shepherd Journal details block of a PR body.
24
+ journal extract Extract a validated Shepherd Journal from a local PR-body file as JSON.
23
25
  build-suggestion-patches
24
26
  Convert ordered GitHub suggestion threads into patches and commit instructions.
25
27
  admin clean Remove pr-shepherd state files.
@@ -246,8 +246,21 @@ Flags:
246
246
  --help, -h Print this help and exit before any GitHub I/O.
247
247
 
248
248
  Exit code: 0 on success (including no-change no-op); nonzero on failure (sysexits.h — see docs/exit-codes.md).`;
249
+ readonly "journal extract": `pr-shepherd journal extract
250
+
251
+ Extract one visible, structurally valid Shepherd Journal from a local PR-body file.
252
+ This command performs no GitHub, configuration, or log I/O.
253
+
254
+ Usage:
255
+ pr-shepherd journal extract --body-file <path>
256
+
257
+ The output is exactly one JSON line: the typed result from extractShepherdJournal.
258
+ Malformed or unrecognized journal content is returned as {"ok":false,...} with exit code 0.
259
+ On POSIX, the final body-file path entry must be a readable regular file in a trusted parent directory;
260
+ symlinks, FIFOs, devices, and unreadable paths exit 66. Unsupported platforms fail closed with exit 66.
261
+ --help, -h Print this help and exit before any I/O.`;
249
262
  readonly "log-file": "pr-shepherd log-file\n\nPrint the per-worktree append-only debug log path for the current repository.\nThe log is created by the first non-help pr-shepherd command that initializes logging.\n\nUsage:\n pr-shepherd log-file [--format text|json]\n\nFlags:\n --format text|json Print a raw path or {\"path\": \"...\"} JSON. Default: text.\n --help, -h Print this help and exit before logging setup.\n\nEnvironment:\n PR_SHEPHERD_LOG_DISABLED=1 disables logging.\n PR_SHEPHERD_STATE_DIR overrides the base state directory.\n\nExit code: 0 on success; 1 if repository identity cannot be resolved.";
250
- readonly top: "pr-shepherd\n\nAutonomous PR CI monitor and review-comment resolver for agentic coding tools.\n\nUsage:\n pr-shepherd --version | -v\n pr-shepherd --help | -h\n pr-shepherd [PR] [poll-flags] [iterate-flags]\n pr-shepherd iterate [PR] [iterate-flags]\n pr-shepherd apply review [PR] [review-flags]\n pr-shepherd apply files [PR] [files...] [--tests] [--match REGEX]\n pr-shepherd apply journal [PR] <item> [--dry-run] [--format text|json]\n pr-shepherd build-suggestion-patches [PR] --thread-id ID --message MSG [groups...]\n pr-shepherd admin clean <pr|branch|current|repo|all> [value] [flags]\n pr-shepherd admin log-file [--format text|json]\n\nCommands:\n [PR] Poll until non-WAIT or timeout. This is the default command.\n iterate Run one iterate tick (single-tick alias).\n apply review Apply review-state mutations after fixes.\n apply files Select changed files; no viewed-state mutation is attempted.\n apply journal Append a list item to the Shepherd Journal details block of a PR body.\n build-suggestion-patches\n Convert ordered GitHub suggestion threads into patches and commit instructions.\n admin clean Remove pr-shepherd state files.\n admin log-file Print the per-worktree debug log path.\n\nPR argument:\n PR may be a number such as 42 or a GitHub pull request URL.\n When omitted, pr-shepherd infers the current branch's pull request.\n\nCommon flags:\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed poll-tick lines.\n --help, -h Print help and exit before any GitHub, git, config, or log I/O.\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\nPolling flags:\n --interval <duration> Delay between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Poll wall-clock cap for WAIT ticks. Bare number = seconds. Default: 4.5m.\n --debounce <duration> Settle window after first FIX_CODE before returning. Bare number = seconds. Default: 60s. 0 disables.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nClean variants:\n pr [number] Remove state for one PR. Defaults to current branch PR.\n branch [name] Remove state for a branch's PR. Defaults to current branch.\n current Alias for branch against the current branch.\n repo Remove all state for the current repository.\n all Remove all pr-shepherd state.\n\nExit codes: 0 done, 10-19 PR state, 64-78 shepherd failed (sysexits.h).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\nSee docs/exit-codes.md for the full sysexits.h error-code table.\n\nDuration examples: 30s, 4.5m, 1h. A bare number uses each flag's default unit (see above); decimals are allowed with an explicit unit (4.5m).\n\nRun 'pr-shepherd <command> --help' for command-specific details.";
263
+ readonly top: "pr-shepherd\n\nAutonomous PR CI monitor and review-comment resolver for agentic coding tools.\n\nUsage:\n pr-shepherd --version | -v\n pr-shepherd --help | -h\n pr-shepherd [PR] [poll-flags] [iterate-flags]\n pr-shepherd iterate [PR] [iterate-flags]\n pr-shepherd apply review [PR] [review-flags]\n pr-shepherd apply files [PR] [files...] [--tests] [--match REGEX]\n pr-shepherd apply journal [PR] <item> [--dry-run] [--format text|json]\n pr-shepherd journal extract --body-file <path>\n pr-shepherd build-suggestion-patches [PR] --thread-id ID --message MSG [groups...]\n pr-shepherd admin clean <pr|branch|current|repo|all> [value] [flags]\n pr-shepherd admin log-file [--format text|json]\n\nCommands:\n [PR] Poll until non-WAIT or timeout. This is the default command.\n iterate Run one iterate tick (single-tick alias).\n apply review Apply review-state mutations after fixes.\n apply files Select changed files; no viewed-state mutation is attempted.\n apply journal Append a list item to the Shepherd Journal details block of a PR body.\n journal extract Extract a validated Shepherd Journal from a local PR-body file as JSON.\n build-suggestion-patches\n Convert ordered GitHub suggestion threads into patches and commit instructions.\n admin clean Remove pr-shepherd state files.\n admin log-file Print the per-worktree debug log path.\n\nPR argument:\n PR may be a number such as 42 or a GitHub pull request URL.\n When omitted, pr-shepherd infers the current branch's pull request.\n\nCommon flags:\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed poll-tick lines.\n --help, -h Print help and exit before any GitHub, git, config, or log I/O.\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\nPolling flags:\n --interval <duration> Delay between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Poll wall-clock cap for WAIT ticks. Bare number = seconds. Default: 4.5m.\n --debounce <duration> Settle window after first FIX_CODE before returning. Bare number = seconds. Default: 60s. 0 disables.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nClean variants:\n pr [number] Remove state for one PR. Defaults to current branch PR.\n branch [name] Remove state for a branch's PR. Defaults to current branch.\n current Alias for branch against the current branch.\n repo Remove all state for the current repository.\n all Remove all pr-shepherd state.\n\nExit codes: 0 done, 10-19 PR state, 64-78 shepherd failed (sysexits.h).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\nSee docs/exit-codes.md for the full sysexits.h error-code table.\n\nDuration examples: 30s, 4.5m, 1h. A bare number uses each flag's default unit (see above); decimals are allowed with an explicit unit (4.5m).\n\nRun 'pr-shepherd <command> --help' for command-specific details.";
251
264
  };
252
265
  /** Resolve help keys for nested public commands before any command I/O. */
253
266
  export declare function helpKeyForArgs(args: string[]): keyof typeof USAGE;
package/bin/cli/help.mjs CHANGED
@@ -13,6 +13,8 @@ export function helpKeyForArgs(args) {
13
13
  return "apply files";
14
14
  if (args[0] === "apply" && args[1] === "journal")
15
15
  return "apply journal";
16
+ if (args[0] === "journal" && args[1] === "extract")
17
+ return "journal extract";
16
18
  if (args[0] === "admin" && args[1] === "clean")
17
19
  return "admin clean";
18
20
  if (args[0] === "admin" && args[1] === "log-file")
@@ -0,0 +1,2 @@
1
+ import type { IterateResult } from "../types.mts";
2
+ export declare function formatActivityLine(result: IterateResult): string | null;
@@ -0,0 +1,26 @@
1
+ export function formatActivityLine(result) {
2
+ const activity = result.activity ?? {
3
+ commitCount: 0,
4
+ reviewRoundCount: 0,
5
+ latestCommitCommittedAtUnix: null,
6
+ reviewItemsSinceLatestCommit: [],
7
+ };
8
+ const hasActiveChecks = (result.inProgressChecks?.length ?? 0) > 0;
9
+ if (activity.commitCount === 0 &&
10
+ activity.reviewRoundCount === 0 &&
11
+ activity.reviewItemsSinceLatestCommit.length === 0 &&
12
+ !hasActiveChecks) {
13
+ return null;
14
+ }
15
+ const parts = [`${activity.commitCount} commits`, `${activity.reviewRoundCount} review rounds`];
16
+ if (activity.reviewItemsSinceLatestCommit.length > 0) {
17
+ parts.push(`${activity.reviewItemsSinceLatestCommit.length} review items since latest commit`);
18
+ }
19
+ if (hasActiveChecks) {
20
+ parts.push(`active: ${result
21
+ .inProgressChecks.slice(0, 5)
22
+ .map((check) => `\`${check.name}\``)
23
+ .join(", ")}`);
24
+ }
25
+ return `**activity** ${parts.join(" · ")}`;
26
+ }
@@ -0,0 +1,2 @@
1
+ import type { RelevantCheck } from "../types.mts";
2
+ export declare function formatRelevantChecks(checks: RelevantCheck[]): string | null;
@@ -0,0 +1,55 @@
1
+ import { renderCheckAnnotation } from "./fix-formatter-extra.mjs";
2
+ export function formatRelevantChecks(checks) {
3
+ if (checks.length === 0)
4
+ return null;
5
+ const lines = ["## Checks", ""];
6
+ for (const check of checks) {
7
+ lines.push(...formatRelevantCheck(check));
8
+ }
9
+ return lines.join("\n");
10
+ }
11
+ function formatRelevantCheck(check) {
12
+ const workflow = check.workflowName ? `${check.workflowName} › ` : "";
13
+ const job = check.jobName ?? check.name;
14
+ const lines = [`- \`${workflow}${job}\` [conclusion: ${check.conclusion}]`];
15
+ appendCheckFields(lines, check);
16
+ appendLogExcerpt(lines, check.logExcerpt);
17
+ appendAnnotations(lines, check.annotations);
18
+ return lines;
19
+ }
20
+ function appendCheckFields(lines, check) {
21
+ const codeFields = [
22
+ ["run", check.runId],
23
+ ["URL", check.detailsUrl],
24
+ ["scope", check.scope],
25
+ ["commit", check.commitOid],
26
+ ];
27
+ for (const [label, value] of codeFields) {
28
+ if (value)
29
+ lines.push(` - ${label}: \`${value}\``);
30
+ }
31
+ const textFields = [
32
+ ["failed step", check.failedStep],
33
+ ["summary", check.summary],
34
+ ];
35
+ for (const [label, value] of textFields) {
36
+ if (value)
37
+ lines.push(` - ${label}: ${value}`);
38
+ }
39
+ }
40
+ function appendLogExcerpt(lines, logExcerpt) {
41
+ if (!logExcerpt)
42
+ return;
43
+ for (const line of logExcerpt.split("\n"))
44
+ lines.push(` > ${line}`);
45
+ }
46
+ function appendAnnotations(lines, annotations) {
47
+ if (!annotations || annotations.length === 0)
48
+ return;
49
+ lines.push(" - annotations:");
50
+ for (const annotation of annotations) {
51
+ for (const line of renderCheckAnnotation(annotation).split("\n")) {
52
+ lines.push(` ${line}`);
53
+ }
54
+ }
55
+ }