pr-shepherd 0.19.0 → 0.21.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 +23 -56
- package/bin/checks/triage.test-support.mjs +1 -2
- package/bin/cli/default-poll.mjs +32 -0
- package/bin/cli/fix-formatter.mjs +3 -3
- package/bin/cli/formatters.mjs +5 -4
- package/bin/cli/handlers.mjs +0 -1
- package/bin/cli/help-command-pages.mjs +153 -0
- package/bin/cli/help-top-page.mjs +59 -0
- package/bin/cli/help.mjs +4 -30
- package/bin/cli/iterate-emitter.mjs +0 -1
- package/bin/cli/iterate-formatter.mjs +4 -6
- package/bin/cli/iterate-instructions.mjs +6 -8
- package/bin/cli/iterate-lean.mjs +4 -6
- package/bin/cli/iterate-lean.test-support.mjs +0 -2
- package/bin/cli/list-formatters.mjs +3 -0
- package/bin/cli/poll-handler.mjs +0 -1
- package/bin/cli/runner.mjs +2 -103
- package/bin/cli/validate-default-args.mjs +32 -0
- package/bin/cli-parser.clean.test-support.mjs +0 -1
- package/bin/cli-parser.commit-suggestion.test-support.mjs +2 -3
- package/bin/cli-parser.iterate-fix.test-support.mjs +1 -2
- package/bin/cli-parser.iterate-fixtures.mjs +1 -1
- package/bin/cli-parser.iterate.test-support.mjs +1 -2
- package/bin/cli-parser.mjs +7 -7
- package/bin/cli-parser.test-support.mjs +1 -2
- package/bin/commands/check.mjs +2 -1
- package/bin/commands/check.test-support.mjs +1 -2
- package/bin/commands/clean.test-support.mjs +0 -1
- package/bin/commands/commit-suggestion-instruction.mjs +2 -2
- package/bin/commands/commit-suggestion.apply.test-support.mjs +1 -2
- package/bin/commands/commit-suggestion.mjs +6 -2
- package/bin/commands/commit-suggestion.test-support.mjs +2 -3
- package/bin/commands/iterate/classify.mjs +2 -2
- package/bin/commands/iterate/fix-code.mjs +3 -3
- package/bin/commands/iterate/index.mjs +0 -1
- package/bin/commands/iterate/render.mjs +4 -3
- package/bin/commands/iterate-stall.test-support.mjs +0 -1
- package/bin/commands/iterate.fix-code-in-progress.test-support.mjs +2 -2
- package/bin/commands/poll.mjs +12 -3
- package/bin/commands/resolve-instructions.mjs +6 -7
- package/bin/commands/resolve.mjs +1 -1
- package/bin/commands/resolve.test-support.mjs +2 -2
- package/bin/commands/shepherd-journal.test-support.mjs +0 -2
- package/bin/comments/resolve.test-support.mjs +1 -2
- package/bin/comments/visible-comments.mjs +3 -2
- package/bin/config/load.mjs +0 -7
- package/bin/config.json +0 -3
- package/bin/github/batch-parsers.test-support.mjs +1 -2
- package/bin/github/batch.test-support.mjs +1 -2
- package/bin/github/http.test-support.mjs +1 -2
- package/bin/reporters/agent.mjs +1 -0
- package/bin/suggestions/patch.test-support.mjs +0 -2
- package/package.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +11 -27
- package/bin/cli/default-iterate.mjs +0 -50
package/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -6,8 +6,7 @@ import { adaptIterateLog, adaptFixCodeInstructions, buildSimpleIterateInstructio
|
|
|
6
6
|
*/
|
|
7
7
|
export function projectIterateLean(result, opts) {
|
|
8
8
|
const readyDelaySuffix = opts?.readyDelaySuffix;
|
|
9
|
-
const
|
|
10
|
-
const simpleInstructions = (r) => buildSimpleIterateInstructions(r, readyDelaySuffix, runner);
|
|
9
|
+
const simpleInstructions = (r) => buildSimpleIterateInstructions(r, readyDelaySuffix);
|
|
11
10
|
const base = {
|
|
12
11
|
action: result.action,
|
|
13
12
|
pr: result.pr,
|
|
@@ -94,7 +93,7 @@ export function projectIterateLean(result, opts) {
|
|
|
94
93
|
}),
|
|
95
94
|
resolveCommand: result.fix.resolveCommand,
|
|
96
95
|
...(result.fix.instructions.length > 0 && {
|
|
97
|
-
instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, readyDelaySuffix
|
|
96
|
+
instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, readyDelaySuffix),
|
|
98
97
|
}),
|
|
99
98
|
},
|
|
100
99
|
};
|
|
@@ -125,13 +124,12 @@ export function projectIterateLean(result, opts) {
|
|
|
125
124
|
}
|
|
126
125
|
export function projectIterateVerbose(result, opts) {
|
|
127
126
|
const readyDelaySuffix = opts?.readyDelaySuffix;
|
|
128
|
-
const runner = opts?.runner;
|
|
129
127
|
if (result.action === "fix_code") {
|
|
130
128
|
return {
|
|
131
129
|
...result,
|
|
132
130
|
fix: {
|
|
133
131
|
...result.fix,
|
|
134
|
-
instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, readyDelaySuffix
|
|
132
|
+
instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, readyDelaySuffix),
|
|
135
133
|
},
|
|
136
134
|
};
|
|
137
135
|
}
|
|
@@ -139,6 +137,6 @@ export function projectIterateVerbose(result, opts) {
|
|
|
139
137
|
return {
|
|
140
138
|
...result,
|
|
141
139
|
...log,
|
|
142
|
-
instructions: buildSimpleIterateInstructions(result, readyDelaySuffix
|
|
140
|
+
instructions: buildSimpleIterateInstructions(result, readyDelaySuffix),
|
|
143
141
|
};
|
|
144
142
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import { describe, it, expect } from "vitest";
|
|
3
1
|
import { projectIterateLean, projectIterateVerbose } from "./iterate-lean.mjs";
|
|
4
2
|
import { makeIterateResult } from "../cli-parser.iterate-fixtures.mjs";
|
|
5
3
|
export { makeIterateResult, projectIterateLean, projectIterateVerbose };
|
|
@@ -39,6 +39,9 @@ export function renderCommentBullet(c, opts = {}) {
|
|
|
39
39
|
const statusSuffix = opts.statusTag ? ` ${opts.statusTag}` : "";
|
|
40
40
|
return `- \`commentId=${c.id}\`${link} (${renderAuthor(c.author, c.authorType)})${statusSuffix}: ${renderBodyPreview(c.body)}`;
|
|
41
41
|
}
|
|
42
|
+
export function renderEditedCommentTag(c) {
|
|
43
|
+
return c.edited ? "[edited since first look]" : undefined;
|
|
44
|
+
}
|
|
42
45
|
export function renderReviewBullet(r, opts = {}) {
|
|
43
46
|
const bodySuffix = opts.includeBody && r.body != null && r.body !== "" ? `: ${renderBodyPreview(r.body)}` : "";
|
|
44
47
|
return `- \`reviewId=${r.id}\` (${renderAuthor(r.author, r.authorType)})${bodySuffix}`;
|
package/bin/cli/poll-handler.mjs
CHANGED
package/bin/cli/runner.mjs
CHANGED
|
@@ -1,111 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
export function buildPrShepherdCommand(args, opts) {
|
|
5
|
-
const runner = resolveCliRunner(opts.runner, opts.cwd);
|
|
6
|
-
const argv = baseArgvForRunner(runner).concat(args);
|
|
1
|
+
export function buildPrShepherdCommand(args) {
|
|
2
|
+
const argv = ["pr-shepherd", ...args];
|
|
7
3
|
return { argv, text: renderShellCommand(argv) };
|
|
8
4
|
}
|
|
9
|
-
export function resolveCliRunner(runner, cwd = process.cwd()) {
|
|
10
|
-
const configured = parseCliRunner(runner);
|
|
11
|
-
return configured === "auto" ? detectPackageRunner(cwd) : configured;
|
|
12
|
-
}
|
|
13
|
-
const VALID_RUNNERS = ["auto", "npx", "pnpm", "yarn", "bun"];
|
|
14
|
-
const VALID_RUNNERS_LIST = VALID_RUNNERS.map((v) => `"${v}"`).join(", ");
|
|
15
|
-
export function parseCliRunner(runner) {
|
|
16
|
-
if (runner === undefined)
|
|
17
|
-
return "auto";
|
|
18
|
-
if (typeof runner !== "string") {
|
|
19
|
-
throw new Error(`Invalid config: cli.runner must be one of ${VALID_RUNNERS_LIST}, got ${JSON.stringify(runner)}`);
|
|
20
|
-
}
|
|
21
|
-
const value = runner.trim();
|
|
22
|
-
if (VALID_RUNNERS.includes(value))
|
|
23
|
-
return value;
|
|
24
|
-
throw new Error(`Invalid config: cli.runner must be one of ${VALID_RUNNERS_LIST}, got ${JSON.stringify(runner)}`);
|
|
25
|
-
}
|
|
26
5
|
export function renderShellCommand(argv) {
|
|
27
6
|
return argv.map(renderShellArg).join(" ");
|
|
28
7
|
}
|
|
29
|
-
function baseArgvForRunner(runner) {
|
|
30
|
-
switch (runner) {
|
|
31
|
-
case "npx":
|
|
32
|
-
return ["npx", "pr-shepherd"];
|
|
33
|
-
case "pnpm":
|
|
34
|
-
return ["pnpm", "exec", "pr-shepherd"];
|
|
35
|
-
case "yarn":
|
|
36
|
-
return ["yarn", "run", "pr-shepherd"];
|
|
37
|
-
case "bun":
|
|
38
|
-
return ["bunx", "pr-shepherd"];
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
const runnerCache = new Map();
|
|
42
|
-
export function __resetRunnerCache() {
|
|
43
|
-
runnerCache.clear();
|
|
44
|
-
}
|
|
45
|
-
function detectPackageRunner(startDir) {
|
|
46
|
-
const cached = runnerCache.get(startDir);
|
|
47
|
-
if (cached)
|
|
48
|
-
return cached;
|
|
49
|
-
const home = homedir();
|
|
50
|
-
const repoRoot = findRepoRoot(startDir);
|
|
51
|
-
let current = startDir;
|
|
52
|
-
while (true) {
|
|
53
|
-
const atBoundary = current === repoRoot || current === home || current === dirname(current);
|
|
54
|
-
// Read signals for this directory unless it is home without being the repo root.
|
|
55
|
-
// This lets dotfiles repos (repoRoot === home) detect their own package manager
|
|
56
|
-
// while still preventing home's lockfiles from influencing unrelated projects.
|
|
57
|
-
if (!atBoundary || current === repoRoot) {
|
|
58
|
-
const packageManager = readPackageManager(current);
|
|
59
|
-
if (packageManager?.startsWith("pnpm@"))
|
|
60
|
-
return cacheRunner(startDir, "pnpm");
|
|
61
|
-
if (packageManager?.startsWith("yarn@"))
|
|
62
|
-
return cacheRunner(startDir, "yarn");
|
|
63
|
-
if (packageManager?.startsWith("npm@"))
|
|
64
|
-
return cacheRunner(startDir, "npx");
|
|
65
|
-
if (packageManager?.startsWith("bun@"))
|
|
66
|
-
return cacheRunner(startDir, "bun");
|
|
67
|
-
if (isFile(join(current, "pnpm-lock.yaml")))
|
|
68
|
-
return cacheRunner(startDir, "pnpm");
|
|
69
|
-
if (isFile(join(current, "yarn.lock")))
|
|
70
|
-
return cacheRunner(startDir, "yarn");
|
|
71
|
-
if (isFile(join(current, "bun.lock")))
|
|
72
|
-
return cacheRunner(startDir, "bun");
|
|
73
|
-
if (isFile(join(current, "bun.lockb")))
|
|
74
|
-
return cacheRunner(startDir, "bun");
|
|
75
|
-
if (isFile(join(current, "package-lock.json")))
|
|
76
|
-
return cacheRunner(startDir, "npx");
|
|
77
|
-
}
|
|
78
|
-
if (atBoundary)
|
|
79
|
-
return cacheRunner(startDir, "npx");
|
|
80
|
-
current = dirname(current);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
function cacheRunner(startDir, runner) {
|
|
84
|
-
runnerCache.set(startDir, runner);
|
|
85
|
-
return runner;
|
|
86
|
-
}
|
|
87
|
-
function readPackageManager(packageDir) {
|
|
88
|
-
try {
|
|
89
|
-
const parsed = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
|
|
90
|
-
return typeof parsed.packageManager === "string" ? parsed.packageManager.trim() : null;
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
function findRepoRoot(startDir) {
|
|
97
|
-
let current = startDir;
|
|
98
|
-
while (true) {
|
|
99
|
-
if (statSync(join(current, ".git"), { throwIfNoEntry: false }))
|
|
100
|
-
return current;
|
|
101
|
-
if (current === dirname(current))
|
|
102
|
-
return null;
|
|
103
|
-
current = dirname(current);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
function isFile(path) {
|
|
107
|
-
return statSync(path, { throwIfNoEntry: false })?.isFile() === true;
|
|
108
|
-
}
|
|
109
8
|
function renderShellArg(arg) {
|
|
110
9
|
if (/^\$[A-Z_][A-Z0-9_]*$/.test(arg))
|
|
111
10
|
return `"${arg}"`;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { parsePrNumber } from "./args.mjs";
|
|
2
|
+
/**
|
|
3
|
+
* Validate args for default-dispatch paths (e.g. `pr-shepherd [PR] [flags]`).
|
|
4
|
+
* Returns false when an unexpected token is found; calls onError with the
|
|
5
|
+
* offending arg so callers can print their own usage message.
|
|
6
|
+
*/
|
|
7
|
+
export function validateDefaultArgs(args, flagsWithValues, booleanFlags, onError) {
|
|
8
|
+
let sawPr = false;
|
|
9
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
10
|
+
const arg = args[i];
|
|
11
|
+
if (flagsWithValues.has(arg)) {
|
|
12
|
+
if (i + 1 >= args.length || args[i + 1].startsWith("--")) {
|
|
13
|
+
onError(arg);
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
i += 1;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const inlineFlag = arg.split("=", 1)[0];
|
|
20
|
+
if (flagsWithValues.has(inlineFlag))
|
|
21
|
+
continue;
|
|
22
|
+
if (booleanFlags.has(arg))
|
|
23
|
+
continue;
|
|
24
|
+
if (parsePrNumber(arg) !== null && !sawPr) {
|
|
25
|
+
sawPr = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
onError(arg);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
1
|
+
import { vi, beforeEach, afterEach } from "vitest";
|
|
3
2
|
vi.mock("./commands/check.mts", () => ({ runCheck: vi.fn() }));
|
|
4
3
|
vi.mock("./commands/resolve.mts", () => ({
|
|
5
4
|
runResolveFetch: vi.fn(),
|
|
@@ -43,7 +42,7 @@ const SUGGESTION_RESULT = {
|
|
|
43
42
|
"Apply the patch to `a.ts`: run `git apply` with the diff shown above.",
|
|
44
43
|
"Stage the file: `git add -- a.ts`",
|
|
45
44
|
'Commit: `git commit -m "apply fix" -m "Co-authored-by: alice <alice@users.noreply.github.com>"`',
|
|
46
|
-
"Resolve the thread on GitHub: `
|
|
45
|
+
"Resolve the thread on GitHub: `pr-shepherd resolve 42 --resolve-thread-ids t1`",
|
|
47
46
|
"Push when ready: `git push` (or `git push --force-with-lease` after rebasing).",
|
|
48
47
|
],
|
|
49
48
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
1
|
+
import { vi, beforeEach, afterEach } from "vitest";
|
|
3
2
|
vi.mock("./commands/check.mts", () => ({ runCheck: vi.fn() }));
|
|
4
3
|
vi.mock("./commands/resolve.mts", () => ({
|
|
5
4
|
runResolveFetch: vi.fn(),
|
|
@@ -35,7 +35,7 @@ export function makeIterateResult(action = "wait") {
|
|
|
35
35
|
checks: [],
|
|
36
36
|
changesRequestedReviews: [],
|
|
37
37
|
resolveCommand: {
|
|
38
|
-
argv: ["
|
|
38
|
+
argv: ["pr-shepherd", "resolve", "42"],
|
|
39
39
|
requiresHeadSha: true,
|
|
40
40
|
requiresDismissMessage: false,
|
|
41
41
|
hasMutations: false,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
1
|
+
import { vi, beforeEach, afterEach } from "vitest";
|
|
3
2
|
vi.mock("./commands/check.mts", () => ({ runCheck: vi.fn() }));
|
|
4
3
|
vi.mock("./commands/resolve.mts", () => ({
|
|
5
4
|
runResolveFetch: vi.fn(),
|
package/bin/cli-parser.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
5
|
* pr-shepherd --version
|
|
6
|
-
* pr-shepherd [PR] [--format text|json] [--ready-delay Nm]
|
|
6
|
+
* pr-shepherd [PR] [--interval 45s] [--timeout 4m] [--format text|json] [--ready-delay Nm]
|
|
7
7
|
* [--stall-timeout <duration>] [--no-auto-mark-ready]
|
|
8
8
|
* [--no-auto-cancel-actionable]
|
|
9
9
|
* pr-shepherd resolve [PR] [--fetch] [--resolve-thread-ids A,B] [--minimize-comment-ids X,Y]
|
|
@@ -22,7 +22,7 @@ import { readFileSync } from "node:fs";
|
|
|
22
22
|
import { runResolveFetch, runResolveMutate } from "./commands/resolve.mjs";
|
|
23
23
|
import { runLogFile } from "./commands/log-file.mjs";
|
|
24
24
|
import { parseCommonArgs, getFlag, hasFlag, parseList } from "./cli/args.mjs";
|
|
25
|
-
import {
|
|
25
|
+
import { isDefaultPollInvocation, validateDefaultPollArgs } from "./cli/default-poll.mjs";
|
|
26
26
|
import { USAGE, maybePrintHelp } from "./cli/help.mjs";
|
|
27
27
|
import { formatFetchResult, formatMutateResult } from "./cli/formatters.mjs";
|
|
28
28
|
import { handleClean, handleCommitSuggestion, handleIterate } from "./cli/handlers.mjs";
|
|
@@ -49,8 +49,8 @@ export async function main(argv) {
|
|
|
49
49
|
}
|
|
50
50
|
// Short-circuit all --help/-h before any I/O or logging.
|
|
51
51
|
if (hasFlag(args, "--help") || hasFlag(args, "-h")) {
|
|
52
|
-
if (
|
|
53
|
-
process.stdout.write(`${USAGE.
|
|
52
|
+
if (isDefaultPollInvocation(subcommand)) {
|
|
53
|
+
process.stdout.write(`${USAGE.poll}\n`);
|
|
54
54
|
}
|
|
55
55
|
else {
|
|
56
56
|
const key = subcommand != null && subcommand in USAGE
|
|
@@ -62,10 +62,10 @@ export async function main(argv) {
|
|
|
62
62
|
}
|
|
63
63
|
// Initialize the per-worktree log and install a stdout tee.
|
|
64
64
|
await setupLog(argv);
|
|
65
|
-
if (
|
|
66
|
-
if (!
|
|
65
|
+
if (isDefaultPollInvocation(subcommand)) {
|
|
66
|
+
if (!validateDefaultPollArgs(args))
|
|
67
67
|
return;
|
|
68
|
-
await
|
|
68
|
+
await handlePoll(args);
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
71
|
switch (subcommand) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import {
|
|
2
|
+
import { vi, beforeEach, afterEach } from "vitest";
|
|
4
3
|
vi.mock("./commands/resolve.mts", () => ({
|
|
5
4
|
runResolveFetch: vi.fn(),
|
|
6
5
|
runResolveMutate: vi.fn(),
|
package/bin/commands/check.mjs
CHANGED
|
@@ -46,7 +46,8 @@ export async function runCheck(opts) {
|
|
|
46
46
|
let autoResolveErrors = [];
|
|
47
47
|
if (opts.autoResolve && outdated.length > 0) {
|
|
48
48
|
const { resolved: resolvedIds, errors } = await autoResolveOutdated(outdated.map((t) => t.id));
|
|
49
|
-
|
|
49
|
+
const resolvedIdsSet = new Set(resolvedIds); // ⚡ Bolt optimization: O(1) lookup
|
|
50
|
+
autoResolved = outdated.filter((t) => resolvedIdsSet.has(t.id));
|
|
50
51
|
autoResolveErrors = errors;
|
|
51
52
|
}
|
|
52
53
|
const activeThreads = unresolvedThreads.filter((t) => !t.isOutdated && !t.isMinimized);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
1
|
+
import { vi, beforeEach } from "vitest";
|
|
3
2
|
vi.mock("../github/batch.mts", () => ({ fetchPrBatch: vi.fn() }));
|
|
4
3
|
vi.mock("../github/client.mts", () => ({
|
|
5
4
|
getRepoInfo: vi.fn().mockResolvedValue({ owner: "owner", name: "repo" }),
|
|
@@ -6,7 +6,7 @@ import { buildPrShepherdCommand } from "../cli/runner.mjs";
|
|
|
6
6
|
* (e.g. `"## Review threads"` for iterate, `"## Actionable Review Threads"` for resolve).
|
|
7
7
|
* @param includeDriftHint - Whether to add the trailing note about drift on failed apply.
|
|
8
8
|
*/
|
|
9
|
-
export function buildCommitSuggestionInstruction(prNumber, sectionName, includeDriftHint
|
|
9
|
+
export function buildCommitSuggestionInstruction(prNumber, sectionName, includeDriftHint) {
|
|
10
10
|
const command = buildPrShepherdCommand([
|
|
11
11
|
"commit-suggestion",
|
|
12
12
|
String(prNumber),
|
|
@@ -15,7 +15,7 @@ export function buildCommitSuggestionInstruction(prNumber, sectionName, includeD
|
|
|
15
15
|
"--message",
|
|
16
16
|
"<one-sentence headline>",
|
|
17
17
|
"--format=json",
|
|
18
|
-
]
|
|
18
|
+
]).text;
|
|
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.";
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
1
|
+
import { vi } from "vitest";
|
|
3
2
|
// ---------------------------------------------------------------------------
|
|
4
3
|
// Hoisted mocks
|
|
5
4
|
// ---------------------------------------------------------------------------
|
|
@@ -5,7 +5,6 @@ import { getRepoInfo, getCurrentPrNumber, getCurrentBranch } from "../github/cli
|
|
|
5
5
|
import { fetchPrBatch } from "../github/batch.mjs";
|
|
6
6
|
import { parseSuggestion, isCommittableSuggestion } from "../suggestions/parse.mjs";
|
|
7
7
|
import { buildUnifiedDiff } from "../suggestions/patch.mjs";
|
|
8
|
-
import { loadConfig } from "../config/load.mjs";
|
|
9
8
|
import { buildPrShepherdCommand } from "../cli/runner.mjs";
|
|
10
9
|
const execFile = promisify(execFileCb);
|
|
11
10
|
export async function runCommitSuggestion(opts) {
|
|
@@ -93,7 +92,12 @@ export async function runCommitSuggestion(opts) {
|
|
|
93
92
|
`-m ${sq(commitMessageArg)}`,
|
|
94
93
|
...commitBodyArg.split("\n\n").map((p) => `-m ${sq(p)}`),
|
|
95
94
|
].join(" ");
|
|
96
|
-
const resolveCommand = buildPrShepherdCommand([
|
|
95
|
+
const resolveCommand = buildPrShepherdCommand([
|
|
96
|
+
"resolve",
|
|
97
|
+
String(prNumber),
|
|
98
|
+
"--resolve-thread-ids",
|
|
99
|
+
opts.threadId,
|
|
100
|
+
]).text;
|
|
97
101
|
const postActionInstructions = [
|
|
98
102
|
`Apply the patch to \`${filePath}\`: run \`git apply\` with the diff shown above, or edit the file directly using the line range (${range}).`,
|
|
99
103
|
`Stage the file: \`git add -- ${quotedPath}\``,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
1
|
+
import { vi, beforeEach } from "vitest";
|
|
3
2
|
// ---------------------------------------------------------------------------
|
|
4
3
|
// Hoisted mocks
|
|
5
4
|
// ---------------------------------------------------------------------------
|
|
@@ -107,7 +106,7 @@ function setupHappyPath() {
|
|
|
107
106
|
// ---------------------------------------------------------------------------
|
|
108
107
|
export function registerHooks() {
|
|
109
108
|
beforeEach(() => {
|
|
110
|
-
mockLoadConfig.mockReturnValue({
|
|
109
|
+
mockLoadConfig.mockReturnValue({});
|
|
111
110
|
});
|
|
112
111
|
}
|
|
113
112
|
export { FILE_CONTENT, GLOBAL_OPTS, fetchPrBatch, getCurrentBranch, getCurrentPrNumber, makeBatch, makeGitSuccess, makeThread, mockExecFile, mockFetchBatch, mockGetCurrentBranch, mockGetCurrentPrNumber, mockLoadConfig, mockReadFile, readFile, runCommitSuggestion, setupHappyPath, };
|
|
@@ -37,8 +37,8 @@ export function classifyReviewSummaries(summaries, approvals, minimizeApprovals,
|
|
|
37
37
|
surfacedApprovals: approvals,
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
-
export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, reviews, checks, prNumber
|
|
41
|
-
const argv = buildPrShepherdCommand(["resolve", String(prNumber)]
|
|
40
|
+
export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, reviews, checks, prNumber) {
|
|
41
|
+
const argv = buildPrShepherdCommand(["resolve", String(prNumber)]).argv;
|
|
42
42
|
const resolveThreadIds = dedupeIds(threads.map((t) => t.id));
|
|
43
43
|
const threadIds = dedupeIds([...resolveThreadIds, ...resolutionOnlyThreads.map((t) => t.id)]);
|
|
44
44
|
if (threadIds.length > 0) {
|
|
@@ -7,7 +7,7 @@ import { buildFixInstructions } from "./render.mjs";
|
|
|
7
7
|
import { applyStallGuard } from "./stall.mjs";
|
|
8
8
|
import { tryCancelRun, buildInProgressRunIds } from "./helpers.mjs";
|
|
9
9
|
export async function handleFixCode(ctx) {
|
|
10
|
-
const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals,
|
|
10
|
+
const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, } = ctx;
|
|
11
11
|
const failingChecks = report.checks.failing;
|
|
12
12
|
const stored = await readFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber });
|
|
13
13
|
const isNewSha = stored?.headSha !== headSha;
|
|
@@ -64,7 +64,7 @@ export async function handleFixCode(ctx) {
|
|
|
64
64
|
const inProgressRunIds = pushLikely ? buildInProgressRunIds(report, cancelledSet) : [];
|
|
65
65
|
const commentMinimizeIds = report.comments.minimizeIds ?? actionableComments.map((c) => c.id);
|
|
66
66
|
const allCommentIds = [...commentMinimizeIds, ...reviewSummaryIds];
|
|
67
|
-
const resolveCommand = buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, changesRequestedReviews, checks, prNumber
|
|
67
|
+
const resolveCommand = buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, changesRequestedReviews, checks, prNumber);
|
|
68
68
|
const overlappingReviewIds = resolveCommand.droppedDismissReviewIds ?? [];
|
|
69
69
|
if (overlappingReviewIds.length > 0) {
|
|
70
70
|
process.stderr.write(`pr-shepherd: resolve command overlap: ${overlappingReviewIds.length} ` +
|
|
@@ -100,7 +100,7 @@ export async function handleFixCode(ctx) {
|
|
|
100
100
|
}
|
|
101
101
|
const firstLookThreads = report.threads.firstLook;
|
|
102
102
|
const firstLookComments = report.comments.firstLook;
|
|
103
|
-
const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prNumber, cancelled.length, firstLookThreads, firstLookComments, firstLookSummaries, editedSummaries, inProgressRunIds, resolutionOnlyThreads
|
|
103
|
+
const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prNumber, cancelled.length, firstLookThreads, firstLookComments, firstLookSummaries, editedSummaries, inProgressRunIds, resolutionOnlyThreads);
|
|
104
104
|
return applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, {
|
|
105
105
|
...base,
|
|
106
106
|
baseBranch: baseLookup.branch,
|
|
@@ -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 = []) {
|
|
19
19
|
const instructions = [];
|
|
20
20
|
const hasNonConflictHints = threads.length > 0 ||
|
|
21
21
|
checks.length > 0 ||
|
|
@@ -53,7 +53,7 @@ export function buildFixInstructions(threads, actionableComments, checks, change
|
|
|
53
53
|
}
|
|
54
54
|
const hasSuggestions = threads.some((t) => t.suggestion);
|
|
55
55
|
if (hasSuggestions) {
|
|
56
|
-
instructions.push(buildCommitSuggestionInstruction(prNumber, "## Review threads", false
|
|
56
|
+
instructions.push(buildCommitSuggestionInstruction(prNumber, "## Review threads", false));
|
|
57
57
|
}
|
|
58
58
|
if (threads.length > 0 || actionableComments.length > 0) {
|
|
59
59
|
const suggestionFallback = hasSuggestions
|
|
@@ -93,10 +93,11 @@ export function buildFixInstructions(threads, actionableComments, checks, change
|
|
|
93
93
|
instructions.push(SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE);
|
|
94
94
|
}
|
|
95
95
|
const editedTotal = editedSummaries.length +
|
|
96
|
+
actionableComments.filter((c) => c.edited).length +
|
|
96
97
|
firstLookThreads.filter((t) => t.edited).length +
|
|
97
98
|
firstLookComments.filter((c) => c.edited).length;
|
|
98
99
|
if (editedTotal > 0) {
|
|
99
|
-
instructions.push(`Items under \`## Review summaries (edited since first look)
|
|
100
|
+
instructions.push(`Items marked \`[edited since first look]\`, items under \`## Review summaries (edited since first look)\`, and any first-look bullet tagged \`, edited\` were updated by their author after you previously acknowledged them. Read the updated body before deciding whether any matching \`## Review threads to resolve\` item should be resolved.`);
|
|
100
101
|
}
|
|
101
102
|
if (resolveCommand.hasMutations) {
|
|
102
103
|
instructions.push(buildShepherdJournalInstruction(prNumber, SHEPHERD_JOURNAL_REFERENCE_GUIDANCE_THREADS_AND_COMMENTS_IN_ITEM_HEADINGS));
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
1
|
+
import { vi, beforeEach, afterEach } from "vitest";
|
|
3
2
|
const mockFetch = vi.fn();
|
|
4
3
|
vi.stubGlobal("fetch", mockFetch);
|
|
5
4
|
const { mockExecFile } = vi.hoisted(() => ({ mockExecFile: vi.fn() }));
|
|
@@ -44,6 +43,7 @@ function makeReport(overrides = {}) {
|
|
|
44
43
|
repo: "owner/repo",
|
|
45
44
|
status: "READY",
|
|
46
45
|
baseBranch: "main",
|
|
46
|
+
branchProtection: null,
|
|
47
47
|
mergeStatus: {
|
|
48
48
|
status: "CLEAN",
|
|
49
49
|
state: "OPEN",
|
package/bin/commands/poll.mjs
CHANGED
|
@@ -3,9 +3,12 @@ function sleep(ms) {
|
|
|
3
3
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4
4
|
}
|
|
5
5
|
function writeTickProgress(tick, elapsedSeconds, sleepSeconds, verbose) {
|
|
6
|
-
if (
|
|
6
|
+
if (verbose) {
|
|
7
7
|
process.stderr.write(`[poll tick ${tick} / +${elapsedSeconds}s] WAIT — sleeping ${sleepSeconds}s\n`);
|
|
8
8
|
}
|
|
9
|
+
else {
|
|
10
|
+
process.stderr.write(".");
|
|
11
|
+
}
|
|
9
12
|
}
|
|
10
13
|
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
11
14
|
export async function runPoll(opts) {
|
|
@@ -16,17 +19,23 @@ export async function runPoll(opts) {
|
|
|
16
19
|
let tick = 0;
|
|
17
20
|
let lastResult;
|
|
18
21
|
const verbose = opts.verbose === true;
|
|
22
|
+
let dotsPrinted = false;
|
|
19
23
|
while (true) {
|
|
20
24
|
tick += 1;
|
|
21
25
|
lastResult = await runIterate(iterateOpts);
|
|
22
26
|
if (lastResult.action !== "wait")
|
|
23
|
-
|
|
27
|
+
break;
|
|
24
28
|
const elapsedMs = Date.now() - start;
|
|
25
29
|
const remainingMs = timeoutMs - elapsedMs;
|
|
26
30
|
if (remainingMs <= 0)
|
|
27
|
-
|
|
31
|
+
break;
|
|
28
32
|
const nextSleepMs = Math.min(intervalMs, remainingMs);
|
|
29
33
|
writeTickProgress(tick, Math.round(elapsedMs / 1000), Math.round(nextSleepMs / 1000), verbose);
|
|
34
|
+
if (!verbose)
|
|
35
|
+
dotsPrinted = true;
|
|
30
36
|
await sleep(nextSleepMs);
|
|
31
37
|
}
|
|
38
|
+
if (dotsPrinted)
|
|
39
|
+
process.stderr.write("\n");
|
|
40
|
+
return lastResult;
|
|
32
41
|
}
|
|
@@ -6,7 +6,7 @@ import { buildCommitSuggestionInstruction } from "./commit-suggestion-instructio
|
|
|
6
6
|
* Steps are conditionally emitted based on what the fetch returned (mirrors
|
|
7
7
|
* `buildFixInstructions` in `commands/iterate/render.mts`).
|
|
8
8
|
*/
|
|
9
|
-
export function buildFetchInstructions(prNumber, result
|
|
9
|
+
export function buildFetchInstructions(prNumber, result) {
|
|
10
10
|
const { actionableThreads, resolutionOnlyThreads, firstLookThreads, actionableComments, firstLookComments, changesRequestedReviews, reviewSummaries, commitSuggestionsEnabled, } = result;
|
|
11
11
|
const firstLookTotal = firstLookThreads.length + firstLookComments.length;
|
|
12
12
|
const total = actionableThreads.length +
|
|
@@ -27,13 +27,14 @@ export function buildFetchInstructions(prNumber, result, runner) {
|
|
|
27
27
|
if (firstLookTotal > 0) {
|
|
28
28
|
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\`, include its ID in \`--resolve-thread-ids\`; otherwise do not pass first-look-only IDs to mutation flags.`);
|
|
29
29
|
}
|
|
30
|
-
const editedTotal =
|
|
30
|
+
const editedTotal = actionableComments.filter((c) => c.edited).length +
|
|
31
|
+
firstLookThreads.filter((t) => t.edited).length +
|
|
31
32
|
firstLookComments.filter((c) => c.edited).length;
|
|
32
33
|
if (editedTotal > 0) {
|
|
33
|
-
instructions.push(`
|
|
34
|
+
instructions.push(`Actionable comments marked \`[edited since first look]\` and first-look bullets tagged \`, edited\` were updated by their author after you previously acknowledged them. Read the updated body before deciding whether any matching \`## Review threads to resolve\` item should be resolved.`);
|
|
34
35
|
}
|
|
35
36
|
if (hasSuggestions) {
|
|
36
|
-
instructions.push(buildCommitSuggestionInstruction(prNumber, "## Actionable Review Threads", true
|
|
37
|
+
instructions.push(buildCommitSuggestionInstruction(prNumber, "## Actionable Review Threads", true));
|
|
37
38
|
}
|
|
38
39
|
if (hasCodeItems) {
|
|
39
40
|
instructions.push(`Read and edit each file referenced under \`## Actionable Review Threads\`, \`## Actionable PR Comments\`, and \`## Pending CHANGES_REQUESTED reviews\` above. Reclassify each fixed item as Fixed. If an item is too complex to address, leave it as Actionable for the final report.`);
|
|
@@ -50,9 +51,7 @@ export function buildFetchInstructions(prNumber, result, runner) {
|
|
|
50
51
|
: reviewSummaries.length > 0
|
|
51
52
|
? ` Review-summary IDs (\`PRR_…\` from \`## Review summaries\`) go into \`--minimize-comment-ids\`.`
|
|
52
53
|
: "";
|
|
53
|
-
const resolveCommand = `${buildPrShepherdCommand(["resolve", String(prNumber)]
|
|
54
|
-
runner,
|
|
55
|
-
}).text} [--resolve-thread-ids <ids>] [--minimize-comment-ids <ids>] [--dismiss-review-ids <ids> --message "<reason>"]`;
|
|
54
|
+
const resolveCommand = `${buildPrShepherdCommand(["resolve", String(prNumber)]).text} [--resolve-thread-ids <ids>] [--minimize-comment-ids <ids>] [--dismiss-review-ids <ids> --message "<reason>"]`;
|
|
56
55
|
instructions.push(`Run \`${resolveCommand}\` with only the non-empty flag subsets. Skip the command entirely if all three ID lists are empty.${requireShaHint}${dismissNote}`);
|
|
57
56
|
instructions.push(buildShepherdJournalInstruction(prNumber, SHEPHERD_JOURNAL_REFERENCE_GUIDANCE_THREADS_AND_COMMENTS_IN_ITEMS));
|
|
58
57
|
instructions.push(`Report: echo the CLI's mutation output, then one line per Acknowledged item: \`Acknowledged <id> (@<author>): <reason>\`. If any fetched item was neither resolved nor acknowledged, stop and escalate: "<N> item(s) fetched but not acted on or acknowledged — need human direction before closing".`);
|
package/bin/commands/resolve.mjs
CHANGED
|
@@ -129,5 +129,5 @@ export async function runResolveFetch(opts) {
|
|
|
129
129
|
reviewSummaries: cfg.resolve.fetchReviewSummaries ? data.reviewSummaries : [],
|
|
130
130
|
commitSuggestionsEnabled: cfg.actions.commitSuggestions,
|
|
131
131
|
};
|
|
132
|
-
return { ...result, instructions: buildFetchInstructions(prNumber, result
|
|
132
|
+
return { ...result, instructions: buildFetchInstructions(prNumber, result) };
|
|
133
133
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
1
|
+
import { vi, beforeEach } from "vitest";
|
|
3
2
|
vi.mock("../github/client.mts", () => ({
|
|
4
3
|
getRepoInfo: vi.fn().mockResolvedValue({ owner: "owner", name: "repo" }),
|
|
5
4
|
getCurrentPrNumber: vi.fn().mockResolvedValue(42),
|
|
@@ -67,6 +66,7 @@ function makeBatchData(overrides = {}) {
|
|
|
67
66
|
reviewSummaries: [],
|
|
68
67
|
approvedReviews: [],
|
|
69
68
|
checks: [],
|
|
69
|
+
branchProtection: null,
|
|
70
70
|
...overrides,
|
|
71
71
|
};
|
|
72
72
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
1
|
import { buildShepherdJournalInstruction, SHEPHERD_JOURNAL_APPEND_HINT, SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE, SHEPHERD_JOURNAL_REFERENCE_GUIDANCE_THREADS_AND_COMMENTS_IN_ITEM_HEADINGS, SHEPHERD_JOURNAL_REFERENCE_GUIDANCE_THREADS_AND_COMMENTS_IN_ITEMS, SHEPHERD_JOURNAL_SECTION, SHEPHERD_JOURNAL_SECTION_PATTERN, } from "./shepherd-journal.mjs";
|
|
4
2
|
import { buildFixInstructions } from "./iterate/render.mjs";
|
|
5
3
|
import { buildFetchInstructions } from "./resolve-instructions.mjs";
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
1
|
+
import { vi, beforeEach } from "vitest";
|
|
3
2
|
// ---------------------------------------------------------------------------
|
|
4
3
|
// Mock github/client.mts before any imports.
|
|
5
4
|
// ---------------------------------------------------------------------------
|