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.
Files changed (57) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +23 -56
  3. package/bin/checks/triage.test-support.mjs +1 -2
  4. package/bin/cli/default-poll.mjs +32 -0
  5. package/bin/cli/fix-formatter.mjs +3 -3
  6. package/bin/cli/formatters.mjs +5 -4
  7. package/bin/cli/handlers.mjs +0 -1
  8. package/bin/cli/help-command-pages.mjs +153 -0
  9. package/bin/cli/help-top-page.mjs +59 -0
  10. package/bin/cli/help.mjs +4 -30
  11. package/bin/cli/iterate-emitter.mjs +0 -1
  12. package/bin/cli/iterate-formatter.mjs +4 -6
  13. package/bin/cli/iterate-instructions.mjs +6 -8
  14. package/bin/cli/iterate-lean.mjs +4 -6
  15. package/bin/cli/iterate-lean.test-support.mjs +0 -2
  16. package/bin/cli/list-formatters.mjs +3 -0
  17. package/bin/cli/poll-handler.mjs +0 -1
  18. package/bin/cli/runner.mjs +2 -103
  19. package/bin/cli/validate-default-args.mjs +32 -0
  20. package/bin/cli-parser.clean.test-support.mjs +0 -1
  21. package/bin/cli-parser.commit-suggestion.test-support.mjs +2 -3
  22. package/bin/cli-parser.iterate-fix.test-support.mjs +1 -2
  23. package/bin/cli-parser.iterate-fixtures.mjs +1 -1
  24. package/bin/cli-parser.iterate.test-support.mjs +1 -2
  25. package/bin/cli-parser.mjs +7 -7
  26. package/bin/cli-parser.test-support.mjs +1 -2
  27. package/bin/commands/check.mjs +2 -1
  28. package/bin/commands/check.test-support.mjs +1 -2
  29. package/bin/commands/clean.test-support.mjs +0 -1
  30. package/bin/commands/commit-suggestion-instruction.mjs +2 -2
  31. package/bin/commands/commit-suggestion.apply.test-support.mjs +1 -2
  32. package/bin/commands/commit-suggestion.mjs +6 -2
  33. package/bin/commands/commit-suggestion.test-support.mjs +2 -3
  34. package/bin/commands/iterate/classify.mjs +2 -2
  35. package/bin/commands/iterate/fix-code.mjs +3 -3
  36. package/bin/commands/iterate/index.mjs +0 -1
  37. package/bin/commands/iterate/render.mjs +4 -3
  38. package/bin/commands/iterate-stall.test-support.mjs +0 -1
  39. package/bin/commands/iterate.fix-code-in-progress.test-support.mjs +2 -2
  40. package/bin/commands/poll.mjs +12 -3
  41. package/bin/commands/resolve-instructions.mjs +6 -7
  42. package/bin/commands/resolve.mjs +1 -1
  43. package/bin/commands/resolve.test-support.mjs +2 -2
  44. package/bin/commands/shepherd-journal.test-support.mjs +0 -2
  45. package/bin/comments/resolve.test-support.mjs +1 -2
  46. package/bin/comments/visible-comments.mjs +3 -2
  47. package/bin/config/load.mjs +0 -7
  48. package/bin/config.json +0 -3
  49. package/bin/github/batch-parsers.test-support.mjs +1 -2
  50. package/bin/github/batch.test-support.mjs +1 -2
  51. package/bin/github/http.test-support.mjs +1 -2
  52. package/bin/reporters/agent.mjs +1 -0
  53. package/bin/suggestions/patch.test-support.mjs +0 -2
  54. package/package.json +1 -1
  55. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  56. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +11 -27
  57. package/bin/cli/default-iterate.mjs +0 -50
@@ -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 runner = opts?.runner;
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, runner),
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, runner),
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, runner),
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}`;
@@ -37,6 +37,5 @@ export async function handlePoll(args) {
37
37
  format: globalOpts.format,
38
38
  verbose: globalOpts.verbose ?? false,
39
39
  readyDelaySuffix: flags.readyDelaySuffix ?? undefined,
40
- runner: cfg.cli?.runner,
41
40
  });
42
41
  }
@@ -1,111 +1,10 @@
1
- import { readFileSync, statSync } from "node:fs";
2
- import { dirname, join } from "node:path";
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,4 +1,3 @@
1
- // @ts-nocheck
2
1
  import { vi, beforeEach, afterEach } from "vitest";
3
2
  vi.mock("./commands/clean.mts", () => ({
4
3
  runClean: vi.fn(),
@@ -1,5 +1,4 @@
1
- // @ts-nocheck
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: `npx pr-shepherd resolve 42 --resolve-thread-ids t1`",
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
- // @ts-nocheck
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: ["npx", "pr-shepherd", "resolve", "42"],
38
+ argv: ["pr-shepherd", "resolve", "42"],
39
39
  requiresHeadSha: true,
40
40
  requiresDismissMessage: false,
41
41
  hasMutations: false,
@@ -1,5 +1,4 @@
1
- // @ts-nocheck
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(),
@@ -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 { isDefaultIterateInvocation, validateDefaultIterateArgs } from "./cli/default-iterate.mjs";
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 (isDefaultIterateInvocation(subcommand)) {
53
- process.stdout.write(`${USAGE.iterate}\n`);
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 (isDefaultIterateInvocation(subcommand)) {
66
- if (!validateDefaultIterateArgs(args))
65
+ if (isDefaultPollInvocation(subcommand)) {
66
+ if (!validateDefaultPollArgs(args))
67
67
  return;
68
- await handleIterate(args);
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 { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { vi, beforeEach, afterEach } from "vitest";
4
3
  vi.mock("./commands/resolve.mts", () => ({
5
4
  runResolveFetch: vi.fn(),
6
5
  runResolveMutate: vi.fn(),
@@ -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
- autoResolved = outdated.filter((t) => resolvedIds.includes(t.id));
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
- // @ts-nocheck
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" }),
@@ -1,4 +1,3 @@
1
- // @ts-nocheck
2
1
  import { vi, beforeEach, afterEach } from "vitest";
3
2
  import { join } from "node:path";
4
3
  import { mkdtemp, realpath, rm, mkdir, writeFile, stat } from "node:fs/promises";
@@ -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, runner) {
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
- ], { runner }).text;
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
- // @ts-nocheck
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(["resolve", String(prNumber), "--resolve-thread-ids", opts.threadId], { runner: loadConfig().cli?.runner }).text;
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
- // @ts-nocheck
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({ cli: { runner: "auto" } });
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, runner) {
41
- const argv = buildPrShepherdCommand(["resolve", String(prNumber)], { runner }).argv;
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, cliRunner, } = ctx;
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, cliRunner);
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, cliRunner);
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,
@@ -122,7 +122,6 @@ export async function runIterate(opts) {
122
122
  firstLookSummaries,
123
123
  editedSummaries,
124
124
  surfacedApprovals,
125
- cliRunner: config.cli?.runner,
126
125
  });
127
126
  }
128
127
  const canMarkReady = report.status === "READY" &&
@@ -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 = [], runner) {
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, runner));
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)\` 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
+ 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,4 +1,3 @@
1
- // @ts-nocheck
2
1
  import { vi } from "vitest";
3
2
  import { NOW, makeOpts } from "./iterate-test-support.mjs";
4
3
  import { readStallState, writeStallState } from "../state/iterate-stall.mjs";
@@ -1,5 +1,4 @@
1
- // @ts-nocheck
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",
@@ -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 (process.stderr.isTTY || verbose) {
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
- return lastResult;
27
+ break;
24
28
  const elapsedMs = Date.now() - start;
25
29
  const remainingMs = timeoutMs - elapsedMs;
26
30
  if (remainingMs <= 0)
27
- return lastResult;
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, runner) {
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 = firstLookThreads.filter((t) => t.edited).length +
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(`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
+ 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, runner));
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".`);
@@ -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, cfg.cli?.runner) };
132
+ return { ...result, instructions: buildFetchInstructions(prNumber, result) };
133
133
  }
@@ -1,5 +1,4 @@
1
- // @ts-nocheck
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
- // @ts-nocheck
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
  // ---------------------------------------------------------------------------