pr-shepherd 0.25.1 → 0.25.3

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.
@@ -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.25.1",
4
+ "version": "0.25.3",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
@@ -121,6 +121,8 @@ export function formatFixCodeResult(header, result, opts) {
121
121
  }
122
122
  sections.push("## Post-fix push");
123
123
  const postFixLines = [`- base: \`${result.baseBranch}\``];
124
+ if (result.fix.resolveOnlyCommand?.hasMutations)
125
+ postFixLines.push(`- resolve-only: \`${renderResolveCommand(result.fix.resolveOnlyCommand)}\``);
124
126
  if (result.fix.resolveCommand.hasMutations) {
125
127
  postFixLines.push(`- resolve: \`${renderResolveCommand(result.fix.resolveCommand)}\``);
126
128
  }
@@ -10,14 +10,16 @@ Usage:
10
10
  [--require-sha SHA] [--format text|json]
11
11
 
12
12
  Flags:
13
- --resolve-thread-ids <ids> Comma-separated review thread IDs to resolve.
13
+ --resolve-thread-ids <ids> Comma-separated review thread IDs (PRRT_*) to resolve.
14
14
  Human-authored thread IDs are skipped; use --reply-thread-ids.
15
+ Note: comment IDs (PRRC_*) from gh api are not thread IDs and will fail.
15
16
  --reply-thread-ids <ids> Comma-separated human review thread IDs to reply to.
16
17
  --minimize-comment-ids <ids> Comma-separated issue/review comment IDs to minimize.
17
18
  --dismiss-review-ids <ids> Comma-separated CHANGES_REQUESTED review IDs to dismiss.
18
19
  --message <text> Reply/dismiss message. Required with --reply-thread-ids
19
20
  or --dismiss-review-ids.
20
21
  --require-sha <sha> Wait until GitHub reports this PR head SHA before mutating.
22
+ Must be a full 40-character lowercase hex SHA. Use $(git rev-parse HEAD).
21
23
  --format text|json Output format. Default: text.
22
24
  --help, -h Print this help and exit before GitHub I/O.
23
25
 
@@ -92,6 +92,9 @@ export function projectIterateLean(result, opts) {
92
92
  changesRequestedReviews: result.fix.changesRequestedReviews,
93
93
  }),
94
94
  resolveCommand: result.fix.resolveCommand,
95
+ ...(result.fix.resolveOnlyCommand && {
96
+ resolveOnlyCommand: result.fix.resolveOnlyCommand,
97
+ }),
95
98
  ...(result.fix.instructions.length > 0 && {
96
99
  instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, readyDelaySuffix),
97
100
  }),
@@ -0,0 +1,24 @@
1
+ export function rejectPrrcMinimizeIds(ids) {
2
+ const prrcIds = ids.filter((id) => id.startsWith("PRRC_"));
3
+ if (prrcIds.length > 0) {
4
+ process.stderr.write(`pr-shepherd: resolve: --minimize-comment-ids contains thread comment IDs (PRRC_*): ${prrcIds.join(", ")}. Thread comments cannot be minimized individually — resolve the parent thread using --resolve-thread-ids with the PRRT_* thread ID instead.\n`);
5
+ process.exitCode = 1;
6
+ }
7
+ return prrcIds;
8
+ }
9
+ export function warnPrrcThreadIds(ids) {
10
+ const prrcIds = ids.filter((id) => id.startsWith("PRRC_"));
11
+ if (prrcIds.length > 0) {
12
+ process.stderr.write(`pr-shepherd: resolve: warning: --resolve-thread-ids contains comment IDs (PRRC_*) instead of thread IDs (PRRT_*): ${prrcIds.join(", ")}. The resolveReviewThread mutation requires PRRT_* thread IDs. Run a GraphQL query for pullRequest.reviewThreads to get the correct IDs.\n`);
13
+ }
14
+ return prrcIds;
15
+ }
16
+ export function validateRequireSha(sha) {
17
+ if (sha === undefined)
18
+ return true;
19
+ if (/^[0-9a-f]{40}$/.test(sha))
20
+ return true;
21
+ process.stderr.write(`pr-shepherd: resolve: --require-sha must be a full 40-character lowercase hex SHA, got "${sha}". Short SHAs will never match GitHub's headRefOid. Use $(git rev-parse HEAD) to get the full SHA.\n`);
22
+ process.exitCode = 1;
23
+ return false;
24
+ }
@@ -30,6 +30,7 @@ import { USAGE, maybePrintHelp } from "./cli/help.mjs";
30
30
  import { formatMutateResult } from "./cli/formatters.mjs";
31
31
  import { handleClean, handleCommitSuggestion, handleIterate, handleMarkFilesAsViewed, } from "./cli/handlers.mjs";
32
32
  import { handlePoll } from "./cli/poll-handler.mjs";
33
+ import { warnPrrcThreadIds, validateRequireSha, rejectPrrcMinimizeIds, } from "./cli/resolve-validators.mjs";
33
34
  import { setupLog } from "./log/setup.mjs";
34
35
  // ---------------------------------------------------------------------------
35
36
  // Entry
@@ -130,6 +131,11 @@ async function handleResolve(args) {
130
131
  const dismissReviewIds = parseList(getFlag(extra, "--dismiss-review-ids"));
131
132
  const dismissMessage = getFlag(extra, "--message") ?? undefined;
132
133
  const requireSha = getFlag(extra, "--require-sha") ?? undefined;
134
+ warnPrrcThreadIds(resolveThreadIds);
135
+ if (!validateRequireSha(requireSha))
136
+ return;
137
+ if (rejectPrrcMinimizeIds(minimizeCommentIds).length > 0)
138
+ return;
133
139
  if (hasFlag(extra, "--fetch")) {
134
140
  process.stderr.write("pr-shepherd: resolve: --fetch has been removed; run pr-shepherd iterate or poll to fetch the next action.\n");
135
141
  process.exitCode = 1;
@@ -44,7 +44,6 @@ export function classifyReviewSummaries(summaries, approvals, minimizeApprovals,
44
44
  };
45
45
  }
46
46
  export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, _reviews, checks, prNumber, botUsernames = new Set()) {
47
- const argv = buildPrShepherdCommand(["resolve", String(prNumber)]).argv;
48
47
  const allThreads = [...threads, ...resolutionOnlyThreads];
49
48
  const replyThreadIds = dedupeIds(allThreads
50
49
  .filter((t) => isHumanAuthor(t) && !isConfiguredBotAuthor(t, botUsernames))
@@ -52,6 +51,41 @@ export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentId
52
51
  const resolveThreadIds = dedupeIds(allThreads
53
52
  .filter((t) => !isHumanAuthor(t) || isConfiguredBotAuthor(t, botUsernames))
54
53
  .map((t) => t.id));
54
+ const hasReply = replyThreadIds.length > 0;
55
+ const hasResolveOrMinimize = resolveThreadIds.length > 0 || allCommentIds.length > 0;
56
+ if (hasReply && hasResolveOrMinimize) {
57
+ // Split: reply command needs SHA; resolve/minimize command does not.
58
+ const resolveArgv = buildPrShepherdCommand(["resolve", String(prNumber)]).argv;
59
+ resolveArgv.push("--reply-thread-ids", replyThreadIds.join(","));
60
+ resolveArgv.push("--message", "$DISMISS_MESSAGE");
61
+ // `requiresHeadSha` is only true when actionable thread fixes or failing
62
+ // checks are being addressed — mutations that can race with a moving HEAD.
63
+ const requiresHeadSha = threads.length > 0 || checks.length > 0;
64
+ const resolveCommand = {
65
+ argv: resolveArgv,
66
+ requiresHeadSha,
67
+ requiresDismissMessage: true,
68
+ replyThreadIds,
69
+ hasMutations: true,
70
+ };
71
+ const resolveOnlyArgv = buildPrShepherdCommand(["resolve", String(prNumber)]).argv;
72
+ if (resolveThreadIds.length > 0) {
73
+ resolveOnlyArgv.push("--resolve-thread-ids", resolveThreadIds.join(","));
74
+ }
75
+ if (allCommentIds.length > 0) {
76
+ resolveOnlyArgv.push("--minimize-comment-ids", allCommentIds.join(","));
77
+ }
78
+ const resolveOnlyCommand = {
79
+ argv: resolveOnlyArgv,
80
+ requiresHeadSha: false,
81
+ requiresDismissMessage: false,
82
+ ...(resolveThreadIds.length > 0 ? { resolveThreadIds } : undefined),
83
+ hasMutations: true,
84
+ };
85
+ return { resolveCommand, resolveOnlyCommand };
86
+ }
87
+ // Single command: either reply-only or resolve/minimize-only (no split needed).
88
+ const argv = buildPrShepherdCommand(["resolve", String(prNumber)]).argv;
55
89
  if (replyThreadIds.length > 0) {
56
90
  argv.push("--reply-thread-ids", replyThreadIds.join(","));
57
91
  argv.push("--message", "$DISMISS_MESSAGE");
@@ -62,16 +96,11 @@ export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentId
62
96
  if (allCommentIds.length > 0) {
63
97
  argv.push("--minimize-comment-ids", allCommentIds.join(","));
64
98
  }
65
- // hasMutations = we appended at least one reply, resolve, or minimize mutation. Returned explicitly
66
- // (rather than derived from argv.length) so callers don't couple to the
67
- // base-argv shape.
68
- const hasMutations = replyThreadIds.length > 0 || resolveThreadIds.length > 0 || allCommentIds.length > 0;
69
- // `requiresHeadSha` is only added when this resolve command includes a
70
- // mutation that can race with a moving HEAD: replying after actionable
71
- // thread fixes or addressing failing checks.
72
- const hasCodeMutations = hasMutations && (threads.length > 0 || checks.length > 0);
73
- const requiresHeadSha = hasCodeMutations;
74
- return {
99
+ const hasMutations = hasReply || hasResolveOrMinimize;
100
+ // SHA is only required when replying after actionable fixes or failing checks.
101
+ // Resolve/minimize-only mutations never need SHA.
102
+ const requiresHeadSha = hasReply && (threads.length > 0 || checks.length > 0);
103
+ const resolveCommand = {
75
104
  argv,
76
105
  requiresHeadSha,
77
106
  requiresDismissMessage: replyThreadIds.length > 0,
@@ -79,4 +108,5 @@ export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentId
79
108
  ...(resolveThreadIds.length > 0 ? { resolveThreadIds } : undefined),
80
109
  hasMutations,
81
110
  };
111
+ return { resolveCommand };
82
112
  }
@@ -76,7 +76,7 @@ export async function handleFixCode(ctx) {
76
76
  const inProgressRunIds = pushLikely ? buildInProgressRunIds(report, cancelledSet) : [];
77
77
  const commentMinimizeIds = report.comments.minimizeIds ?? actionableComments.map((c) => c.id);
78
78
  const allCommentIds = [...commentMinimizeIds, ...reviewSummaryIds];
79
- const resolveCommand = buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, changesRequestedReviews, checks, prNumber, botUsernames);
79
+ const { resolveCommand, resolveOnlyCommand } = buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, changesRequestedReviews, checks, prNumber, botUsernames);
80
80
  // Safety: if the base branch is unknown, escalate when a push is plausible — the agent
81
81
  // would need the correct base to rebase safely. This is a conservative guard, not a
82
82
  // prediction that the agent *will* push. Intentionally broader than `pushLikely` above:
@@ -106,7 +106,7 @@ export async function handleFixCode(ctx) {
106
106
  }
107
107
  const firstLookThreads = report.threads.firstLook;
108
108
  const firstLookComments = report.comments.firstLook;
109
- const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prNumber, cancelled.length, firstLookThreads, firstLookComments, firstLookSummaries, editedSummaries, inProgressRunIds, resolutionOnlyThreads);
109
+ const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prNumber, cancelled.length, firstLookThreads, firstLookComments, firstLookSummaries, editedSummaries, inProgressRunIds, resolutionOnlyThreads, resolveOnlyCommand);
110
110
  const prospectiveResult = {
111
111
  ...base,
112
112
  baseBranch: baseLookup.branch,
@@ -122,6 +122,7 @@ export async function handleFixCode(ctx) {
122
122
  checks,
123
123
  changesRequestedReviews,
124
124
  resolveCommand,
125
+ ...(resolveOnlyCommand !== undefined ? { resolveOnlyCommand } : undefined),
125
126
  instructions,
126
127
  firstLookThreads,
127
128
  firstLookComments,
@@ -15,7 +15,7 @@ export function renderResolveCommand(rc) {
15
15
  }
16
16
  return renderShellCommand(parts);
17
17
  }
18
- export function buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseBranch, resolveCommand, hasConflicts, prNumber, cancelledCount, firstLookThreads = [], firstLookComments = [], firstLookSummaries = [], editedSummaries = [], inProgressRunIds = [], resolutionOnlyThreads = []) {
18
+ export function buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseBranch, resolveCommand, hasConflicts, prNumber, cancelledCount, firstLookThreads = [], firstLookComments = [], firstLookSummaries = [], editedSummaries = [], inProgressRunIds = [], resolutionOnlyThreads = [], resolveOnlyCommand) {
19
19
  const instructions = [];
20
20
  const hasNonConflictHints = threads.length > 0 ||
21
21
  checks.length > 0 ||
@@ -55,9 +55,8 @@ export function buildFixInstructions(threads, actionableComments, checks, change
55
55
  instructions.push(`If you decide to push new commits: cancel each in-progress run listed under \`## In-progress runs\` before applying code fixes (e.g. \`gh run cancel <id>\`). Runs may complete between the tick and your action; treat cancellation errors on already-finished runs as non-fatal. Skip this step if you are only resolving threads without pushing — the existing runs remain relevant.`);
56
56
  }
57
57
  const hasSuggestions = threads.some((t) => t.suggestion);
58
- if (hasSuggestions) {
58
+ if (hasSuggestions)
59
59
  instructions.push(buildCommitSuggestionInstruction(prNumber, "## Review threads", false));
60
- }
61
60
  if (threads.length > 0 || actionableComments.length > 0) {
62
61
  const suggestionFallback = hasSuggestions
63
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.`
@@ -74,9 +73,10 @@ export function buildFixInstructions(threads, actionableComments, checks, change
74
73
  if (changesRequestedReviews.length > 0) {
75
74
  instructions.push(`For each bullet under \`## Changes-requested reviews\` above: read the review body and apply the requested changes.`);
76
75
  }
77
- if (hasNonConflictHints) {
76
+ if (hasNonConflictHints)
78
77
  instructions.push(`If you applied code edits: commit them with a descriptive message, then rebase onto \`origin/${baseBranch}\` per your repository's conventions before pushing.`);
79
- }
78
+ if (resolveOnlyCommand?.hasMutations)
79
+ instructions.push(`Run the \`resolve-only:\` command shown above — no substitutions needed.`);
80
80
  if (resolveCommand.hasMutations) {
81
81
  if ((resolveCommand.replyThreadIds?.length ?? 0) > 0) {
82
82
  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.`);
@@ -98,9 +98,8 @@ export function buildFixInstructions(threads, actionableComments, checks, change
98
98
  if (firstLookTotal > 0) {
99
99
  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.`);
100
100
  }
101
- if (firstLookSummaries.length > 0) {
101
+ if (firstLookSummaries.length > 0)
102
102
  instructions.push(SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE);
103
- }
104
103
  const editedTotal = editedSummaries.length +
105
104
  actionableComments.filter((c) => c.edited).length +
106
105
  firstLookThreads.filter((t) => t.edited).length +
@@ -1,7 +1,7 @@
1
1
  export const SHEPHERD_JOURNAL_SECTION = "## Shepherd Journal";
2
2
  export const SHEPHERD_JOURNAL_SECTION_PATTERN = /^##\s+Shepherd\s+Journal$/;
3
3
  export const SHEPHERD_JOURNAL_APPEND_HINT = "If this section already exists, append your entries under it instead of creating a duplicate heading.";
4
- export const SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE = "Review the bodies shown under `## Review summaries (first look)` — you are seeing these for the first time. Eligible non-human IDs, when present, are already included in the resolve command's `--minimize-comment-ids`; if any warrants a Shepherd Journal note, append it before running resolve.";
4
+ export const SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE = "Review the bodies shown under `## Review summaries (first look)` — you are seeing these for the first time. Eligible non-human IDs, when present, are already included in `--minimize-comment-ids` in the resolve or resolve-only command above; if any warrants a Shepherd Journal note, append it before running resolve.";
5
5
  export function buildShepherdJournalInstruction(prNumber, itemReferenceGuidance) {
6
6
  return [
7
7
  `For any large decisions or rejections you made this iteration, add or update a \`${SHEPHERD_JOURNAL_SECTION}\` section in the PR description (\`gh pr edit ${prNumber} --body …\`) summarizing each decision.`,
@@ -8,6 +8,7 @@ export function classifyVisibleComments(comments, seenMap, minimizeComments, bot
8
8
  if (shouldMinimizeAuthor(c.authorType, minimizeComments, c.author, botUsernames)) {
9
9
  actionable.push(c);
10
10
  minimizeIds.push(c.id);
11
+ toMarkSeen.push(c); // prevents re-surfacing as first-look after GitHub marks it minimized
11
12
  continue;
12
13
  }
13
14
  const cls = classifyItem(c.id, c.body, seenMap);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.25.1",
3
+ "version": "0.25.3",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
5
5
  "license": "MIT",
6
6
  "author": "Jonathan Ong",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.25.1",
3
+ "version": "0.25.3",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for Codex.",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",