pr-shepherd 0.40.0 → 0.41.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.
@@ -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.40.0",
4
+ "version": "0.41.0",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
package/README.md CHANGED
@@ -25,7 +25,7 @@ Full reference: [docs/README.md](docs/README.md). Feature matrix: [docs/features
25
25
 
26
26
  `pr-shepherd` moves deterministic PR orchestration into a local MCP server, with a CLI for shells and CI. Both interfaces fetch the same GitHub state, emit raw-enough context, and return a numbered plan for the calling agent to follow.
27
27
 
28
- The MCP server exposes three tools: `iterate`, `apply`, and `build_suggestion_patch`. `apply` accepts ordered review mutations, file-view mutations, and journal entries. The shipped skills are thin dispatchers for those tools.
28
+ The MCP server exposes three tools: `iterate`, `apply`, and `build_suggestion_patch`. `apply` accepts ordered review mutations, file-view mutations, and journal entries. Direct MCP calls require a repository-qualified `pr`: a GitHub PR URL or `owner/repo#N`, matching the repository where the server started. The CLI and programmatic API also retain bare-number and current-branch PR discovery. The shipped skills are thin dispatchers for those tools.
29
29
 
30
30
  Each tick returns exactly one action:
31
31
 
@@ -115,7 +115,7 @@ Grok:
115
115
  /pr-shepherd 42
116
116
  ```
117
117
 
118
- MCP clients call `iterate` once per tick, then use `apply` for review/file/journal mutations and `build_suggestion_patch` for an anchored suggestion. `iterate` returns the same structured action data as the CLI, including its review mutation arguments. The client owns recurrence, so this works consistently in Codex, Claude Code, Grok, and any other stdio MCP client.
118
+ MCP clients call `iterate` once per tick, then use `apply` for review/file/journal mutations and `build_suggestion_patch` for an anchored suggestion. Every direct MCP call supplies the same repository-qualified PR reference. `iterate` returns the same structured action data as the CLI, including its review mutation arguments. The client owns recurrence, so this works consistently in Codex, Claude Code, Grok, and any other stdio MCP client.
119
119
 
120
120
  The CLI remains useful for shell workflows. Its canonical polling form is:
121
121
 
@@ -133,6 +133,24 @@ pr-shepherd iterate 42 # single tick
133
133
 
134
134
  Use `apply` with ordered operations to reply/resolve/minimize/dismiss review items, mark changed files viewed, or append an idempotent Shepherd Journal item. Use `build_suggestion_patch` to turn one review suggestion into a validated patch and commit metadata; it never changes the worktree or git history.
135
135
 
136
+ ### Extract Shepherd Journal Entries
137
+
138
+ The pure `pr-shepherd/journal` entry point can extract one validated journal without GitHub access:
139
+
140
+ ```ts
141
+ import { extractShepherdJournal } from "pr-shepherd/journal";
142
+
143
+ const result = extractShepherdJournal(prBody);
144
+ if (!result.ok) throw new Error(result.error);
145
+
146
+ for (const entry of result.journal?.entries ?? []) console.log(entry);
147
+ ```
148
+
149
+ The result identifies canonical `details` versus historical `legacy` H2 journals and returns each
150
+ complete Markdown list item with LF line endings. It fails closed for malformed or ambiguous
151
+ containers and ignores journal-shaped examples hidden in Markdown constructs. The full journal API,
152
+ including append and reconciliation helpers, is documented in [docs/api.md](docs/api.md).
153
+
136
154
  ### Clean Local State
137
155
 
138
156
  `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`).
package/bin/api.d.mts CHANGED
@@ -6,7 +6,7 @@ export interface CreatePrShepherdOptions {
6
6
  /** Working directory used for git, config, and classification-rule lookups. */
7
7
  cwd?: string;
8
8
  }
9
- /** A positive PR number or canonical GitHub pull-request URL. */
9
+ /** A positive PR number, GitHub pull-request URL, or owner/repo#number reference. */
10
10
  export type PrReference = number | string;
11
11
  export type IterateInput = Omit<IterateCommandOptions, "format" | "prNumber"> & {
12
12
  pr?: PrReference;
package/bin/api.mjs CHANGED
@@ -8,6 +8,7 @@ import { runMarkFilesAsViewed, } from "./commands/mark-files-as-viewed.mjs";
8
8
  import { runResolveMutate } from "./commands/resolve-mutate.mjs";
9
9
  import { runWithExecutionCwd } from "./execution-context.mjs";
10
10
  import { getRepoInfo } from "./github/client.mjs";
11
+ import { parsePrReference } from "./pr-reference.mjs";
11
12
  /** Raised before any API mutation when an input cannot be validated. */
12
13
  export class PrShepherdValidationError extends Error {
13
14
  constructor(message) {
@@ -108,7 +109,7 @@ function validateApplyInput(input) {
108
109
  }
109
110
  for (const operation of input.operations)
110
111
  validateOperation(operation);
111
- parsePrReference(input.pr);
112
+ validatePrReference(input.pr);
112
113
  }
113
114
  function validateOperation(operation) {
114
115
  if (!operation || typeof operation !== "object") {
@@ -192,38 +193,21 @@ function validateSuggestionPatchInput(input) {
192
193
  if (input.description !== undefined && typeof input.description !== "string") {
193
194
  throw new PrShepherdValidationError("buildSuggestionPatch.description must be a string");
194
195
  }
195
- parsePrReference(input.pr);
196
+ validatePrReference(input.pr);
196
197
  }
197
- function parsePrReference(pr) {
198
- if (pr === undefined)
199
- return {};
200
- if (typeof pr === "number" && Number.isInteger(pr) && pr > 0)
201
- return { number: pr };
202
- if (typeof pr === "string") {
203
- try {
204
- const url = new URL(pr);
205
- const parts = url.pathname.split("/").filter(Boolean);
206
- if ((url.protocol === "https:" || url.protocol === "http:") &&
207
- (url.hostname === "github.com" || url.hostname === "www.github.com") &&
208
- parts.length === 4 &&
209
- parts[2] === "pull" &&
210
- /^[1-9][0-9]*$/.test(parts[3])) {
211
- return { number: Number(parts[3]), repository: `${parts[0]}/${parts[1]}` };
212
- }
213
- }
214
- catch {
215
- // Construct the uniform public validation error below.
216
- }
217
- }
218
- throw new PrShepherdValidationError("pr must be a positive number or a GitHub pull-request URL");
198
+ function validatePrReference(pr) {
199
+ const parsed = parsePrReference(pr);
200
+ if (parsed !== null)
201
+ return parsed;
202
+ throw new PrShepherdValidationError("pr must be a positive number, a GitHub pull-request URL, or owner/repo#number");
219
203
  }
220
204
  async function resolvePrReference(pr) {
221
- const parsed = parsePrReference(pr);
205
+ const parsed = validatePrReference(pr);
222
206
  if (parsed.repository !== undefined) {
223
207
  const repo = await getRepoInfo();
224
208
  const currentRepository = `${repo.owner}/${repo.name}`;
225
209
  if (parsed.repository.toLowerCase() !== currentRepository.toLowerCase()) {
226
- throw new PrShepherdValidationError(`PR URL repository ${parsed.repository} does not match the configured repository ${currentRepository}`);
210
+ throw new PrShepherdValidationError(`PR reference repository ${parsed.repository} does not match the configured repository ${currentRepository}`);
227
211
  }
228
212
  }
229
213
  return parsed.number;
@@ -0,0 +1,16 @@
1
+ /** Result of extracting the single visible structural Shepherd Journal from Markdown. */
2
+ export type ShepherdJournalExtraction = {
3
+ journal: null;
4
+ ok: true;
5
+ } | {
6
+ journal: {
7
+ entries: string[];
8
+ format: "details" | "legacy";
9
+ };
10
+ ok: true;
11
+ } | {
12
+ error: string;
13
+ ok: false;
14
+ };
15
+ /** Extract ordered journal entries while failing closed on malformed or ambiguous content. */
16
+ export declare function extractShepherdJournal(body: string): ShepherdJournalExtraction;
@@ -0,0 +1,71 @@
1
+ import { parseShepherdJournalEntries, scanShepherdJournal } from "./reconcile.mjs";
2
+ import { isMarkdownBlockStart, scanMarkdownLines } from "./markdown-line.mjs";
3
+ import { stripMarkdownContainer } from "./markdown-container.mjs";
4
+ import { isIndentedCode } from "./markdown-structure.mjs";
5
+ function hasUnrecognizedJournalContent(lines) {
6
+ const syntax = scanMarkdownLines(lines);
7
+ let foundEntry = false;
8
+ let lazyContinuation = false;
9
+ let nestedBlock = false;
10
+ for (const [index, line] of lines.entries()) {
11
+ const scanned = syntax[index];
12
+ if (scanned.visiblePrefix.startsWith("- ") && /^- \S/.test(line)) {
13
+ foundEntry = true;
14
+ lazyContinuation = true;
15
+ nestedBlock = false;
16
+ continue;
17
+ }
18
+ if (line.trim() === "") {
19
+ if (foundEntry)
20
+ lazyContinuation = false;
21
+ continue;
22
+ }
23
+ if (!foundEntry)
24
+ return true;
25
+ const content = stripMarkdownContainer(line, [{ kind: "list", width: 2 }]);
26
+ if (content === null) {
27
+ if (!lazyContinuation || isMarkdownBlockStart(line))
28
+ return true;
29
+ continue;
30
+ }
31
+ if (scanned.ignored || isMarkdownBlockStart(content) || isIndentedCode(content)) {
32
+ lazyContinuation = false;
33
+ nestedBlock = true;
34
+ continue;
35
+ }
36
+ if (nestedBlock && /^[ \t]/.test(content)) {
37
+ lazyContinuation = false;
38
+ continue;
39
+ }
40
+ lazyContinuation = true;
41
+ nestedBlock = false;
42
+ }
43
+ return false;
44
+ }
45
+ /** Extract ordered journal entries while failing closed on malformed or ambiguous content. */
46
+ export function extractShepherdJournal(body) {
47
+ const lines = body.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
48
+ const journal = scanShepherdJournal(lines);
49
+ if (journal === "error") {
50
+ return {
51
+ error: "malformed, duplicate, or ambiguous Shepherd Journal container",
52
+ ok: false,
53
+ };
54
+ }
55
+ if (!journal)
56
+ return { journal: null, ok: true };
57
+ const content = lines.slice(journal.contentStart, journal.contentEnd);
58
+ if (hasUnrecognizedJournalContent(content)) {
59
+ return {
60
+ error: "Shepherd Journal content uses an unrecognized entry format",
61
+ ok: false,
62
+ };
63
+ }
64
+ return {
65
+ journal: {
66
+ entries: parseShepherdJournalEntries(content).map((entry) => entry.join("\n")),
67
+ format: journal.format,
68
+ },
69
+ ok: true,
70
+ };
71
+ }
@@ -1,3 +1,4 @@
1
1
  /** GitHub-free Shepherd Journal helpers for programmatic PR-body reconciliation. */
2
2
  export { appendJournalItem, validateJournalItem, type AppendResult } from "./append.mts";
3
+ export { extractShepherdJournal, type ShepherdJournalExtraction } from "./extract.mts";
3
4
  export { reconcileShepherdJournal, type ShepherdJournalReconcileResult } from "./reconcile.mts";
@@ -1,3 +1,4 @@
1
1
  /** GitHub-free Shepherd Journal helpers for programmatic PR-body reconciliation. */
2
2
  export { appendJournalItem, validateJournalItem } from "./append.mjs";
3
+ export { extractShepherdJournal } from "./extract.mjs";
3
4
  export { reconcileShepherdJournal } from "./reconcile.mjs";
@@ -3,6 +3,8 @@ type MarkdownLine = {
3
3
  nested: boolean;
4
4
  visiblePrefix: string;
5
5
  };
6
+ /** Return whether a Markdown line starts a block that interrupts inline content. */
7
+ export declare function isMarkdownBlockStart(line: string): boolean;
6
8
  export declare const scanMarkdownLines: (lines: string[]) => MarkdownLine[];
7
9
  export declare const isSafeMarkdownInsertionPoint: (lines: string[]) => boolean;
8
10
  export {};
@@ -2,18 +2,20 @@ import { fenceStart, resolveMarkdownContainer, stripMarkdownContainer, } from ".
2
2
  import { inQuotedHtmlAttribute, rawHtmlEnd, rawHtmlStart, } from "./markdown-html.mjs";
3
3
  import { backtickRuns, nextBacktickRun, nextCodeOpener } from "./markdown-backticks.mjs";
4
4
  import { isIndentedCode, structuralDetailsStart } from "./markdown-structure.mjs";
5
- function interruptsInlineContent(line) {
5
+ /** Return whether a Markdown line starts a block that interrupts inline content. */
6
+ export function isMarkdownBlockStart(line) {
6
7
  return (line.trim() === "" ||
7
8
  /^ {0,3}(?:#{1,6}(?:[ \t]+|$)|>)/.test(line) ||
8
9
  /^ {0,3}(?:[-+*]|\d{1,9}[.)])[ \t]+/.test(line) ||
9
10
  /^ {0,3}(?:(?:\*[ \t]*){3,}|-+[ \t]*|(?:_[ \t]*){3,}|=+[ \t]*)$/.test(line) ||
11
+ /^ {0,3}<!--/.test(line) ||
10
12
  fenceStart(line) !== null ||
11
13
  rawHtmlStart(line) !== null ||
12
14
  structuralDetailsStart(line) !== null);
13
15
  }
14
16
  function hasCloser(lines, lineIndex, offset, length) {
15
17
  for (let i = lineIndex; i < lines.length; i++) {
16
- if (i > lineIndex && interruptsInlineContent(lines[i]))
18
+ if (i > lineIndex && isMarkdownBlockStart(lines[i]))
17
19
  return false;
18
20
  if (backtickRuns(lines[i]).some((run) => run.length === length && (i > lineIndex || run.index > offset)))
19
21
  return true;
@@ -13,5 +13,6 @@ export type ShepherdJournalBounds = {
13
13
  start: number;
14
14
  };
15
15
  export declare function scanShepherdJournal(lines: string[]): ShepherdJournalBounds | null | "error";
16
+ export declare function parseShepherdJournalEntries(lines: string[]): string[][];
16
17
  export declare function containsJournalEntry(lines: string[], item: string): boolean;
17
18
  export declare function reconcileShepherdJournal(suppliedBody: string, liveBody: string): ShepherdJournalReconcileResult;
@@ -61,7 +61,10 @@ export function scanShepherdJournal(lines) {
61
61
  return "error";
62
62
  }
63
63
  if (JOURNAL_SUMMARY.test(visible.trimStart())) {
64
- if (lines[i] !== SUMMARY || lines[i - 1] !== OPEN || lines[i + 1] !== "")
64
+ if (detailsDepth !== 1 ||
65
+ lines[i] !== SUMMARY ||
66
+ lines[i - 1] !== OPEN ||
67
+ lines[i + 1] !== "")
65
68
  return "error";
66
69
  const end = close(lines, syntax, i + 2);
67
70
  if (end === null)
@@ -78,7 +81,7 @@ export function scanShepherdJournal(lines) {
78
81
  continue;
79
82
  }
80
83
  if (LEGACY.test(visible)) {
81
- if (legacy)
84
+ if (detailsDepth !== 0 || legacy)
82
85
  return "error";
83
86
  legacy = {
84
87
  contentEnd: lines.length,
@@ -108,7 +111,7 @@ function trim(lines) {
108
111
  b--;
109
112
  return lines.slice(a, b);
110
113
  }
111
- function entries(lines) {
114
+ export function parseShepherdJournalEntries(lines) {
112
115
  const result = [];
113
116
  const syntax = scanMarkdownLines(lines.map((line) => (line.endsWith("\r") ? line.slice(0, -1) : line)));
114
117
  let current = null;
@@ -125,7 +128,7 @@ function entries(lines) {
125
128
  result.push(trim(current));
126
129
  return result;
127
130
  }
128
- function hasUnrecognizedLeadingContent(lines) {
131
+ function hasUnrecognizedLeadingJournalContent(lines) {
129
132
  const syntax = scanMarkdownLines(lines.map((line) => (line.endsWith("\r") ? line.slice(0, -1) : line)));
130
133
  const firstEntry = syntax.findIndex((line) => line.visiblePrefix.startsWith("- "));
131
134
  if (firstEntry === -1)
@@ -137,7 +140,7 @@ function contains(a, b) {
137
140
  }
138
141
  export function containsJournalEntry(lines, item) {
139
142
  const target = item.split("\n");
140
- return entries(lines).some((entry) => contains(target, entry));
143
+ return parseShepherdJournalEntries(lines).some((entry) => contains(target, entry));
141
144
  }
142
145
  function fail(reason) {
143
146
  return {
@@ -169,11 +172,11 @@ export function reconcileShepherdJournal(suppliedBody, liveBody) {
169
172
  ok: true,
170
173
  };
171
174
  }
172
- const liveEntries = entries(liveLines.slice(live.contentStart, live.contentEnd));
173
175
  const liveJournalLines = liveLines.slice(live.contentStart, live.contentEnd);
174
- if (!liveEntries.length || hasUnrecognizedLeadingContent(liveJournalLines))
176
+ const liveEntries = parseShepherdJournalEntries(liveJournalLines);
177
+ if (!liveEntries.length || hasUnrecognizedLeadingJournalContent(liveJournalLines))
175
178
  return fail("live Shepherd Journal content uses an unrecognized entry format");
176
- const target = entries(suppliedLines.slice(supplied.contentStart, supplied.contentEnd));
179
+ const target = parseShepherdJournalEntries(suppliedLines.slice(supplied.contentStart, supplied.contentEnd));
177
180
  for (const entry of liveEntries) {
178
181
  const match = target.findIndex((candidate) => contains(entry, candidate));
179
182
  if (match === -1)
@@ -3,10 +3,15 @@ import { readFileSync } from "node:fs";
3
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import { z } from "zod";
5
5
  import { createPrShepherd, PartialApplyError, PrShepherdValidationError, } from "../api.mjs";
6
+ import { isRepositoryQualifiedPrReference } from "../pr-reference.mjs";
6
7
  import { formatJournalResult } from "../cli/journal-formatter.mjs";
7
8
  import { formatCommitSuggestionResult, formatIterateResult, formatMarkFilesAsViewedResult, formatMutateResult, } from "../cli/formatters.mjs";
8
9
  import { errorToExitCode, EXIT } from "../exit-codes.mjs";
9
- const pr = z.union([z.number().int().positive(), z.string().url()]).optional();
10
+ const QUALIFIED_PR_ERROR = "pr must be a GitHub pull-request URL or an owner/repo#number reference";
11
+ const pr = z
12
+ .string()
13
+ .refine(isRepositoryQualifiedPrReference, { message: QUALIFIED_PR_ERROR })
14
+ .describe("GitHub pull-request URL or owner/repo#number");
10
15
  const ids = z.array(z.string().min(1)).optional();
11
16
  const iterateInputSchema = z.object({
12
17
  pr,
@@ -65,7 +70,7 @@ export function createPrShepherdMcpServer(options = {}) {
65
70
  idempotentHint: false,
66
71
  openWorldHint: true,
67
72
  },
68
- }, async (input) => runTool(() => shepherd.iterate(input), formatIterateResult));
73
+ }, async (input) => runTool(() => shepherd.iterate(requireRepositoryQualifiedPr(input)), formatIterateResult));
69
74
  server.registerTool("apply", {
70
75
  description: "Apply ordered review, file-view, and journal operations after prevalidation.",
71
76
  inputSchema: applyInputSchema,
@@ -75,7 +80,7 @@ export function createPrShepherdMcpServer(options = {}) {
75
80
  idempotentHint: false,
76
81
  openWorldHint: true,
77
82
  },
78
- }, async (input) => runTool(() => shepherd.apply(input), formatApplyResult));
83
+ }, async (input) => runTool(() => shepherd.apply(requireRepositoryQualifiedPr(input)), formatApplyResult));
79
84
  server.registerTool("build_suggestion_patch", {
80
85
  description: "Build, but never apply, a patch from an eligible review suggestion.",
81
86
  inputSchema: suggestionPatchInputSchema,
@@ -85,9 +90,15 @@ export function createPrShepherdMcpServer(options = {}) {
85
90
  idempotentHint: true,
86
91
  openWorldHint: true,
87
92
  },
88
- }, async (input) => runTool(() => shepherd.buildSuggestionPatch(input), formatCommitSuggestionResult));
93
+ }, async (input) => runTool(() => shepherd.buildSuggestionPatch(requireRepositoryQualifiedPr(input)), formatCommitSuggestionResult));
89
94
  return server;
90
95
  }
96
+ function requireRepositoryQualifiedPr(input) {
97
+ if (!isRepositoryQualifiedPrReference(input.pr)) {
98
+ throw new PrShepherdValidationError(QUALIFIED_PR_ERROR);
99
+ }
100
+ return input;
101
+ }
91
102
  function readPackageVersion() {
92
103
  const packageJson = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
93
104
  return packageJson.version;
@@ -0,0 +1,7 @@
1
+ export interface ParsedPrReference {
2
+ number?: number;
3
+ repository?: string;
4
+ }
5
+ /** Parses API PR references without performing repository or GitHub I/O. */
6
+ export declare function parsePrReference(pr: number | string | undefined): ParsedPrReference | null;
7
+ export declare function isRepositoryQualifiedPrReference(pr: unknown): pr is string;
@@ -0,0 +1,36 @@
1
+ /** Parses API PR references without performing repository or GitHub I/O. */
2
+ export function parsePrReference(pr) {
3
+ if (pr === undefined)
4
+ return {};
5
+ if (typeof pr === "number" && Number.isInteger(pr) && pr > 0)
6
+ return { number: pr };
7
+ if (typeof pr !== "string")
8
+ return null;
9
+ const shorthand = /^([^/#\s]+)\/([^/#\s]+)#([1-9][0-9]*)$/.exec(pr);
10
+ if (shorthand) {
11
+ return {
12
+ number: Number(shorthand[3]),
13
+ repository: `${shorthand[1]}/${shorthand[2]}`,
14
+ };
15
+ }
16
+ try {
17
+ const url = new URL(pr);
18
+ const parts = url.pathname.split("/").filter(Boolean);
19
+ if ((url.protocol === "https:" || url.protocol === "http:") &&
20
+ (url.hostname === "github.com" || url.hostname === "www.github.com") &&
21
+ parts.length === 4 &&
22
+ parts[2] === "pull" &&
23
+ /^[1-9][0-9]*$/.test(parts[3])) {
24
+ return { number: Number(parts[3]), repository: `${parts[0]}/${parts[1]}` };
25
+ }
26
+ }
27
+ catch {
28
+ // Return the uniform invalid result below.
29
+ }
30
+ return null;
31
+ }
32
+ export function isRepositoryQualifiedPrReference(pr) {
33
+ if (typeof pr !== "string")
34
+ return false;
35
+ return parsePrReference(pr)?.repository !== undefined;
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
5
5
  "keywords": [
6
6
  "automation",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for Codex.",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "pr-shepherd": {
4
4
  "command": "npx",
5
- "args": ["--yes", "--package", "pr-shepherd@0.40.0", "pr-shepherd-mcp"]
5
+ "args": ["--yes", "--package", "pr-shepherd@0.41.0", "pr-shepherd-mcp"]
6
6
  }
7
7
  }
8
8
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "pr-shepherd": {
3
3
  "command": "npx",
4
- "args": ["--yes", "--package", "pr-shepherd@0.40.0", "pr-shepherd-mcp"]
4
+ "args": ["--yes", "--package", "pr-shepherd@0.41.0", "pr-shepherd-mcp"]
5
5
  }
6
6
  }
@@ -12,6 +12,6 @@ Thin dispatcher for marking PR files viewed. Use the MCP server when it is avail
12
12
 
13
13
  ## Arguments: $ARGUMENTS
14
14
 
15
- 1. Parse an optional PR number or GitHub PR URL. Treat standalone `tests` as `--tests`; preserve explicit paths and `--match <regex>` selectors.
15
+ 1. Parse an optional PR number, repository-qualified `owner/repo#N`, or GitHub PR URL. Treat standalone `tests` as `--tests`; preserve explicit paths and `--match <regex>` selectors.
16
16
 
17
- 2. If the `apply` MCP tool is available, call it with one `mark_files_viewed` operation, then print the full result. Otherwise run `pr-shepherd apply files` with the parsed PR and selectors and print the full result.
17
+ 2. If the `apply` MCP tool is available, first obtain a repository-qualified reference: use a supplied GitHub PR URL or `owner/repo#N` unchanged; for a bare number, run `gh pr view <number> --json url --jq .url`; when omitted, run `gh pr view --json url --jq .url`. If that does not produce one qualified PR reference, stop and report that MCP cannot safely determine the PR. Otherwise call `apply` with that qualified reference and one `mark_files_viewed` operation, then print the full result. If MCP is unavailable and the parsed PR is repository-qualified, run `gh repo view --json nameWithOwner --jq .nameWithOwner` and verify that repository matches the reference case-insensitively. Stop on a mismatch or failed lookup; the CLI does not validate the URL repository. Convert a verified `owner/repo#N` to `https://github.com/owner/repo/pull/N`, then run `pr-shepherd apply files` with the parsed PR and selectors and print the full result.
@@ -12,11 +12,11 @@ Thin dispatcher for iterating a PR. Poll with the CLI; use MCP `iterate` only wh
12
12
 
13
13
  ## Arguments: $ARGUMENTS
14
14
 
15
- 1. Parse an optional PR number or GitHub PR URL from `$ARGUMENTS`; otherwise let pr-shepherd infer the current branch PR.
15
+ 1. Parse an optional PR number, repository-qualified `owner/repo#N`, or GitHub PR URL from `$ARGUMENTS`; otherwise let pr-shepherd infer the current branch PR.
16
16
 
17
- 2. Run the poll command `pr-shepherd` with the optional PR argument and print its full result. Do not run `pr-shepherd iterate`. If the CLI is unavailable and the `iterate` MCP tool is available, call `iterate` and print its full result.
17
+ 2. Before passing a supplied GitHub PR URL or `owner/repo#N` to the CLI, run `gh repo view --json nameWithOwner --jq .nameWithOwner` and verify that repository matches the reference case-insensitively. Stop on a mismatch or failed lookup; the CLI does not validate the URL repository. Convert a verified `owner/repo#N` to `https://github.com/owner/repo/pull/N`, then run the poll command `pr-shepherd` with the optional PR argument and print its full result. Do not run `pr-shepherd iterate`. If the CLI is unavailable and the `iterate` MCP tool is available, first obtain a repository-qualified reference: use a supplied GitHub PR URL or `owner/repo#N` unchanged; for a bare number, run `gh pr view <number> --json url --jq .url`; when omitted, run `gh pr view --json url --jq .url`. If that does not produce one qualified PR reference, stop and report that MCP cannot safely determine the PR. Otherwise call `iterate` with that qualified reference and print its full result.
18
18
 
19
- 3. Print the full result and follow every returned `## Instructions` step exactly. For CLI output, run each printed mutation command when instructed. For MCP output, use MCP `apply` and `build_suggestion_patch`; do not run a shell `pr-shepherd apply` command.
19
+ 3. Print the full result and follow every returned `## Instructions` step exactly. For CLI output, run each printed mutation command when instructed. For MCP output, use MCP `apply` and `build_suggestion_patch` with the same qualified PR reference; do not run a shell `pr-shepherd apply` command.
20
20
 
21
21
  4. After completing the returned instructions, repeat step 2 unless the action is `[CANCEL]` or `[ESCALATE]`, the instructions require a human handoff, or the human directs you to stop.
22
22