pr-shepherd 0.33.0 → 0.35.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 (198) hide show
  1. package/.claude-plugin/plugin.json +3 -2
  2. package/.grok-plugin/marketplace.json +17 -0
  3. package/README.md +63 -41
  4. package/bin/api.d.mts +80 -0
  5. package/bin/api.mjs +236 -0
  6. package/bin/checks/classify.d.mts +41 -0
  7. package/bin/checks/startup-failures.d.mts +2 -0
  8. package/bin/checks/superseded.d.mts +21 -0
  9. package/bin/checks/triage.d.mts +4 -0
  10. package/bin/classify/apply.d.mts +18 -0
  11. package/bin/classify/apply.mjs +4 -0
  12. package/bin/classify/loader.d.mts +10 -0
  13. package/bin/classify/types.d.mts +35 -0
  14. package/bin/cli/args.d.mts +18 -0
  15. package/bin/cli/clean-formatter.d.mts +2 -0
  16. package/bin/cli/default-poll.d.mts +2 -0
  17. package/bin/cli/default-poll.mjs +2 -1
  18. package/bin/cli/duration-flag.d.mts +2 -0
  19. package/bin/cli/duration-flag.mjs +5 -4
  20. package/bin/cli/duration.d.mts +13 -0
  21. package/bin/cli/{exit-codes.mjs → duration.mjs} +0 -26
  22. package/bin/cli/fence.d.mts +1 -0
  23. package/bin/cli/fix-formatter-extra.d.mts +3 -0
  24. package/bin/cli/fix-formatter.d.mts +2 -0
  25. package/bin/cli/fix-formatter.mjs +6 -6
  26. package/bin/cli/formatters.d.mts +7 -0
  27. package/bin/cli/handlers.d.mts +4 -0
  28. package/bin/cli/handlers.mjs +20 -18
  29. package/bin/cli/help-command-pages.d.mts +231 -0
  30. package/bin/cli/help-command-pages.mjs +116 -72
  31. package/bin/cli/help-iterate-poll-pages.d.mts +4 -0
  32. package/bin/cli/help-iterate-poll-pages.mjs +74 -0
  33. package/bin/cli/help-log-file-page.d.mts +1 -0
  34. package/bin/cli/help-top-page.d.mts +1 -0
  35. package/bin/cli/help-top-page.mjs +22 -20
  36. package/bin/cli/help.d.mts +236 -0
  37. package/bin/cli/help.mjs +17 -0
  38. package/bin/cli/iterate-emitter.d.mts +8 -0
  39. package/bin/cli/iterate-emitter.mjs +2 -2
  40. package/bin/cli/iterate-flags.d.mts +11 -0
  41. package/bin/cli/iterate-flags.mjs +1 -1
  42. package/bin/cli/iterate-formatter.d.mts +19 -0
  43. package/bin/cli/iterate-instructions.d.mts +6 -0
  44. package/bin/cli/iterate-lean.d.mts +12 -0
  45. package/bin/cli/journal-formatter.d.mts +2 -0
  46. package/bin/cli/journal-formatter.mjs +15 -0
  47. package/bin/cli/journal-handler.d.mts +1 -0
  48. package/bin/cli/journal-handler.mjs +13 -25
  49. package/bin/cli/list-formatters.d.mts +76 -0
  50. package/bin/cli/list-formatters.mjs +9 -9
  51. package/bin/cli/mark-files-as-viewed-flags.d.mts +11 -0
  52. package/bin/cli/mark-files-as-viewed-formatter.d.mts +2 -0
  53. package/bin/cli/mutate-formatter.d.mts +2 -0
  54. package/bin/cli/poll-handler.d.mts +1 -0
  55. package/bin/cli/poll-handler.mjs +3 -3
  56. package/bin/cli/resolve-validators.d.mts +3 -0
  57. package/bin/cli/resolve-validators.mjs +6 -5
  58. package/bin/cli/runner.d.mts +7 -0
  59. package/bin/cli/suggestion-renderer.d.mts +3 -0
  60. package/bin/cli/validate-default-args.d.mts +6 -0
  61. package/bin/cli-parser.d.mts +2 -0
  62. package/bin/cli-parser.mjs +77 -20
  63. package/bin/commands/check-annotations.d.mts +5 -0
  64. package/bin/commands/check-status.d.mts +3 -0
  65. package/bin/commands/check-terminal-report.d.mts +5 -0
  66. package/bin/commands/check.d.mts +6 -0
  67. package/bin/commands/check.mjs +4 -2
  68. package/bin/commands/clean.d.mts +21 -0
  69. package/bin/commands/commit-suggestion-instruction.d.mts +8 -0
  70. package/bin/commands/commit-suggestion-instruction.mjs +3 -3
  71. package/bin/commands/commit-suggestion.d.mts +8 -0
  72. package/bin/commands/commit-suggestion.mjs +28 -26
  73. package/bin/commands/iterate/check-instructions.d.mts +16 -0
  74. package/bin/commands/iterate/check-instructions.mjs +3 -3
  75. package/bin/commands/iterate/classify.d.mts +18 -0
  76. package/bin/commands/iterate/classify.mjs +4 -4
  77. package/bin/commands/iterate/escalate.d.mts +31 -0
  78. package/bin/commands/iterate/escalate.mjs +7 -4
  79. package/bin/commands/iterate/fix-code.d.mts +25 -0
  80. package/bin/commands/iterate/helpers.d.mts +13 -0
  81. package/bin/commands/iterate/helpers.mjs +4 -1
  82. package/bin/commands/iterate/index.d.mts +2 -0
  83. package/bin/commands/iterate/index.mjs +10 -5
  84. package/bin/commands/iterate/render.d.mts +5 -0
  85. package/bin/commands/iterate/render.mjs +8 -6
  86. package/bin/commands/iterate/reruns.d.mts +20 -0
  87. package/bin/commands/iterate/stall.d.mts +6 -0
  88. package/bin/commands/journal/index.d.mts +14 -0
  89. package/bin/commands/journal/index.mjs +1 -0
  90. package/bin/commands/journal/transform.d.mts +22 -0
  91. package/bin/commands/log-file.d.mts +5 -0
  92. package/bin/commands/mark-files-as-viewed.d.mts +26 -0
  93. package/bin/commands/mark-files-as-viewed.mjs +8 -5
  94. package/bin/commands/poll.d.mts +10 -0
  95. package/bin/commands/poll.mjs +1 -0
  96. package/bin/commands/ready-delay.d.mts +29 -0
  97. package/bin/commands/ready-mergeability.d.mts +15 -0
  98. package/bin/commands/resolve-mutate.d.mts +4 -0
  99. package/bin/commands/resolve-mutate.mjs +3 -1
  100. package/bin/commands/resolve.d.mts +4 -0
  101. package/bin/commands/shepherd-journal.d.mts +7 -0
  102. package/bin/commands/shepherd-journal.mjs +2 -2
  103. package/bin/comments/authors.d.mts +14 -0
  104. package/bin/comments/marker.d.mts +2 -0
  105. package/bin/comments/minimize-policy.d.mts +4 -0
  106. package/bin/comments/pending-ops.d.mts +15 -0
  107. package/bin/comments/rate-limit.d.mts +18 -0
  108. package/bin/comments/resolve.d.mts +34 -0
  109. package/bin/comments/resolve.mjs +1 -0
  110. package/bin/comments/review-thread-markers.d.mts +8 -0
  111. package/bin/comments/review-visibility.d.mts +28 -0
  112. package/bin/comments/sha-poll.d.mts +2 -0
  113. package/bin/comments/thread-visibility.d.mts +11 -0
  114. package/bin/comments/visible-comments.d.mts +11 -0
  115. package/bin/config/load.d.mts +60 -0
  116. package/bin/config/load.mjs +72 -1
  117. package/bin/config.json +0 -2
  118. package/bin/execution-context.d.mts +9 -0
  119. package/bin/execution-context.mjs +19 -0
  120. package/bin/exit-codes.d.mts +51 -0
  121. package/bin/exit-codes.mjs +74 -0
  122. package/bin/github/activity.d.mts +3 -0
  123. package/bin/github/activity.mjs +7 -0
  124. package/bin/github/batch-parser-helpers.d.mts +22 -0
  125. package/bin/github/batch-parsers.d.mts +3 -0
  126. package/bin/github/batch-parsers.mjs +6 -0
  127. package/bin/github/batch-raw-types.d.mts +207 -0
  128. package/bin/github/batch-response.d.mts +4 -0
  129. package/bin/github/batch-response.mjs +8 -3
  130. package/bin/github/batch.d.mts +22 -0
  131. package/bin/github/branch-protection.d.mts +3 -0
  132. package/bin/github/check-annotations.d.mts +2 -0
  133. package/bin/github/client.d.mts +46 -0
  134. package/bin/github/client.mjs +7 -2
  135. package/bin/github/errors.d.mts +25 -0
  136. package/bin/github/errors.mjs +26 -2
  137. package/bin/github/gql/batch-pr.gql +5 -0
  138. package/bin/github/gql/review-thread-comments.gql +1 -0
  139. package/bin/github/graphql-http.d.mts +19 -0
  140. package/bin/github/graphql-response.d.mts +7 -0
  141. package/bin/github/graphql-response.mjs +5 -0
  142. package/bin/github/http-auth.d.mts +4 -0
  143. package/bin/github/http-auth.mjs +2 -1
  144. package/bin/github/http-request.d.mts +7 -0
  145. package/bin/github/http-utils.d.mts +11 -0
  146. package/bin/github/http.d.mts +5 -0
  147. package/bin/github/pagination.d.mts +45 -0
  148. package/bin/github/queries.d.mts +24 -0
  149. package/bin/github/rest-http.d.mts +2 -0
  150. package/bin/github/rest-http.mjs +19 -5
  151. package/bin/github/thread-comments.d.mts +2 -0
  152. package/bin/index.d.mts +10 -0
  153. package/bin/index.mjs +3 -2
  154. package/bin/log/log-file.d.mts +28 -0
  155. package/bin/log/session.d.mts +31 -0
  156. package/bin/log/setup.d.mts +6 -0
  157. package/bin/mcp/index.d.mts +5 -0
  158. package/bin/mcp/index.mjs +8 -0
  159. package/bin/mcp/server.d.mts +8 -0
  160. package/bin/mcp/server.mjs +157 -0
  161. package/bin/mcp-stdio.d.mts +2 -0
  162. package/bin/mcp-stdio.mjs +7 -0
  163. package/bin/merge-status/derive.d.mts +19 -0
  164. package/bin/reporters/agent.d.mts +23 -0
  165. package/bin/reporters/agent.mjs +3 -0
  166. package/bin/state/base.d.mts +1 -0
  167. package/bin/state/bot-cr-seen.d.mts +51 -0
  168. package/bin/state/bot-cr-seen.mjs +1 -1
  169. package/bin/state/fix-attempts.d.mts +27 -0
  170. package/bin/state/iterate-stall.d.mts +27 -0
  171. package/bin/state/seen-comments.d.mts +62 -0
  172. package/bin/suggestions/extract.d.mts +8 -0
  173. package/bin/suggestions/parse.d.mts +48 -0
  174. package/bin/suggestions/patch.d.mts +14 -0
  175. package/bin/threads/transcript.d.mts +14 -0
  176. package/bin/threads/transcript.mjs +4 -0
  177. package/bin/types/activity.d.mts +30 -0
  178. package/bin/types/agent-thread.d.mts +9 -0
  179. package/bin/types/check-annotations.d.mts +14 -0
  180. package/bin/types/check-classification.d.mts +19 -0
  181. package/bin/types/github.d.mts +139 -0
  182. package/bin/types/iterate.d.mts +157 -0
  183. package/bin/types/protected-run.d.mts +6 -0
  184. package/bin/types/report.d.mts +176 -0
  185. package/bin/types/review-thread.d.mts +12 -0
  186. package/bin/types.d.mts +9 -0
  187. package/bin/util/markdown.d.mts +1 -0
  188. package/bin/util/path-segment.d.mts +2 -0
  189. package/bin/util/sleep.d.mts +1 -0
  190. package/bin/util/worktree.d.mts +9 -0
  191. package/bin/util/worktree.mjs +4 -1
  192. package/package.json +51 -37
  193. package/plugins/pr-shepherd/.codex-plugin/plugin.json +3 -2
  194. package/plugins/pr-shepherd/.codex.mcp.json +8 -0
  195. package/plugins/pr-shepherd/.mcp.json +6 -0
  196. package/plugins/pr-shepherd/skills/mark-files-as-viewed/SKILL.md +5 -19
  197. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +6 -15
  198. package/src/classify/types.mts +12 -0
@@ -0,0 +1,5 @@
1
+ import type { BatchPrData, MergeStatusResult, ShepherdReport } from "../types.mts";
2
+ export declare function buildTerminalReport(prNumber: number, repo: {
3
+ owner: string;
4
+ name: string;
5
+ }, batchData: BatchPrData, mergeStatus: MergeStatusResult, status: "MERGED" | "CLOSED"): ShepherdReport;
@@ -0,0 +1,6 @@
1
+ import type { GlobalOptions, ShepherdReport } from "../types.mts";
2
+ export declare function runCheck(opts: GlobalOptions & {
3
+ autoResolve?: boolean;
4
+ autoMinimizeSuppressed?: boolean;
5
+ skipTriage?: boolean;
6
+ }): Promise<ShepherdReport>;
@@ -19,11 +19,13 @@ import { markReviewInlineThreadMarkers } from "../comments/review-thread-markers
19
19
  import { normalizeBotUsernames } from "../comments/authors.mjs";
20
20
  import { discoverRuleFiles, loadRules } from "../classify/loader.mjs";
21
21
  import { buildClassifyIndex, partitionBatch } from "../classify/apply.mjs";
22
+ import { EXIT, ShepherdError } from "../exit-codes.mjs";
23
+ import { getEffectiveCwd } from "../execution-context.mjs";
22
24
  export async function runCheck(opts) {
23
25
  const repo = await getRepoInfo();
24
26
  const prNumber = opts.prNumber ?? (await getCurrentPrNumber());
25
27
  if (prNumber === null) {
26
- throw new Error("No open PR found for current branch. Pass a PR number explicitly.");
28
+ throw new ShepherdError("No open PR found for current branch. Pass a PR number explicitly.", EXIT.UNAVAILABLE);
27
29
  }
28
30
  const config = loadConfig();
29
31
  const paginateApprovedReviews = config.iterate.minimizeApprovals;
@@ -49,7 +51,7 @@ export async function runCheck(opts) {
49
51
  const stateKey = { owner: repo.owner, repo: repo.name, pr: prNumber };
50
52
  const seenMap = await loadSeenMap(stateKey);
51
53
  const botUsernames = normalizeBotUsernames(config.botUsernames);
52
- const ruleSet = await loadRules(discoverRuleFiles(process.cwd()));
54
+ const ruleSet = await loadRules(discoverRuleFiles(getEffectiveCwd()));
53
55
  const classifyIndex = buildClassifyIndex(ruleSet, batchData);
54
56
  const partition = partitionBatch(classifyIndex, batchData);
55
57
  const triaged = await attachUnseenCheckAnnotations(triagedBase, seenMap, prNumber);
@@ -0,0 +1,21 @@
1
+ export type CleanVariant = "pr" | "branch" | "current" | "repo" | "all";
2
+ interface CleanOptions {
3
+ variant: CleanVariant;
4
+ /** PR number string (for "pr") or branch name (for "branch"). Defaults to current. */
5
+ value?: string;
6
+ dryRun?: boolean;
7
+ }
8
+ export interface CleanResult {
9
+ ok: boolean;
10
+ variant: CleanVariant;
11
+ dryRun: boolean;
12
+ base: string;
13
+ target: string;
14
+ /** Paths removed (actual delete) or that would be removed (dry-run). */
15
+ deleted: string[];
16
+ /** Target path when it did not exist. */
17
+ skipped: string[];
18
+ error?: string;
19
+ }
20
+ export declare function runClean(opts: CleanOptions): Promise<CleanResult>;
21
+ export {};
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Build the `build-suggestion-patch` instruction step for agent consumers.
3
+ * Currently emitted by iterate `fix_code` for suggestion review threads.
4
+ * @param sectionName - The markdown section heading where suggestion threads appear,
5
+ * e.g. `"## Review threads"`.
6
+ * @param includeDriftHint - Whether to add the trailing note about drift on failed apply.
7
+ */
8
+ export declare function buildCommitSuggestionInstruction(prNumber: number, sectionName: string, includeDriftHint: boolean): string;
@@ -1,6 +1,6 @@
1
1
  import { buildPrShepherdCommand } from "../cli/runner.mjs";
2
2
  /**
3
- * Build the `commit-suggestion` instruction step for agent consumers.
3
+ * Build the `build-suggestion-patch` instruction step for agent consumers.
4
4
  * Currently emitted by iterate `fix_code` for suggestion review threads.
5
5
  * @param sectionName - The markdown section heading where suggestion threads appear,
6
6
  * e.g. `"## Review threads"`.
@@ -8,7 +8,7 @@ import { buildPrShepherdCommand } from "../cli/runner.mjs";
8
8
  */
9
9
  export function buildCommitSuggestionInstruction(prNumber, sectionName, includeDriftHint) {
10
10
  const command = buildPrShepherdCommand([
11
- "commit-suggestion",
11
+ "build-suggestion-patch",
12
12
  String(prNumber),
13
13
  "--thread-id",
14
14
  "<id>",
@@ -19,5 +19,5 @@ export function buildCommitSuggestionInstruction(prNumber, sectionName, includeD
19
19
  const driftHint = includeDriftHint
20
20
  ? " If the patch fails to apply (drift since the suggestion was written), fall through to the manual fix step."
21
21
  : " If the patch fails to apply, fall through to the manual-edit step.";
22
- return `For each thread marked \`[suggestion]\` under \`${sectionName}\`: run \`${command}\` to retrieve the patch and suggested commit. The CLI does not mutate the working tree — apply the patch yourself (run \`git apply\` with the diff shown, or edit the file directly using the line range), then stage the listed file and run the suggested \`git commit\` from the \`## Instructions\` section. Human-authored thread IDs are replied to by the resolve command below; Shepherd does not auto-resolve them.${driftHint} Do not retry the same command.`;
22
+ return `For each thread marked \`[suggestion]\` under \`${sectionName}\`: run \`${command}\` to retrieve the patch and suggested commit. The CLI does not mutate the working tree — apply the patch yourself (run \`git apply\` with the diff shown, or edit the file directly using the line range), then stage the listed file and run the suggested \`git commit\` from the \`## Instructions\` section. Human-authored thread IDs are replied to by the apply command below; Shepherd does not auto-resolve them.${driftHint} Do not retry the same command.`;
23
23
  }
@@ -0,0 +1,8 @@
1
+ import type { CommitSuggestionResult, GlobalOptions } from "../types.mts";
2
+ export interface CommitSuggestionOptions extends GlobalOptions {
3
+ threadId: string;
4
+ message: string;
5
+ description?: string;
6
+ }
7
+ /** @deprecated Hidden implementation for `commit-suggestion`; use `build-suggestion-patch`. */
8
+ export declare function runCommitSuggestion(opts: CommitSuggestionOptions): Promise<CommitSuggestionResult>;
@@ -1,78 +1,79 @@
1
1
  import { execFile as execFileCb } from "node:child_process";
2
2
  import { readFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
3
4
  import { promisify } from "node:util";
4
5
  import { getRepoInfo, getCurrentPrNumber, getCurrentBranch } from "../github/client.mjs";
5
6
  import { fetchPrBatch } from "../github/batch.mjs";
6
7
  import { parseSuggestion, isCommittableSuggestion } from "../suggestions/parse.mjs";
7
8
  import { buildUnifiedDiff } from "../suggestions/patch.mjs";
9
+ import { EXIT, ShepherdError } from "../exit-codes.mjs";
8
10
  import { buildPrShepherdCommand } from "../cli/runner.mjs";
11
+ import { getEffectiveCwd, getExecutionCwd } from "../execution-context.mjs";
9
12
  const execFile = promisify(execFileCb);
13
+ /** @deprecated Hidden implementation for `commit-suggestion`; use `build-suggestion-patch`. */
10
14
  export async function runCommitSuggestion(opts) {
11
15
  if (!opts.threadId) {
12
- throw new Error("--thread-id is required");
16
+ throw new ShepherdError("--thread-id is required", EXIT.USAGE);
13
17
  }
14
18
  if (!opts.message || opts.message.trim() === "") {
15
- throw new Error("--message is required and must be non-empty");
19
+ throw new ShepherdError("--message is required and must be non-empty", EXIT.USAGE);
16
20
  }
17
21
  const repo = await getRepoInfo();
18
22
  const prNumber = opts.prNumber ?? (await getCurrentPrNumber());
19
23
  if (prNumber === null) {
20
- throw new Error("No open PR found for current branch. Pass a PR number explicitly.");
24
+ throw new ShepherdError("No open PR found for current branch. Pass a PR number explicitly.", EXIT.UNAVAILABLE);
21
25
  }
22
26
  const currentBranch = await getCurrentBranch();
23
- const { stdout: localHeadOut } = await execFile("git", ["rev-parse", "HEAD"]);
27
+ const { stdout: localHeadOut } = await execFile("git", ["rev-parse", "HEAD"], {
28
+ cwd: getExecutionCwd(),
29
+ });
24
30
  const localHeadSha = localHeadOut.trim();
25
31
  const { data } = await fetchPrBatch(prNumber, repo);
26
32
  if (!data.headRepoWithOwner) {
27
- throw new Error(`PR #${prNumber} head repository is unavailable (fork may have been deleted).`);
33
+ throw new ShepherdError(`PR #${prNumber} head repository is unavailable (fork may have been deleted).`, EXIT.UNAVAILABLE);
28
34
  }
29
35
  if (currentBranch !== data.headRefName) {
30
- throw new Error(`Current branch "${currentBranch}" does not match PR head branch "${data.headRefName}". ` +
31
- `Check out "${data.headRefName}" before applying suggestions.`);
36
+ throw new ShepherdError(`Current branch "${currentBranch}" does not match PR head branch "${data.headRefName}". ` +
37
+ `Check out "${data.headRefName}" before applying suggestions.`, EXIT.UNAVAILABLE);
32
38
  }
33
39
  if (localHeadSha !== data.headRefOid) {
34
- throw new Error(`Local HEAD ${localHeadSha} does not match PR head ${data.headRefOid}. ` +
35
- `Pull/rebase "${data.headRefName}" to the latest PR head and try again.`);
40
+ throw new ShepherdError(`Local HEAD ${localHeadSha} does not match PR head ${data.headRefOid}. ` +
41
+ `Pull/rebase "${data.headRefName}" to the latest PR head and try again.`, EXIT.UNAVAILABLE);
36
42
  }
37
43
  const thread = data.reviewThreads.find((t) => t.id === opts.threadId);
38
44
  if (!thread) {
39
- throw new Error(`Thread ${opts.threadId} not found on PR #${prNumber}.`);
45
+ throw new ShepherdError(`Thread ${opts.threadId} not found on PR #${prNumber}.`, EXIT.UNAVAILABLE);
40
46
  }
41
47
  if (thread.isResolved) {
42
- throw new Error(`Thread ${opts.threadId} is already resolved.`);
48
+ throw new ShepherdError(`Thread ${opts.threadId} is already resolved.`, EXIT.UNAVAILABLE);
43
49
  }
44
50
  if (thread.isOutdated) {
45
- throw new Error(`Thread ${opts.threadId} is outdated.`);
51
+ throw new ShepherdError(`Thread ${opts.threadId} is outdated.`, EXIT.UNAVAILABLE);
46
52
  }
47
53
  if (thread.isMinimized) {
48
- throw new Error(`Thread ${opts.threadId} is minimized.`);
54
+ throw new ShepherdError(`Thread ${opts.threadId} is minimized.`, EXIT.UNAVAILABLE);
49
55
  }
50
56
  if (!thread.path || thread.line === null) {
51
- throw new Error(`Thread ${opts.threadId} has no file/line anchor.`);
57
+ throw new ShepherdError(`Thread ${opts.threadId} has no file/line anchor.`, EXIT.UNAVAILABLE);
52
58
  }
53
59
  // Validate the target file is clean before generating the patch, so the emitted
54
60
  // `git add -- <file>` instruction cannot accidentally stage unrelated local edits.
55
- const { stdout: fileStatus } = await execFile("git", [
56
- "status",
57
- "--porcelain",
58
- "--",
59
- thread.path,
60
- ]);
61
+ const { stdout: fileStatus } = await execFile("git", ["status", "--porcelain", "--", thread.path], { cwd: getExecutionCwd() });
61
62
  if (fileStatus.trim() !== "") {
62
- throw new Error(`${thread.path} has uncommitted changes. Commit or stash them before running commit-suggestion.`);
63
+ throw new ShepherdError(`${thread.path} has uncommitted changes. Commit or stash them before running build-suggestion-patch.`, EXIT.UNAVAILABLE);
63
64
  }
64
65
  const parsed = parseSuggestion(thread.body);
65
66
  if (!parsed) {
66
- throw new Error(`Thread ${opts.threadId} has no suggestion block in the comment body.`);
67
+ throw new ShepherdError(`Thread ${opts.threadId} has no suggestion block in the comment body.`, EXIT.UNAVAILABLE);
67
68
  }
68
69
  if (!isCommittableSuggestion(parsed)) {
69
- throw new Error(`Thread ${opts.threadId}'s suggestion body contains nested suggestion fencing or unbalanced ` +
70
- `3+ backtick fences — refusing to apply (could silently truncate).`);
70
+ throw new ShepherdError(`Thread ${opts.threadId}'s suggestion body contains nested suggestion fencing or unbalanced ` +
71
+ `3+ backtick fences — refusing to apply (could silently truncate).`, EXIT.UNAVAILABLE);
71
72
  }
72
73
  const startLine = thread.startLine ?? thread.line;
73
74
  const endLine = thread.line;
74
75
  const filePath = thread.path;
75
- const originalContent = await readFile(filePath, "utf8");
76
+ const originalContent = await readFile(resolve(getEffectiveCwd(), filePath), "utf8");
76
77
  const patch = buildUnifiedDiff({
77
78
  path: filePath,
78
79
  originalContent,
@@ -93,7 +94,8 @@ export async function runCommitSuggestion(opts) {
93
94
  ...commitBodyArg.split("\n\n").map((p) => `-m ${sq(p)}`),
94
95
  ].join(" ");
95
96
  const resolveCommand = buildPrShepherdCommand([
96
- "resolve",
97
+ "apply",
98
+ "review",
97
99
  String(prNumber),
98
100
  "--resolve-thread-ids",
99
101
  opts.threadId,
@@ -0,0 +1,16 @@
1
+ import type { AgentCheck, ResolveCommand, Review } from "../../types.mts";
2
+ /** Build the stale-CR clause appended to the `## Changes-requested reviews` instruction. */
3
+ export declare function buildCrStaleClause(reviews: Review[]): string;
4
+ /**
5
+ * Build the optional behind-base push hint. Empty unless the branch is actually behind its base
6
+ * and the user configured a non-blank `iterate.behindBaseHint` — the CLI never prescribes
7
+ * rebase/merge mechanics itself (see "Keep skills and loop prompts minimal" in CLAUDE.md); this
8
+ * only echoes back the caller's own configured pointer. `hint` is trimmed and type-checked at the
9
+ * point of use (rather than at config load) so a malformed rc file value (non-string, or
10
+ * whitespace-only) degrades to "no hint" instead of rendering garbage into agent-facing text or
11
+ * discarding the rest of the user's config.
12
+ */
13
+ export declare function buildBehindBaseHintInstruction(baseBranch: string, hint: string, isBehind: boolean): string[];
14
+ /** Build the `Run the apply review: command` instruction, including its optional substitution hint. */
15
+ export declare function buildResolveCommandInstruction(resolveCommand: ResolveCommand): string[];
16
+ export declare function buildFailingCheckInstructions(checks: AgentCheck[]): string[];
@@ -23,13 +23,13 @@ export function buildBehindBaseHintInstruction(baseBranch, hint, isBehind) {
23
23
  return [];
24
24
  return [`The branch is behind \`origin/${baseBranch}\` — ${trimmedHint} before pushing.`];
25
25
  }
26
- /** Build the `Run the resolve: command` instruction, including its optional substitution hint. */
26
+ /** Build the `Run the apply review: command` instruction, including its optional substitution hint. */
27
27
  export function buildResolveCommandInstruction(resolveCommand) {
28
28
  if (!resolveCommand.hasMutations)
29
29
  return [];
30
30
  const instructions = [];
31
31
  if ((resolveCommand.replyThreadIds?.length ?? 0) > 0) {
32
- instructions.push(`Before running the \`resolve:\` command, remove any thread from \`--reply-thread-ids\` if the latest visible comment in that thread is your own prior Shepherd reply. Do not reply to your own comments.`);
32
+ instructions.push(`Before running the \`apply review:\` command, remove any thread from \`--reply-thread-ids\` if the latest visible comment in that thread is your own prior Shepherd reply. Do not reply to your own comments.`);
33
33
  }
34
34
  const substituteParts = [];
35
35
  if (resolveCommand.requiresHeadSha) {
@@ -39,7 +39,7 @@ export function buildResolveCommandInstruction(resolveCommand) {
39
39
  substituteParts.push(`\`$DISMISS_MESSAGE\` with a one-sentence reply/description of what you changed`);
40
40
  }
41
41
  const substituteHint = substituteParts.length > 0 ? `, substituting ${substituteParts.join(" and ")}` : "";
42
- instructions.push(`Run the \`resolve:\` command shown above${substituteHint}.`);
42
+ instructions.push(`Run the \`apply review:\` command shown above${substituteHint}.`);
43
43
  return instructions;
44
44
  }
45
45
  export function buildFailingCheckInstructions(checks) {
@@ -0,0 +1,18 @@
1
+ import type { AgentThread, Review, ResolveCommand, AgentCheck, ReviewThread } from "../../types.mts";
2
+ import { type NormalizedBotUsernames } from "../../comments/authors.mts";
3
+ import type { MinimizeCommentsPolicy } from "../../config/load.mts";
4
+ export declare function classifyReviewSummaries(summaries: {
5
+ firstLook: Review[];
6
+ seen: Review[];
7
+ edited: Review[];
8
+ }, approvals: Review[], minimizeApprovals: boolean, minimizeComments?: MinimizeCommentsPolicy | undefined, botUsernames?: NormalizedBotUsernames, unresolvedThreads?: ReviewThread[], ruleAutoResolveIds?: string[]): {
9
+ minimizeIds: string[];
10
+ selfMinimizeIds: string[];
11
+ firstLookSummaries: Review[];
12
+ editedSummaries: Review[];
13
+ surfacedApprovals: Review[];
14
+ };
15
+ export declare function buildResolveCommand(threads: AgentThread[], resolutionOnlyThreads: ReviewThread[], allCommentIds: string[], reviews: Review[], checks: AgentCheck[], prNumber: number, botUsernames?: NormalizedBotUsernames, ruleAutoResolveThreadIds?: string[]): {
16
+ resolveCommand: ResolveCommand;
17
+ resolveOnlyCommand?: ResolveCommand;
18
+ };
@@ -17,7 +17,7 @@ export function classifyReviewSummaries(summaries, approvals, minimizeApprovals,
17
17
  const eligible = (r) => shouldMinimizeAuthor(r.authorType, minimizeComments, r.author, botUsernames) &&
18
18
  !blockedReviewIds.has(r.id);
19
19
  // First-look summaries still need one tick to surface their body to the agent,
20
- // so their minimize IDs ride in the agent-facing resolve command. Seen summaries
20
+ // so their minimize IDs ride in the agent-facing apply command. Seen summaries
21
21
  // (already surfaced in a prior tick) have no new content to show — the CLI
22
22
  // self-minimizes them in-process (selfMinimizeIds) instead of routing a
23
23
  // cosmetic-only mutation through fix_code (issue #313). Edited summaries are
@@ -80,7 +80,7 @@ export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentId
80
80
  if (hasMessageMutations && hasResolveOrMinimize) {
81
81
  // Split: message-bearing mutations (replies + dismissals) ride in resolveArgv;
82
82
  // resolve/minimize mutations go in resolveOnlyArgv so they can run without SHA or message.
83
- const resolveArgv = buildPrShepherdCommand(["resolve", String(prNumber)]).argv;
83
+ const resolveArgv = buildPrShepherdCommand(["apply", "review", String(prNumber)]).argv;
84
84
  if (replyThreadIds.length > 0) {
85
85
  resolveArgv.push("--reply-thread-ids", replyThreadIds.join(","));
86
86
  }
@@ -99,7 +99,7 @@ export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentId
99
99
  ...(hasDismiss ? { dismissReviewIds } : undefined),
100
100
  hasMutations: true,
101
101
  };
102
- const resolveOnlyArgv = buildPrShepherdCommand(["resolve", String(prNumber)]).argv;
102
+ const resolveOnlyArgv = buildPrShepherdCommand(["apply", "review", String(prNumber)]).argv;
103
103
  if (resolveThreadIds.length > 0) {
104
104
  resolveOnlyArgv.push("--resolve-thread-ids", resolveThreadIds.join(","));
105
105
  }
@@ -116,7 +116,7 @@ export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentId
116
116
  return { resolveCommand, resolveOnlyCommand };
117
117
  }
118
118
  // Single command: all mutations combined (or only one category present).
119
- const argv = buildPrShepherdCommand(["resolve", String(prNumber)]).argv;
119
+ const argv = buildPrShepherdCommand(["apply", "review", String(prNumber)]).argv;
120
120
  if (replyThreadIds.length > 0) {
121
121
  argv.push("--reply-thread-ids", replyThreadIds.join(","));
122
122
  argv.push("--message", "$DISMISS_MESSAGE");
@@ -0,0 +1,31 @@
1
+ import type { EscalateDetails, EscalateTrigger, ReviewThread } from "../../types.mts";
2
+ interface EscalateCheck {
3
+ triggers: EscalateTrigger[];
4
+ thrashHistory?: EscalateDetails["thrashHistory"];
5
+ }
6
+ export declare function checkEscalateTriggers(actionableThreads: ReviewThread[], threadAttempts: Record<string, number>): EscalateCheck;
7
+ interface BaseBranchLookup {
8
+ branch: string;
9
+ /** True when we could not confirm the branch name from GitHub. Callers must
10
+ * escalate rather than emitting a rebase against a potentially-wrong base. */
11
+ isFallback: boolean;
12
+ /** Populated when `isFallback`; one-line reason shown in escalate output. */
13
+ failureReason?: string;
14
+ }
15
+ /**
16
+ * Validate the base branch name from the GraphQL batch (`report.baseBranch`)
17
+ * and fall back safely if it's missing/unsafe. The branch is interpolated into
18
+ * shell commands by `buildFixInstructions`, so we reject anything outside
19
+ * `[A-Za-z0-9._/-]` to prevent shell injection.
20
+ */
21
+ /**
22
+ * Render a seconds count as an approximate human duration: "8 seconds" below one minute,
23
+ * otherwise whole minutes ("166 minutes", "60 minutes") — matching the precision the
24
+ * generic stall timer has always reported, just adding a seconds tier for sub-minute ages
25
+ * instead of always flooring to "0 minutes".
26
+ */
27
+ export declare function formatDurationApprox(seconds: number): string;
28
+ export declare function validateBaseBranch(raw: string): BaseBranchLookup;
29
+ export declare function buildEscalateHumanMessage(escalate: Omit<EscalateDetails, "humanMessage">, pr: number): string;
30
+ export declare function buildEscalateSuggestion(triggers: EscalateTrigger[], detail?: string): string;
31
+ export {};
@@ -1,4 +1,7 @@
1
1
  import { loadConfig } from "../../config/load.mjs";
2
+ function renderEscalateAuthor(item) {
3
+ return [`@${item.author}`, item.authorType, item.authorAssociation].filter(Boolean).join(" · ");
4
+ }
2
5
  export function checkEscalateTriggers(actionableThreads, threadAttempts) {
3
6
  const triggers = [];
4
7
  const maxAttempts = loadConfig().iterate.fixAttemptsPerThread;
@@ -85,21 +88,21 @@ export function buildEscalateHumanMessage(escalate, pr) {
85
88
  lines.push("");
86
89
  for (const t of escalate.unresolvedThreads) {
87
90
  const loc = t.path ? `\`${t.path}:${t.line ?? "?"}\`` : "(no location)";
88
- lines.push(`- thread \`${t.id}\` — ${loc} (@${t.author}):`);
91
+ lines.push(`- thread \`${t.id}\` — ${loc} (${renderEscalateAuthor(t)}):`);
89
92
  lines.push("");
90
93
  for (const bodyLine of t.body.split("\n"))
91
94
  lines.push(` > ${bodyLine}`);
92
95
  lines.push("");
93
96
  }
94
97
  for (const r of escalate.changesRequestedReviews) {
95
- lines.push(`- review \`${r.id}\` (@${r.author}):`);
98
+ lines.push(`- review \`${r.id}\` (${renderEscalateAuthor(r)}):`);
96
99
  lines.push("");
97
100
  for (const bodyLine of r.body.split("\n"))
98
101
  lines.push(` > ${bodyLine}`);
99
102
  lines.push("");
100
103
  }
101
104
  for (const c of escalate.ambiguousComments) {
102
- lines.push(`- comment \`${c.id}\` (@${c.author}):`);
105
+ lines.push(`- comment \`${c.id}\` (${renderEscalateAuthor(c)}):`);
103
106
  lines.push("");
104
107
  for (const bodyLine of c.body.split("\n"))
105
108
  lines.push(` > ${bodyLine}`);
@@ -137,7 +140,7 @@ export function buildEscalateSuggestion(triggers, detail) {
137
140
  }
138
141
  if (triggers.includes("bot-cr-not-dismissed")) {
139
142
  const ids = detail ? ` (review IDs: ${detail})` : "";
140
- return `Bot CHANGES_REQUESTED review(s) remained undismissed past the stall window${ids}. The agent likely dropped \`--dismiss-review-ids\` from a prior resolve command. Dismiss the review(s) manually (or re-run resolve with the IDs) to unblock the PR.`;
143
+ return `Bot CHANGES_REQUESTED review(s) remained undismissed past the stall window${ids}. The agent likely dropped \`--dismiss-review-ids\` from a prior apply command. Dismiss the review(s) manually (or re-run \`pr-shepherd apply review\` with the IDs) to unblock the PR.`;
141
144
  }
142
145
  return "Ambiguous state — automated handling cannot proceed safely. Inspect the PR and act manually.";
143
146
  }
@@ -0,0 +1,25 @@
1
+ import type { IterateCommandOptions, IterateResult, IterateResultBase, Review, ShepherdReport } from "../../types.mts";
2
+ import type { NormalizedBotUsernames } from "../../comments/authors.mts";
3
+ interface HandleFixCodeContext {
4
+ base: IterateResultBase;
5
+ report: ShepherdReport;
6
+ opts: IterateCommandOptions;
7
+ headSha: string;
8
+ stallKey: {
9
+ owner: string;
10
+ repo: string;
11
+ pr: number;
12
+ };
13
+ prNumber: number;
14
+ stallTimeoutSeconds: number;
15
+ repoOwner: string;
16
+ repoName: string;
17
+ reviewSummaryIds: string[];
18
+ firstLookSummaries: Review[];
19
+ editedSummaries: Review[];
20
+ surfacedApprovals: Review[];
21
+ botUsernames: NormalizedBotUsernames;
22
+ ruleAutoResolveThreadIds?: string[];
23
+ }
24
+ export declare function handleFixCode(ctx: HandleFixCodeContext): Promise<IterateResult>;
25
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { ActiveCheck, IterateResult, IterateResultBase, IterateResultSummary, RelevantCheck, ShepherdReport } from "../../types.mts";
2
+ export { buildAutoCancelRunIdsWithOptions, buildInProgressRunIds, buildRunProtection, } from "./reruns.mts";
3
+ export declare function buildSummary(report: ShepherdReport): IterateResultSummary;
4
+ /** Non-blocking check-name lists (ignored/superseded), omitted from the result when empty. */
5
+ export declare function buildSuppressedCheckFields(report: ShepherdReport): Pick<IterateResultBase, "ignoredNames" | "supersededNames">;
6
+ /** Build the `cancel` result for a merged/closed PR — caller has already updated ready-delay/stall state. */
7
+ export declare function buildTerminalCancelResult(report: ShepherdReport): IterateResult;
8
+ /** Build completed, non-skipped checks relevant to PR readiness. */
9
+ export declare function buildRelevantChecks(report: ShepherdReport): RelevantCheck[];
10
+ export declare function buildActiveChecks(report: ShepherdReport): ActiveCheck[];
11
+ export declare function tryCancelRun(runId: string, owner: string, repo: string): Promise<string | null>;
12
+ export declare function getCurrentHeadSha(): Promise<string | null>;
13
+ export declare function buildWaitLog(base: IterateResultBase): string;
@@ -1,6 +1,7 @@
1
1
  import { execFile as execFileCb } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import { rest } from "../../github/http.mjs";
4
+ import { getExecutionCwd } from "../../execution-context.mjs";
4
5
  const execFile = promisify(execFileCb);
5
6
  export { buildAutoCancelRunIdsWithOptions, buildInProgressRunIds, buildRunProtection, } from "./reruns.mjs";
6
7
  export function buildSummary(report) {
@@ -114,7 +115,9 @@ export async function tryCancelRun(runId, owner, repo) {
114
115
  }
115
116
  export async function getCurrentHeadSha() {
116
117
  try {
117
- const { stdout } = await execFile("git", ["rev-parse", "HEAD"]);
118
+ const { stdout } = await execFile("git", ["rev-parse", "HEAD"], {
119
+ cwd: getExecutionCwd(),
120
+ });
118
121
  return stdout.trim();
119
122
  }
120
123
  catch {
@@ -0,0 +1,2 @@
1
+ import type { IterateCommandOptions, IterateResult } from "../../types.mts";
2
+ export declare function runIterate(opts: IterateCommandOptions): Promise<IterateResult>;
@@ -4,6 +4,7 @@ import { getCurrentPrNumber } from "../../github/client.mjs";
4
4
  import { graphql } from "../../github/http.mjs";
5
5
  import { MARK_PR_READY_MUTATION } from "../../github/queries.mjs";
6
6
  import { loadConfig } from "../../config/load.mjs";
7
+ import { EXIT, ShepherdError } from "../../exit-codes.mjs";
7
8
  import { getCurrentHeadSha, buildSummary, buildRelevantChecks, buildActiveChecks, buildWaitLog, buildSuppressedCheckFields, buildTerminalCancelResult, } from "./helpers.mjs";
8
9
  import { classifyReviewSummaries } from "./classify.mjs";
9
10
  import { applyStallGuard } from "./stall.mjs";
@@ -17,18 +18,22 @@ export async function runIterate(opts) {
17
18
  const readyDelaySeconds = opts.readyDelaySeconds ?? config.watch.readyDelayMinutes * 60;
18
19
  const stallTimeoutSeconds = opts.stallTimeoutSeconds ?? config.iterate.stallTimeoutMinutes * 60;
19
20
  const prNumber = opts.prNumber ?? (await getCurrentPrNumber());
20
- if (prNumber === null)
21
- throw new Error("No open PR found for current branch. Pass a PR number explicitly.");
21
+ if (prNumber === null) {
22
+ throw new ShepherdError("No open PR found for current branch. Pass a PR number explicitly.", EXIT.UNAVAILABLE);
23
+ }
22
24
  const neverCancelRuns = opts.neverCancelRuns ?? config.actions.neverCancelRuns;
23
25
  const report = await runCheck({
24
26
  ...opts,
25
27
  prNumber,
26
- autoResolve: config.actions.autoResolveOutdated,
28
+ // Outdated Shepherd-visible threads are always resolved by iterate. The
29
+ // old actions.autoResolveOutdated switch is retained only as an ignored
30
+ // compatibility key in the config loader.
31
+ autoResolve: true,
27
32
  autoMinimizeSuppressed: config.actions.autoMinimizeSuppressed,
28
33
  });
29
34
  const [repoOwner, repoName] = report.repo.split("/");
30
35
  if (!repoOwner || !repoName) {
31
- throw new Error(`Unexpected repo format: "${report.repo}" (expected "owner/name")`);
36
+ throw new ShepherdError(`Unexpected repo format: "${report.repo}" (expected "owner/name")`, EXIT.DATAERR);
32
37
  }
33
38
  const stallKey = { owner: repoOwner, repo: repoName, pr: prNumber };
34
39
  if (report.mergeStatus.state !== "OPEN") {
@@ -46,7 +51,7 @@ export async function runIterate(opts) {
46
51
  // GitHub can still return a null/error/rate-limit result per ID without
47
52
  // throwing (autoMinimizeComments reports this via `errors`, not a rejection);
48
53
  // any ID it did not confirm minimized falls back into the agent-facing set so
49
- // the resolve command remains a working fallback instead of silently dropping it.
54
+ // the apply command remains a working fallback instead of silently dropping it.
50
55
  let reviewSummaryIds = minimizeIds;
51
56
  if (selfMinimizeIds.length > 0) {
52
57
  const { minimized } = await autoMinimizeComments(selfMinimizeIds);
@@ -0,0 +1,5 @@
1
+ import type { AgentThread, AgentComment, AgentCheck, Review, ResolveCommand, FirstLookThread, FirstLookComment, ReviewThread } from "../../types.mts";
2
+ /** Render a resolve command as a shell snippet. Appends `--require-sha "$HEAD_SHA"` when set. */
3
+ export declare function renderResolveCommand(rc: ResolveCommand): string;
4
+ export declare function buildFixInstructions(threads: AgentThread[], actionableComments: AgentComment[], checks: AgentCheck[], changesRequestedReviews: Review[], baseBranch: string, resolveCommand: ResolveCommand, hasConflicts: boolean, prNumber: number, cancelledCount: number, firstLookThreads?: FirstLookThread[], firstLookComments?: FirstLookComment[], firstLookSummaries?: Review[], editedSummaries?: Review[], inProgressRunIds?: string[], resolutionOnlyThreads?: ReviewThread[], resolveOnlyCommand?: ResolveCommand, behindBaseHint?: string, // iterate.behindBaseHint — see buildBehindBaseHintInstruction
5
+ isBehind?: boolean): string[];
@@ -32,14 +32,16 @@ isBehind = false) {
32
32
  if (changesRequestedReviews.length > 0)
33
33
  actionableSections.push("`## Changes-requested reviews`");
34
34
  const sectionRef = actionableSections.length > 0 ? `under ${actionableSections.join(", ")}` : "above";
35
- const resolveClause = resolveCommand.hasMutations ? ", then run the `resolve:` command" : "";
35
+ const resolveClause = resolveCommand.hasMutations
36
+ ? ", then run the `apply review:` command"
37
+ : "";
36
38
  if (hasConflicts) {
37
39
  // Conflicts make push mandatory regardless of whether code edits are needed.
38
40
  instructions.push(`The branch has merge conflicts that must be resolved before merging (see \`**branch**\` above). Apply any code edits for items ${sectionRef}, then commit and push${resolveClause}.`);
39
41
  }
40
42
  else {
41
43
  const skipClause = resolveCommand.hasMutations
42
- ? "skip the commit/push and run the `resolve:` command"
44
+ ? "skip the commit/push and run the `apply review:` command"
43
45
  : "no push is needed";
44
46
  instructions.push(`Decide for each item ${sectionRef} whether a code change is warranted. **If any code changes are needed:** apply edits, commit, push${resolveClause}. **If no code changes are needed:** ${skipClause}.`);
45
47
  }
@@ -59,12 +61,12 @@ isBehind = false) {
59
61
  // is only accurate when threads are present.
60
62
  const filesRef = threads.length > 0 ? "each file referenced above" : "the relevant files";
61
63
  const suggestionFallback = hasSuggestions
62
- ? ` When applying a \`[suggestion]\` thread manually (e.g. after a failed \`commit-suggestion\` run), replace the exact line range shown in the heading (\`path:startLine-endLine\`) with the replacement shown in its \`Replaces lines …\` block verbatim — an empty replacement deletes those lines, a single blank line replaces the range with one blank line.`
64
+ ? ` When applying a \`[suggestion]\` thread manually (e.g. after a failed \`build-suggestion-patch\` run), replace the exact line range shown in the heading (\`path:startLine-endLine\`) with the replacement shown in its \`Replaces lines …\` block verbatim — an empty replacement deletes those lines, a single blank line replaces the range with one blank line.`
63
65
  : "";
64
66
  instructions.push(`Apply code fixes: read and edit ${filesRef}.${suggestionFallback}`);
65
67
  }
66
68
  if (resolutionOnlyThreads.length > 0) {
67
- instructions.push(`Review the threads under \`## Review threads to resolve\`. Human-authored threads are replied to by the \`resolve:\` command shown below; Shepherd does not resolve them. Bot/non-human threads are included in \`--resolve-thread-ids\`.`);
69
+ instructions.push(`Review the threads under \`## Review threads to resolve\`. Human-authored threads are replied to by the \`apply review:\` command shown below; Shepherd does not resolve them. Bot/non-human threads are included in \`--resolve-thread-ids\`.`);
68
70
  }
69
71
  instructions.push(...buildFailingCheckInstructions(checks));
70
72
  if (checks.some((c) => (c.annotations?.length ?? 0) > 0)) {
@@ -74,7 +76,7 @@ isBehind = false) {
74
76
  const staleClause = buildCrStaleClause(changesRequestedReviews);
75
77
  instructions.push(`For each bullet under \`## Changes-requested reviews\` above: read the review body and apply the requested changes.${staleClause}`);
76
78
  if ((resolveCommand.dismissReviewIds?.length ?? 0) > 0)
77
- instructions.push(`Pass every ID listed in \`--dismiss-review-ids\` to the \`resolve:\` command verbatim — these are bot/non-human CR reviews that the agent (not the author) must dismiss. Dropping an ID leaves the PR in \`CHANGES_REQUESTED\` state; the next tick re-surfaces it as \`[pending dismissal]\` and an unattended bot CR escalates after \`iterate.stallTimeoutMinutes\`.`);
79
+ instructions.push(`Pass every ID listed in \`--dismiss-review-ids\` to the \`apply review:\` command verbatim — these are bot/non-human CR reviews that the agent (not the author) must dismiss. Dropping an ID leaves the PR in \`CHANGES_REQUESTED\` state; the next tick re-surfaces it as \`[pending dismissal]\` and an unattended bot CR escalates after \`iterate.stallTimeoutMinutes\`.`);
78
80
  }
79
81
  if (resolveOnlyCommand?.hasMutations)
80
82
  instructions.push(`Run the \`resolve-only:\` command shown above — no substitutions needed.`);
@@ -84,7 +86,7 @@ isBehind = false) {
84
86
  }
85
87
  const firstLookTotal = firstLookThreads.length + firstLookComments.length;
86
88
  if (firstLookTotal > 0) {
87
- instructions.push(`Items in \`## First-look items\` are shown so you can acknowledge their current status before acting. If a first-look thread also appears under \`## Review threads to resolve\`, its ID is already included in the \`resolve:\` command; otherwise do not pass first-look-only IDs to mutation flags.`);
89
+ instructions.push(`Items in \`## First-look items\` are shown so you can acknowledge their current status before acting. If a first-look thread also appears under \`## Review threads to resolve\`, its ID is already included in the \`apply review:\` command; otherwise do not pass first-look-only IDs to mutation flags.`);
88
90
  }
89
91
  if (firstLookSummaries.length > 0)
90
92
  instructions.push(SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE);
@@ -0,0 +1,20 @@
1
+ import type { ShepherdReport } from "../../types.mts";
2
+ import type { ProtectedRun } from "../../types/protected-run.mts";
3
+ type FailingCheck = ShepherdReport["checks"]["failing"][number];
4
+ type InProgressCheck = ShepherdReport["checks"]["inProgress"][number];
5
+ type CancellableCheck = FailingCheck | InProgressCheck;
6
+ interface RunProtection {
7
+ protectedRunIds: Set<string>;
8
+ protectedRuns: ProtectedRun[];
9
+ }
10
+ interface BuildRunIdOptions {
11
+ protectedRunIds?: Set<string>;
12
+ }
13
+ export declare function buildAutoCancelRunIdsWithOptions(report: ShepherdReport, opts?: BuildRunIdOptions): string[];
14
+ interface BuildInProgressRunIdOptions {
15
+ suppressProtectedFreshReruns?: boolean;
16
+ protectedRunIds?: Set<string>;
17
+ }
18
+ export declare function buildInProgressRunIds(report: ShepherdReport, cancelledSet: Set<string>, opts?: BuildInProgressRunIdOptions): string[];
19
+ export declare function buildRunProtection(checks: CancellableCheck[], patterns?: string[]): RunProtection;
20
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { IterateResult, IterateResultBase, ShepherdReport } from "../../types.mts";
2
+ export declare function applyStallGuard(stallKey: {
3
+ owner: string;
4
+ repo: string;
5
+ pr: number;
6
+ }, stallTimeoutSeconds: number, headSha: string, base: IterateResultBase, prNumber: number, prospectiveResult: IterateResult, report: ShepherdReport, reviewSummaryIds: string[]): Promise<IterateResult>;
@@ -0,0 +1,14 @@
1
+ export interface RunJournalOptions {
2
+ prNumber: number | undefined;
3
+ rawItem: string;
4
+ dryRun: boolean;
5
+ }
6
+ export interface JournalResult {
7
+ prNumber: number;
8
+ mutated: boolean;
9
+ sectionExisted: boolean;
10
+ dryRun: boolean;
11
+ previewBody?: string;
12
+ }
13
+ /** @deprecated Hidden implementation for standalone `journal`; use `apply journal`. */
14
+ export declare function runJournal(opts: RunJournalOptions): Promise<JournalResult>;
@@ -1,5 +1,6 @@
1
1
  import { getRepoInfo, getPullRequestBody, updatePullRequestBody, getCurrentPrNumber, } from "../../github/client.mjs";
2
2
  import { validateJournalItem, appendJournalItem } from "./transform.mjs";
3
+ /** @deprecated Hidden implementation for standalone `journal`; use `apply journal`. */
3
4
  export async function runJournal(opts) {
4
5
  const validation = validateJournalItem(opts.rawItem);
5
6
  if (!validation.ok) {