pr-shepherd 0.40.0 → 0.42.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +25 -7
- package/bin/api.d.mts +13 -2
- package/bin/api.mjs +41 -24
- package/bin/cli/formatters.d.mts +2 -1
- package/bin/cli/formatters.mjs +33 -0
- package/bin/cli/handlers.d.mts +1 -0
- package/bin/cli/handlers.mjs +20 -1
- package/bin/cli/help-command-pages.d.mts +20 -4
- package/bin/cli/help-command-pages.mjs +20 -4
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +3 -3
- package/bin/cli/help.d.mts +21 -5
- package/bin/cli/suggestion-patch-flags.d.mts +11 -0
- package/bin/cli/suggestion-patch-flags.mjs +83 -0
- package/bin/cli-parser.mjs +6 -2
- package/bin/commands/commit-suggestion-instruction.d.mts +1 -1
- package/bin/commands/commit-suggestion-instruction.mjs +3 -3
- package/bin/commands/commit-suggestion.d.mts +1 -1
- package/bin/commands/commit-suggestion.mjs +19 -138
- package/bin/commands/suggestion-patch-git.d.mts +6 -0
- package/bin/commands/suggestion-patch-git.mjs +54 -0
- package/bin/commands/suggestion-patch-item.d.mts +15 -0
- package/bin/commands/suggestion-patch-item.mjs +106 -0
- package/bin/commands/suggestion-patches.d.mts +6 -0
- package/bin/commands/suggestion-patches.mjs +108 -0
- package/bin/github/gql/{commit-suggestion-thread.gql → suggestion-threads.gql} +2 -2
- package/bin/github/queries.d.mts +1 -1
- package/bin/github/queries.mjs +1 -1
- package/bin/github/suggestion-thread.d.mts +3 -3
- package/bin/github/suggestion-thread.mjs +8 -5
- package/bin/journal/extract.d.mts +16 -0
- package/bin/journal/extract.mjs +71 -0
- package/bin/journal/index.d.mts +1 -0
- package/bin/journal/index.mjs +1 -0
- package/bin/journal/markdown-line.d.mts +2 -0
- package/bin/journal/markdown-line.mjs +4 -2
- package/bin/journal/reconcile.d.mts +1 -0
- package/bin/journal/reconcile.mjs +11 -8
- package/bin/mcp/server.d.mts +1 -1
- package/bin/mcp/server.mjs +38 -7
- package/bin/pr-reference.d.mts +7 -0
- package/bin/pr-reference.mjs +36 -0
- package/bin/types/report.d.mts +0 -19
- package/bin/types/suggestion-patch.d.mts +30 -0
- package/bin/types/suggestion-patch.mjs +1 -0
- package/bin/types.d.mts +1 -0
- package/bin/types.mjs +1 -0
- package/package.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- package/plugins/pr-shepherd/.mcp.json +1 -1
- package/plugins/pr-shepherd/skills/mark-files-as-viewed/SKILL.md +2 -2
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +10 -9
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { ReviewThread } from "../types.mts";
|
|
2
2
|
import type { RepoInfo } from "./client.mts";
|
|
3
|
-
export interface
|
|
3
|
+
export interface SuggestionThreadsResult {
|
|
4
4
|
headRefOid: string;
|
|
5
5
|
headRefName: string;
|
|
6
6
|
headRepoWithOwner: string | null;
|
|
7
|
-
|
|
7
|
+
threads: (ReviewThread | null)[];
|
|
8
8
|
}
|
|
9
|
-
export declare function
|
|
9
|
+
export declare function fetchSuggestionThreads(pr: number, repo: RepoInfo, threadIds: readonly string[]): Promise<SuggestionThreadsResult>;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { graphql } from "./client.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { SUGGESTION_THREADS_QUERY } from "./queries.mjs";
|
|
3
3
|
import { mapAuthorType, parseCreatedAt } from "./batch-parser-helpers.mjs";
|
|
4
|
-
export async function
|
|
5
|
-
const result = await graphql(
|
|
4
|
+
export async function fetchSuggestionThreads(pr, repo, threadIds) {
|
|
5
|
+
const result = await graphql(SUGGESTION_THREADS_QUERY, {
|
|
6
6
|
owner: repo.owner,
|
|
7
7
|
repo: repo.name,
|
|
8
8
|
pr,
|
|
9
|
-
|
|
9
|
+
threadIds,
|
|
10
10
|
});
|
|
11
11
|
const pull = result.data.repository.pullRequest;
|
|
12
12
|
if (!pull) {
|
|
@@ -16,7 +16,10 @@ export async function fetchSuggestionThread(pr, repo, threadId) {
|
|
|
16
16
|
headRefOid: pull.headRefOid,
|
|
17
17
|
headRefName: pull.headRefName,
|
|
18
18
|
headRepoWithOwner: pull.headRepository?.nameWithOwner ?? null,
|
|
19
|
-
|
|
19
|
+
threads: threadIds.map((threadId) => {
|
|
20
|
+
const raw = result.data.nodes.find((candidate) => candidate?.id === threadId) ?? null;
|
|
21
|
+
return parseThread(raw, pr, threadId);
|
|
22
|
+
}),
|
|
20
23
|
};
|
|
21
24
|
}
|
|
22
25
|
function parseThread(raw, pr, threadId) {
|
|
@@ -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
|
+
}
|
package/bin/journal/index.d.mts
CHANGED
|
@@ -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";
|
package/bin/journal/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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 &&
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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)
|
package/bin/mcp/server.d.mts
CHANGED
|
@@ -4,5 +4,5 @@ export interface CreatePrShepherdMcpServerOptions extends CreatePrShepherdOption
|
|
|
4
4
|
/** Optional injection point for embedding hosts and focused tests. */
|
|
5
5
|
shepherd?: PrShepherd;
|
|
6
6
|
}
|
|
7
|
-
/** Creates a local-only MCP server with
|
|
7
|
+
/** Creates a local-only MCP server with Shepherd's public operations. */
|
|
8
8
|
export declare function createPrShepherdMcpServer(options?: CreatePrShepherdMcpServerOptions): McpServer;
|
package/bin/mcp/server.mjs
CHANGED
|
@@ -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
|
-
import { formatCommitSuggestionResult, formatIterateResult, formatMarkFilesAsViewedResult, formatMutateResult, } from "../cli/formatters.mjs";
|
|
8
|
+
import { formatCommitSuggestionResult, formatSuggestionPatchesResult, formatIterateResult, formatMarkFilesAsViewedResult, formatMutateResult, } from "../cli/formatters.mjs";
|
|
8
9
|
import { errorToExitCode, EXIT } from "../exit-codes.mjs";
|
|
9
|
-
const
|
|
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,
|
|
@@ -52,7 +57,17 @@ const suggestionPatchInputSchema = z.object({
|
|
|
52
57
|
message: z.string().min(1),
|
|
53
58
|
description: z.string().optional(),
|
|
54
59
|
});
|
|
55
|
-
|
|
60
|
+
const suggestionPatchesInputSchema = z.object({
|
|
61
|
+
pr,
|
|
62
|
+
suggestions: z
|
|
63
|
+
.array(z.object({
|
|
64
|
+
threadId: z.string().min(1),
|
|
65
|
+
message: z.string().min(1),
|
|
66
|
+
description: z.string().optional(),
|
|
67
|
+
}))
|
|
68
|
+
.min(1),
|
|
69
|
+
});
|
|
70
|
+
/** Creates a local-only MCP server with Shepherd's public operations. */
|
|
56
71
|
export function createPrShepherdMcpServer(options = {}) {
|
|
57
72
|
const shepherd = options.shepherd ?? createPrShepherd({ cwd: options.cwd });
|
|
58
73
|
const server = new McpServer({ name: "pr-shepherd", version: readPackageVersion() });
|
|
@@ -65,7 +80,7 @@ export function createPrShepherdMcpServer(options = {}) {
|
|
|
65
80
|
idempotentHint: false,
|
|
66
81
|
openWorldHint: true,
|
|
67
82
|
},
|
|
68
|
-
}, async (input) => runTool(() => shepherd.iterate(input), formatIterateResult));
|
|
83
|
+
}, async (input) => runTool(() => shepherd.iterate(requireRepositoryQualifiedPr(input)), formatIterateResult));
|
|
69
84
|
server.registerTool("apply", {
|
|
70
85
|
description: "Apply ordered review, file-view, and journal operations after prevalidation.",
|
|
71
86
|
inputSchema: applyInputSchema,
|
|
@@ -75,9 +90,19 @@ export function createPrShepherdMcpServer(options = {}) {
|
|
|
75
90
|
idempotentHint: false,
|
|
76
91
|
openWorldHint: true,
|
|
77
92
|
},
|
|
78
|
-
}, async (input) => runTool(() => shepherd.apply(input), formatApplyResult));
|
|
93
|
+
}, async (input) => runTool(() => shepherd.apply(requireRepositoryQualifiedPr(input)), formatApplyResult));
|
|
94
|
+
server.registerTool("build_suggestion_patches", {
|
|
95
|
+
description: "Build, but never apply, an ordered list of eligible review suggestion patches.",
|
|
96
|
+
inputSchema: suggestionPatchesInputSchema,
|
|
97
|
+
annotations: {
|
|
98
|
+
readOnlyHint: true,
|
|
99
|
+
destructiveHint: false,
|
|
100
|
+
idempotentHint: true,
|
|
101
|
+
openWorldHint: true,
|
|
102
|
+
},
|
|
103
|
+
}, async (input) => runTool(() => shepherd.buildSuggestionPatches(requireRepositoryQualifiedPr(input)), formatSuggestionPatchesResult));
|
|
79
104
|
server.registerTool("build_suggestion_patch", {
|
|
80
|
-
description: "
|
|
105
|
+
description: "Deprecated: use build_suggestion_patches with a one-item suggestions array.",
|
|
81
106
|
inputSchema: suggestionPatchInputSchema,
|
|
82
107
|
annotations: {
|
|
83
108
|
readOnlyHint: true,
|
|
@@ -85,9 +110,15 @@ export function createPrShepherdMcpServer(options = {}) {
|
|
|
85
110
|
idempotentHint: true,
|
|
86
111
|
openWorldHint: true,
|
|
87
112
|
},
|
|
88
|
-
}, async (input) => runTool(() => shepherd.buildSuggestionPatch(input), formatCommitSuggestionResult));
|
|
113
|
+
}, async (input) => runTool(() => shepherd.buildSuggestionPatch(requireRepositoryQualifiedPr(input)), formatCommitSuggestionResult));
|
|
89
114
|
return server;
|
|
90
115
|
}
|
|
116
|
+
function requireRepositoryQualifiedPr(input) {
|
|
117
|
+
if (!isRepositoryQualifiedPrReference(input.pr)) {
|
|
118
|
+
throw new PrShepherdValidationError(QUALIFIED_PR_ERROR);
|
|
119
|
+
}
|
|
120
|
+
return input;
|
|
121
|
+
}
|
|
91
122
|
function readPackageVersion() {
|
|
92
123
|
const packageJson = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
93
124
|
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/bin/types/report.d.mts
CHANGED
|
@@ -150,25 +150,6 @@ export interface RelevantCheck {
|
|
|
150
150
|
/** Marker-gated inline annotations from this check. */
|
|
151
151
|
annotations?: CheckAnnotation[];
|
|
152
152
|
}
|
|
153
|
-
export interface CommitSuggestionResult {
|
|
154
|
-
pr: number;
|
|
155
|
-
repo: string;
|
|
156
|
-
threadId: string;
|
|
157
|
-
path: string;
|
|
158
|
-
startLine: number;
|
|
159
|
-
endLine: number;
|
|
160
|
-
author: string;
|
|
161
|
-
/** The unified diff generated for this suggestion. */
|
|
162
|
-
patch: string;
|
|
163
|
-
/** The commit subject line (user-supplied --message). */
|
|
164
|
-
commitMessage: string;
|
|
165
|
-
/** The commit body (optional description + Co-authored-by trailer). */
|
|
166
|
-
commitBody: string;
|
|
167
|
-
/** Files the agent should stage before committing. */
|
|
168
|
-
filesToStage: string[];
|
|
169
|
-
/** Numbered steps the agent must execute to apply, commit, resolve, and push. */
|
|
170
|
-
postActionInstructions: string[];
|
|
171
|
-
}
|
|
172
153
|
export interface GlobalOptions {
|
|
173
154
|
prNumber?: number;
|
|
174
155
|
format: "text" | "json";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface SuggestionPatchResult {
|
|
2
|
+
threadId: string;
|
|
3
|
+
path: string;
|
|
4
|
+
startLine: number;
|
|
5
|
+
endLine: number;
|
|
6
|
+
author: string;
|
|
7
|
+
/** The unified diff generated for this suggestion. */
|
|
8
|
+
patch: string;
|
|
9
|
+
/** The commit subject line supplied by the caller. */
|
|
10
|
+
commitMessage: string;
|
|
11
|
+
/** The optional description plus Co-authored-by trailer. */
|
|
12
|
+
commitBody: string;
|
|
13
|
+
/** Files the agent should stage before committing. */
|
|
14
|
+
filesToStage: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface BuildSuggestionPatchesResult {
|
|
17
|
+
pr: number;
|
|
18
|
+
repo: string;
|
|
19
|
+
/** Suggestion patches in caller-supplied application order. */
|
|
20
|
+
patches: SuggestionPatchResult[];
|
|
21
|
+
/** Steps for applying and committing every patch, then pushing once. */
|
|
22
|
+
postActionInstructions: string[];
|
|
23
|
+
}
|
|
24
|
+
/** Compatibility result for the singular suggestion-patch adapter. */
|
|
25
|
+
export interface CommitSuggestionResult extends SuggestionPatchResult {
|
|
26
|
+
pr: number;
|
|
27
|
+
repo: string;
|
|
28
|
+
/** Numbered steps for the deprecated singular workflow. */
|
|
29
|
+
postActionInstructions: string[];
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/bin/types.d.mts
CHANGED
|
@@ -6,5 +6,6 @@ export * from "./types/review-thread.mts";
|
|
|
6
6
|
export * from "./types/agent-thread.mts";
|
|
7
7
|
export * from "./types/check-annotations.mts";
|
|
8
8
|
export * from "./types/report.mts";
|
|
9
|
+
export * from "./types/suggestion-patch.mts";
|
|
9
10
|
export * from "./types/iterate.mts";
|
|
10
11
|
export * from "./types/merge-requirements.mts";
|
package/bin/types.mjs
CHANGED
|
@@ -6,5 +6,6 @@ export * from "./types/review-thread.mjs";
|
|
|
6
6
|
export * from "./types/agent-thread.mjs";
|
|
7
7
|
export * from "./types/check-annotations.mjs";
|
|
8
8
|
export * from "./types/report.mjs";
|
|
9
|
+
export * from "./types/suggestion-patch.mjs";
|
|
9
10
|
export * from "./types/iterate.mjs";
|
|
10
11
|
export * from "./types/merge-requirements.mjs";
|
package/package.json
CHANGED
|
@@ -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
|
|
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.
|
|
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 `
|
|
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_patches` 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
|
|
|
@@ -27,11 +27,11 @@ mechanics every tick. Apply the referenced playbook in full whenever a step poin
|
|
|
27
27
|
|
|
28
28
|
### Suggestion patches
|
|
29
29
|
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
- If the
|
|
34
|
-
-
|
|
30
|
+
- Run one plural `build-suggestion-patches` command with a repeated `--thread-id … --message … [--description …]` group for every marked thread in displayed order.
|
|
31
|
+
- The CLI only builds patches. Apply, stage, and commit the returned patches in order, then push once.
|
|
32
|
+
- The command builds from the fetched PR head and accepts a clean local descendant only when the complete ordered patch stream passes `git apply --check`.
|
|
33
|
+
- If the command refuses because a suggestion is unsafe or no longer applies, inspect the current source, the displayed replacement block, and reviewer intent before editing manually. Do not apply a stale numeric range blindly or retry unchanged input.
|
|
34
|
+
- A returned patch was checked against the then-current worktree. If it later fails, re-inspect the worktree because it changed after validation.
|
|
35
35
|
- Keep human-authored thread IDs in `apply review:` so Shepherd replies instead of resolving them.
|
|
36
36
|
|
|
37
37
|
### CI failure triage
|
|
@@ -53,12 +53,13 @@ More specific rows win over the general "GitHub Actions failure" row — check c
|
|
|
53
53
|
|
|
54
54
|
Applies to every `apply review:` / `resolve-only:` command the CLI prints. Covers only what stays safe if you run the printed command **unmodified** — `$HEAD_SHA`/`$DISMISS_MESSAGE` substitution and the self-reply exclusion rule are separate CLI-printed steps, not covered here, because the printed command is unsafe by default without them.
|
|
55
55
|
|
|
56
|
+
- Run every generated `apply review:` / `resolve-only:` command even when no code change is warranted. The command records the agent's disposition of the included review items; skipping it leaves bot threads active and can eventually trigger `fix-thrash`.
|
|
56
57
|
- Never add first-look-only or check-annotation IDs to `--reply-thread-ids`, `--resolve-thread-ids`, `--dismiss-review-ids`, or `--minimize-comment-ids` — those flags are pre-populated by the CLI.
|
|
57
58
|
- Keep every existing `--dismiss-review-ids` ID the CLI already included. Each is a bot or non-human review that must be dismissed; omitting one leaves the PR in `CHANGES_REQUESTED`.
|
|
58
59
|
|
|
59
60
|
### Review-mutation routing
|
|
60
61
|
|
|
61
|
-
For threads under `## Review threads to resolve
|
|
62
|
+
For threads under both `## Review threads` and `## Review threads to resolve`, evaluate every thread before running mutations. Keep bot and non-human IDs in `--resolve-thread-ids`, including when the feedback is advisory, already satisfied, or otherwise warrants no code change. Human-authored IDs use `--reply-thread-ids`; Shepherd replies instead of resolving them. Never move a human-authored ID to `--resolve-thread-ids`. Use the commands as generated — do not move an ID between flags.
|
|
62
63
|
|
|
63
64
|
### Shepherd Journal
|
|
64
65
|
|