pr-shepherd 0.10.3 → 0.12.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 +2 -2
- package/README.md +51 -9
- package/bin/agent-runtime.mjs +7 -0
- package/bin/checks/triage.mjs +5 -46
- package/bin/cli/args.mjs +10 -2
- package/bin/cli/default-iterate.mjs +54 -0
- package/bin/cli/duration-flag.mjs +22 -0
- package/bin/cli/fix-formatter.mjs +25 -15
- package/bin/cli/formatters.mjs +26 -38
- package/bin/cli/handlers.mjs +26 -24
- package/bin/cli/iterate-formatter.mjs +14 -10
- package/bin/cli/iterate-instructions.mjs +75 -0
- package/bin/cli/iterate-lean.mjs +59 -6
- package/bin/cli/list-formatters.mjs +11 -0
- package/bin/cli-parser.iterate-fixtures.mjs +2 -0
- package/bin/cli-parser.mjs +15 -1
- package/bin/commands/check.mjs +6 -6
- package/bin/commands/commit-suggestion.mjs +34 -80
- package/bin/commands/iterate/classify.mjs +5 -4
- package/bin/commands/iterate/escalate.mjs +2 -1
- package/bin/commands/iterate/fix-code.mjs +16 -8
- package/bin/commands/iterate/helpers.mjs +34 -0
- package/bin/commands/iterate/index.mjs +2 -2
- package/bin/commands/iterate/render.mjs +25 -37
- package/bin/commands/iterate/stall.mjs +3 -1
- package/bin/commands/iterate.mjs +1 -1
- package/bin/commands/monitor.mjs +68 -17
- package/bin/commands/ready-delay.mjs +2 -1
- package/bin/commands/resolve-instructions.mjs +8 -4
- package/bin/commands/resolve.mjs +4 -4
- package/bin/config.json +1 -3
- package/bin/index.mjs +1 -0
- package/bin/reporters/agent.mjs +6 -2
- package/bin/reporters/check-instructions.mjs +11 -5
- package/bin/reporters/json.mjs +2 -2
- package/bin/reporters/text.mjs +24 -16
- package/package.json +3 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-shepherd",
|
|
3
|
-
"description": "Autonomous PR CI monitor and review-comment resolver for
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
|
|
4
|
+
"version": "0.12.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Jonathan Ong",
|
|
7
7
|
"email": "jonathanrichardong@gmail.com"
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pr-shepherd
|
|
2
2
|
|
|
3
|
-
Autonomous PR CI monitor and review-comment resolver for Claude Code.
|
|
3
|
+
Autonomous PR CI monitor and review-comment resolver for agentic coding tools, including Claude Code and Codex.
|
|
4
4
|
The goal is to have an agent take a plan to a human-reviewable PR autonomously.
|
|
5
5
|
|
|
6
6
|
Example Workflow:
|
|
@@ -20,6 +20,8 @@ Example Workflow:
|
|
|
20
20
|
|
|
21
21
|
`pr-shepherd` optimizes token management, rate limits, and agentic orchestration by moving **ALL** deterministic logic and prompts to code via a CLI tool, enshrining what would be a large skill or command prompt (of which the agent would inevitably make mistakes) into the code and returning a clear, actionable prompt.
|
|
22
22
|
|
|
23
|
+
The CLI adapts monitor instructions to the calling agent. Claude Code gets `/loop` bootstrap instructions. Codex is detected with `AGENT=codex` or the current Codex CLI signal `CODEX_CI=1`; it gets a reusable `npx pr-shepherd <PR>` command and explicit goal-friendly iterate instructions because Codex does not provide `/loop` scheduling in this workflow.
|
|
24
|
+
|
|
23
25
|
At a high level, to start the monitor, the skill/command invokes a CLI that returns a prompt to be ingested by the agent _(schematic — paraphrased for brevity; actual output is more detailed)_:
|
|
24
26
|
|
|
25
27
|
```
|
|
@@ -40,7 +42,7 @@ Loop args: `4m`
|
|
|
40
42
|
after completing the actions below. The cron job handles the next fire.
|
|
41
43
|
|
|
42
44
|
Run in a single Bash call:
|
|
43
|
-
npx pr-shepherd
|
|
45
|
+
npx pr-shepherd 123
|
|
44
46
|
|
|
45
47
|
…(self-dedup guidance, error-handling instructions)…
|
|
46
48
|
|
|
@@ -50,10 +52,10 @@ Run in a single Bash call:
|
|
|
50
52
|
2. Otherwise, invoke the /loop skill with Loop args and the full ## Loop prompt body.
|
|
51
53
|
```
|
|
52
54
|
|
|
53
|
-
Each iteration calls `npx pr-shepherd
|
|
55
|
+
Each iteration calls `npx pr-shepherd <PR>`, which provides actionable feedback directly to the agent:
|
|
54
56
|
|
|
55
57
|
```
|
|
56
|
-
> npx pr-shepherd
|
|
58
|
+
> npx pr-shepherd 123
|
|
57
59
|
|
|
58
60
|
# PR #123 [FIX_CODE]
|
|
59
61
|
|
|
@@ -125,7 +127,7 @@ Some other workflow improvements:
|
|
|
125
127
|
|
|
126
128
|
Recommendations:
|
|
127
129
|
|
|
128
|
-
- Run `pr-shepherd` on all your PRs before you go to sleep so that you wake up to reviewable PRs.
|
|
130
|
+
- Run `pr-shepherd` on all your PRs before you go to sleep so that you wake up to reviewable PRs. In Claude Code, `/pr-shepherd:monitor` uses `/loop` and continues working when your rate limit window is reset. In Codex, keep an active goal cycling the reusable `npx pr-shepherd <PR>` command every `watch.interval` (default 4m) until Shepherd emits `[CANCEL]` for ready-delay completion or merged/closed, or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures).
|
|
129
131
|
- Instruct your agents to write comments in a single review (comment, changes requested, or approved). This allows the review's comments/threads to be minimized or resolved together, keeping your pull request history clean. If you write inline comments outside of a review, each comment would still show up in the pull request history and take up space.
|
|
130
132
|
- Avoid sticky comments as they will continue to be hidden. Instead, just make a new comment, especially on reviews. If you really want sticky comments, instruct your agent to unhide/unminimize them when updating them.
|
|
131
133
|
- Avoid having automation edit comments, reviews, or threads in place because updated items get minimized. Instead, always make a new review, comment, thread, etc.
|
|
@@ -142,9 +144,11 @@ Recommendations:
|
|
|
142
144
|
|
|
143
145
|
### Monitor a PR
|
|
144
146
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
In Claude Code, creates a cron loop that fires every 4 minutes, checks CI and review comments, fixes issues, and marks the PR ready for review when clean. The loop cancels automatically when the PR is merged or closed.
|
|
148
|
+
|
|
149
|
+
In Codex, run `npx pr-shepherd monitor <PR>` once to emit the goal-friendly recurrence prompt, then follow that prompt. Its reusable follow-up command is `npx pr-shepherd <PR>`.
|
|
150
|
+
|
|
151
|
+
Claude Code:
|
|
148
152
|
|
|
149
153
|
```
|
|
150
154
|
/pr-shepherd:monitor # infer PR from current branch
|
|
@@ -153,6 +157,16 @@ loop cancels automatically when the PR is merged or closed.
|
|
|
153
157
|
/pr-shepherd:monitor 42 --ready-delay 15m
|
|
154
158
|
```
|
|
155
159
|
|
|
160
|
+
Codex:
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
npx pr-shepherd monitor # bootstrap from current branch, then follow its instructions
|
|
164
|
+
npx pr-shepherd monitor 42 # bootstrap PR #42, then follow its instructions
|
|
165
|
+
npx pr-shepherd 42 # subsequent explicit check/action tick
|
|
166
|
+
npx pr-shepherd 42 --ready-delay 15m
|
|
167
|
+
npx pr-shepherd iterate 42 # legacy-compatible spelling
|
|
168
|
+
```
|
|
169
|
+
|
|
156
170
|
### Check a PR
|
|
157
171
|
|
|
158
172
|
One-shot status snapshot — merge state, CI results, and unresolved comments.
|
|
@@ -208,6 +222,34 @@ claude /plugin install pr-shepherd
|
|
|
208
222
|
|
|
209
223
|
This repo ships two `marketplace.json` files that serve different install flows: the root `marketplace.json` resolves the plugin from the npm registry (used by the `claude /plugin marketplace add` command above); `.claude-plugin/marketplace.json` is the owner-level registry manifest that resolves the plugin from the local plugin directory (used when Claude Code installs from a local or git-based source). Both files are needed to support these two install paths.
|
|
210
224
|
|
|
225
|
+
### For Codex
|
|
226
|
+
|
|
227
|
+
Codex does not use the Claude plugin or `/pr-shepherd:*` slash commands. Install the CLI where Codex will run it, then call `npx pr-shepherd` directly:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
npm install --save-dev pr-shepherd
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
If your Codex environment does not already set `CODEX_CI=1`, set `AGENT=codex` so `pr-shepherd` emits Codex-compatible instructions instead of Claude `/loop` instructions:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
export AGENT=codex
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Then start a PR monitor from Codex:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
npx pr-shepherd monitor 42
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Follow the output's `## Instructions`. The monitor bootstrap runs one tick and prints the reusable follow-up command, usually:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
npx pr-shepherd 42
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
For an active Codex goal, rerun that command every `watch.interval` (default 4m) until Shepherd emits `[CANCEL]` for ready-delay completion or merged/closed, or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures). `npx pr-shepherd iterate 42` remains supported for existing workflows. There is no background `/loop` scheduler in Codex.
|
|
252
|
+
|
|
211
253
|
### Without the plugin
|
|
212
254
|
|
|
213
255
|
See [docs/custom-commands.md](docs/custom-commands.md) for a project-local slash command that wraps the CLI without the plugin.
|
|
@@ -234,7 +276,7 @@ actions:
|
|
|
234
276
|
autoMarkReady: false # disable to stay draft until you manually promote
|
|
235
277
|
```
|
|
236
278
|
|
|
237
|
-
Environment variables: `GH_TOKEN` / `GITHUB_TOKEN` (auth; falls back to `gh auth token`), `PR_SHEPHERD_STATE_DIR` (override loop-state and log base dir), `PR_SHEPHERD_LOG_DISABLED=1` (disable the per-worktree debug log).
|
|
279
|
+
Environment variables: `GH_TOKEN` / `GITHUB_TOKEN` (auth; falls back to `gh auth token`), `PR_SHEPHERD_STATE_DIR` (override loop-state and log base dir), `PR_SHEPHERD_LOG_DISABLED=1` (disable the per-worktree debug log), `AGENT=codex` or `CODEX_CI=1` (emit Codex-compatible monitor instructions).
|
|
238
280
|
|
|
239
281
|
See [docs/configuration.md](docs/configuration.md) for full semantics and deprecated-key migration.
|
|
240
282
|
|
package/bin/checks/triage.mjs
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
|
-
import { rest
|
|
2
|
-
export function triageFailingChecks(failingChecks, repo
|
|
1
|
+
import { rest } from "../github/http.mjs";
|
|
2
|
+
export function triageFailingChecks(failingChecks, repo) {
|
|
3
3
|
const jobsCache = new Map();
|
|
4
|
-
return Promise.all(failingChecks.map((c) => triageCheck(c, repo, jobsCache
|
|
4
|
+
return Promise.all(failingChecks.map((c) => triageCheck(c, repo, jobsCache)));
|
|
5
5
|
}
|
|
6
|
-
async function triageCheck(check, repo, jobsCache
|
|
7
|
-
if (check.runId === null) {
|
|
6
|
+
async function triageCheck(check, repo, jobsCache) {
|
|
7
|
+
if (check.runId === null || check.conclusion === "CANCELLED") {
|
|
8
8
|
return { ...check };
|
|
9
9
|
}
|
|
10
10
|
const jobs = await fetchJobs(check.runId, repo, jobsCache);
|
|
11
11
|
const jobInfo = jobs ? pickJobInfo(jobs, check.name) : undefined;
|
|
12
|
-
const logTail = jobInfo?.jobId !== undefined && logTailLines > 0
|
|
13
|
-
? await fetchLogTail(jobInfo.jobId, repo, logTailLines, logTailChars, jobInfo.failedStep)
|
|
14
|
-
: undefined;
|
|
15
12
|
return {
|
|
16
13
|
...check,
|
|
17
14
|
...(jobInfo?.workflowName !== undefined && { workflowName: jobInfo.workflowName }),
|
|
18
15
|
...(jobInfo?.jobName !== undefined && { jobName: jobInfo.jobName }),
|
|
19
16
|
...(jobInfo?.failedStep !== undefined && { failedStep: jobInfo.failedStep }),
|
|
20
|
-
...(logTail !== undefined && { logTail }),
|
|
21
17
|
};
|
|
22
18
|
}
|
|
23
19
|
function fetchJobs(runId, repo, cache) {
|
|
@@ -67,42 +63,5 @@ function pickJobInfo(jobs, checkName) {
|
|
|
67
63
|
workflowName: job.workflow_name,
|
|
68
64
|
jobName: job.name,
|
|
69
65
|
failedStep,
|
|
70
|
-
jobId: job.id,
|
|
71
66
|
};
|
|
72
67
|
}
|
|
73
|
-
async function fetchLogTail(jobId, repo, logTailLines, logTailChars, failedStepName) {
|
|
74
|
-
const { owner, name } = repo;
|
|
75
|
-
try {
|
|
76
|
-
const text = await restText(`/repos/${owner}/${name}/actions/jobs/${jobId}/logs`);
|
|
77
|
-
const allLines = text.split("\n");
|
|
78
|
-
const stepLines = failedStepName ? extractStepLines(allLines, failedStepName) : null;
|
|
79
|
-
const lines = stepLines ?? allLines;
|
|
80
|
-
const tail = lines.length <= logTailLines ? lines.join("\n") : lines.slice(-logTailLines).join("\n");
|
|
81
|
-
return tail.length <= logTailChars ? tail : tail.slice(-logTailChars);
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
return undefined;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
// Extract the lines inside the ##[group]..##[endgroup] section for the named step.
|
|
88
|
-
// Returns null when no matching group is found so the caller falls back to the full log.
|
|
89
|
-
function extractStepLines(lines, stepName) {
|
|
90
|
-
const lowerStep = stepName.toLowerCase();
|
|
91
|
-
let inStep = false;
|
|
92
|
-
const result = [];
|
|
93
|
-
for (const line of lines) {
|
|
94
|
-
const content = line.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s+/, "");
|
|
95
|
-
if (!inStep) {
|
|
96
|
-
if (content.startsWith("##[group]") && content.slice(9).toLowerCase().includes(lowerStep)) {
|
|
97
|
-
inStep = true;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
else if (content.startsWith("##[endgroup]")) {
|
|
101
|
-
inStep = false;
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
result.push(line);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return result.length > 0 ? result : null;
|
|
108
|
-
}
|
package/bin/cli/args.mjs
CHANGED
|
@@ -94,8 +94,8 @@ export function parseCommonArgs(args) {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
const prIndex = args.findIndex((a, index) => !skipForPrDetect.has(index) && !a.startsWith("--") &&
|
|
98
|
-
const prNumber = prIndex !== -1 ?
|
|
97
|
+
const prIndex = args.findIndex((a, index) => !skipForPrDetect.has(index) && !a.startsWith("--") && parsePrNumber(a) !== null);
|
|
98
|
+
const prNumber = prIndex !== -1 ? parsePrNumber(args[prIndex]) : undefined;
|
|
99
99
|
// Remove consumed global-flag indices (and the PR number itself) from extra.
|
|
100
100
|
if (prIndex !== -1) {
|
|
101
101
|
consumedIndices.add(prIndex);
|
|
@@ -107,6 +107,14 @@ export function parseCommonArgs(args) {
|
|
|
107
107
|
extra,
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
|
+
export function parsePrNumber(value) {
|
|
111
|
+
if (/^\d+$/.test(value))
|
|
112
|
+
return parseInt(value, 10);
|
|
113
|
+
const match = value.match(/^https?:\/\/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)(?:[/?#].*)?$/);
|
|
114
|
+
if (match)
|
|
115
|
+
return parseInt(match[1], 10);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
110
118
|
/** Get the value of a flag like `--flag value` or `--flag=value`. */
|
|
111
119
|
export function getFlag(args, name) {
|
|
112
120
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { parsePrNumber } from "./args.mjs";
|
|
2
|
+
const DEFAULT_ITERATE_FLAGS_WITH_VALUES = new Set([
|
|
3
|
+
"--format",
|
|
4
|
+
"--ready-delay",
|
|
5
|
+
"--cooldown-seconds",
|
|
6
|
+
"--stall-timeout",
|
|
7
|
+
]);
|
|
8
|
+
const DEFAULT_ITERATE_BOOLEAN_FLAGS = new Set([
|
|
9
|
+
"--verbose",
|
|
10
|
+
"--no-auto-mark-ready",
|
|
11
|
+
"--no-auto-cancel-actionable",
|
|
12
|
+
]);
|
|
13
|
+
export function isDefaultIterateInvocation(subcommand) {
|
|
14
|
+
return (subcommand === undefined ||
|
|
15
|
+
parsePrNumber(subcommand) !== null ||
|
|
16
|
+
isDefaultIterateFlag(subcommand));
|
|
17
|
+
}
|
|
18
|
+
export function validateDefaultIterateArgs(args) {
|
|
19
|
+
let sawPr = false;
|
|
20
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
21
|
+
const arg = args[i];
|
|
22
|
+
if (DEFAULT_ITERATE_FLAGS_WITH_VALUES.has(arg)) {
|
|
23
|
+
if (i + 1 >= args.length || args[i + 1].startsWith("--")) {
|
|
24
|
+
writeDefaultUsageError(arg);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
i += 1;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const inlineFlag = arg.split("=", 1)[0];
|
|
31
|
+
if (DEFAULT_ITERATE_FLAGS_WITH_VALUES.has(inlineFlag))
|
|
32
|
+
continue;
|
|
33
|
+
if (DEFAULT_ITERATE_BOOLEAN_FLAGS.has(arg))
|
|
34
|
+
continue;
|
|
35
|
+
if (parsePrNumber(arg) !== null && !sawPr) {
|
|
36
|
+
sawPr = true;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
writeDefaultUsageError(arg);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
function isDefaultIterateFlag(arg) {
|
|
45
|
+
const name = arg.split("=", 1)[0];
|
|
46
|
+
return DEFAULT_ITERATE_FLAGS_WITH_VALUES.has(name) || DEFAULT_ITERATE_BOOLEAN_FLAGS.has(arg);
|
|
47
|
+
}
|
|
48
|
+
function writeDefaultUsageError(arg) {
|
|
49
|
+
process.stderr.write(`Unknown subcommand: ${arg}\n`);
|
|
50
|
+
process.stderr.write("Usage: pr-shepherd [PR] [options]\n" +
|
|
51
|
+
" pr-shepherd <check|resolve|commit-suggestion|iterate|monitor|status|log-file> [options]\n" +
|
|
52
|
+
" pr-shepherd --version | -v\n");
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function validateDurationFlag(command, flag, value, presentAsSeparateArg) {
|
|
2
|
+
if (value === null) {
|
|
3
|
+
if (presentAsSeparateArg) {
|
|
4
|
+
process.stderr.write(`${command}: ${flag} requires a value (e.g. ${flag} 15m)\n`);
|
|
5
|
+
process.exitCode = 1;
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
const trimmed = value.trim();
|
|
11
|
+
if (trimmed.startsWith("--")) {
|
|
12
|
+
process.stderr.write(`${command}: ${flag} requires a value (e.g. ${flag} 15m)\n`);
|
|
13
|
+
process.exitCode = 1;
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
if (!/^\d+(?:m|min|minutes?|h|hours?)?$/.test(trimmed)) {
|
|
17
|
+
process.stderr.write(`${command}: invalid ${flag}: ${value}. Expected a duration like 5m, 2h, 10m, or 1h.\n`);
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return trimmed;
|
|
22
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { renderResolveCommand } from "../commands/iterate.mjs";
|
|
2
|
-
import { safeFence } from "./fence.mjs";
|
|
3
2
|
import { joinSections } from "../util/markdown.mjs";
|
|
4
3
|
import { renderSuggestionBlock, renderLineRange } from "./suggestion-renderer.mjs";
|
|
5
|
-
import { renderThreadBullet, renderCommentBullet, renderReviewBullet, renderFirstLookStatusTag, } from "./list-formatters.mjs";
|
|
6
|
-
|
|
4
|
+
import { renderThreadBullet, renderCommentBullet, renderReviewBullet, renderFirstLookStatusTag, renderThreadResolutionStatusTag, } from "./list-formatters.mjs";
|
|
5
|
+
import { adaptFixCodeInstructions, numberInstructions } from "./iterate-instructions.mjs";
|
|
6
|
+
export function formatFixCodeResult(header, result, opts) {
|
|
7
|
+
const runtime = opts?.runtime ?? "claude";
|
|
8
|
+
const readyDelaySuffix = opts?.readyDelaySuffix;
|
|
9
|
+
const retryInterval = opts?.retryInterval;
|
|
7
10
|
const sections = [header];
|
|
8
11
|
if (result.fix.threads.length > 0) {
|
|
9
12
|
sections.push("## Review threads");
|
|
@@ -19,6 +22,12 @@ export function formatFixCodeResult(header, result) {
|
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
24
|
}
|
|
25
|
+
if (result.fix.resolutionOnlyThreads.length > 0) {
|
|
26
|
+
sections.push("## Review threads to resolve");
|
|
27
|
+
sections.push(result.fix.resolutionOnlyThreads
|
|
28
|
+
.map((t) => renderThreadBullet(t, { statusTag: renderThreadResolutionStatusTag(t) }))
|
|
29
|
+
.join("\n"));
|
|
30
|
+
}
|
|
22
31
|
if (result.fix.actionableComments.length > 0) {
|
|
23
32
|
sections.push("## Actionable comments");
|
|
24
33
|
for (const c of result.fix.actionableComments) {
|
|
@@ -37,16 +46,13 @@ export function formatFixCodeResult(header, result) {
|
|
|
37
46
|
: ch.detailsUrl
|
|
38
47
|
? `external \`${ch.detailsUrl}\``
|
|
39
48
|
: "(no runId)";
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
lines.push(` ${fence}`);
|
|
48
|
-
lines.push(ch.logTail.replace(/^/gm, " ").trimEnd());
|
|
49
|
-
lines.push(` ${fence}`);
|
|
49
|
+
const conclusionTag = ch.conclusion !== null ? ` [conclusion: ${ch.conclusion}]` : "";
|
|
50
|
+
const lines = [`- ${locator} — \`${workflowPrefix}${jobLabel}\`${conclusionTag}`];
|
|
51
|
+
if (ch.conclusion !== "CANCELLED") {
|
|
52
|
+
if (ch.failedStep)
|
|
53
|
+
lines.push(` > ${ch.failedStep}`);
|
|
54
|
+
if (ch.summary)
|
|
55
|
+
lines.push(` > ${ch.summary}`);
|
|
50
56
|
}
|
|
51
57
|
return lines.join("\n");
|
|
52
58
|
});
|
|
@@ -85,7 +91,7 @@ export function formatFixCodeResult(header, result) {
|
|
|
85
91
|
}
|
|
86
92
|
const firstLookTotal = result.fix.firstLookThreads.length + result.fix.firstLookComments.length;
|
|
87
93
|
if (firstLookTotal > 0) {
|
|
88
|
-
sections.push(`## First-look items (${firstLookTotal}) —
|
|
94
|
+
sections.push(`## First-look items (${firstLookTotal}) — acknowledge status before acting`);
|
|
89
95
|
const bullets = [];
|
|
90
96
|
for (const t of result.fix.firstLookThreads) {
|
|
91
97
|
bullets.push(renderThreadBullet(t, { statusTag: renderFirstLookStatusTag(t) }));
|
|
@@ -96,6 +102,10 @@ export function formatFixCodeResult(header, result) {
|
|
|
96
102
|
}
|
|
97
103
|
sections.push(bullets.join("\n"));
|
|
98
104
|
}
|
|
105
|
+
if (result.fix.inProgressRunIds.length > 0) {
|
|
106
|
+
sections.push("## In-progress runs");
|
|
107
|
+
sections.push(result.fix.inProgressRunIds.map((id) => `- \`${id}\``).join("\n"));
|
|
108
|
+
}
|
|
99
109
|
if (result.cancelled.length > 0) {
|
|
100
110
|
sections.push("## Cancelled runs");
|
|
101
111
|
sections.push(result.cancelled.map((id) => `- \`${id}\``).join("\n"));
|
|
@@ -107,7 +117,7 @@ export function formatFixCodeResult(header, result) {
|
|
|
107
117
|
}
|
|
108
118
|
sections.push(postFixLines.join("\n"));
|
|
109
119
|
sections.push("## Instructions");
|
|
110
|
-
sections.push(result.fix.instructions.
|
|
120
|
+
sections.push(numberInstructions(adaptFixCodeInstructions(result.fix.instructions, result.pr, runtime, readyDelaySuffix, retryInterval)));
|
|
111
121
|
return joinSections(sections);
|
|
112
122
|
}
|
|
113
123
|
function blockquote(body) {
|
package/bin/cli/formatters.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export { formatIterateResult } from "./iterate-formatter.mjs";
|
|
2
|
-
export { projectIterateLean } from "./iterate-lean.mjs";
|
|
2
|
+
export { projectIterateLean, projectIterateVerbose } from "./iterate-lean.mjs";
|
|
3
3
|
import { safeFence } from "./fence.mjs";
|
|
4
|
-
import { renderThreadBullet, renderCommentBullet, renderReviewBullet, renderFirstLookStatusTag, } from "./list-formatters.mjs";
|
|
4
|
+
import { renderThreadBullet, renderCommentBullet, renderReviewBullet, renderFirstLookStatusTag, renderThreadResolutionStatusTag, } from "./list-formatters.mjs";
|
|
5
5
|
import { joinSections } from "../util/markdown.mjs";
|
|
6
6
|
export function formatFetchResult(result) {
|
|
7
7
|
const activeTotal = result.actionableThreads.length +
|
|
8
|
+
result.resolutionOnlyThreads.length +
|
|
8
9
|
result.actionableComments.length +
|
|
9
10
|
result.changesRequestedReviews.length +
|
|
10
11
|
result.reviewSummaries.length;
|
|
@@ -25,6 +26,12 @@ export function formatFetchResult(result) {
|
|
|
25
26
|
.map((t) => renderThreadBullet(t, { renderSuggestion: true }))
|
|
26
27
|
.join("\n\n"));
|
|
27
28
|
}
|
|
29
|
+
if (result.resolutionOnlyThreads.length > 0) {
|
|
30
|
+
sections.push(`## Review threads to resolve (${result.resolutionOnlyThreads.length})`);
|
|
31
|
+
sections.push(result.resolutionOnlyThreads
|
|
32
|
+
.map((t) => renderThreadBullet(t, { statusTag: renderThreadResolutionStatusTag(t) }))
|
|
33
|
+
.join("\n"));
|
|
34
|
+
}
|
|
28
35
|
if (result.actionableComments.length > 0) {
|
|
29
36
|
sections.push(`## Actionable PR Comments (${result.actionableComments.length})`);
|
|
30
37
|
sections.push(result.actionableComments.map((c) => renderCommentBullet(c)).join("\n"));
|
|
@@ -38,7 +45,7 @@ export function formatFetchResult(result) {
|
|
|
38
45
|
sections.push(result.reviewSummaries.map((r) => renderReviewBullet(r, { includeBody: true })).join("\n"));
|
|
39
46
|
}
|
|
40
47
|
if (firstLookTotal > 0) {
|
|
41
|
-
sections.push(`## First-look items (${firstLookTotal}) —
|
|
48
|
+
sections.push(`## First-look items (${firstLookTotal}) — acknowledge status before acting`);
|
|
42
49
|
const bullets = [];
|
|
43
50
|
for (const t of result.firstLookThreads) {
|
|
44
51
|
bullets.push(renderThreadBullet(t, { statusTag: renderFirstLookStatusTag(t) }));
|
|
@@ -67,48 +74,29 @@ export function formatCommitSuggestionResult(result) {
|
|
|
67
74
|
const range = result.startLine === result.endLine
|
|
68
75
|
? `line ${result.startLine}`
|
|
69
76
|
: `lines ${result.startLine}–${result.endLine}`;
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
lines.push(`Suggestion from @${result.author} for PR #${result.pr} — thread ${result.threadId}:`);
|
|
78
|
+
lines.push(` repo: ${result.repo}`);
|
|
79
|
+
lines.push(` ${result.path} (${range})`);
|
|
80
|
+
if (result.patch) {
|
|
81
|
+
const fence = safeFence(result.patch);
|
|
72
82
|
lines.push("");
|
|
73
83
|
lines.push(`${fence}diff`);
|
|
74
|
-
lines.push(patch.trimEnd());
|
|
84
|
+
lines.push(result.patch.trimEnd());
|
|
75
85
|
lines.push(fence);
|
|
76
86
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
lines.push(`- path: ${result.path} (${range})`);
|
|
85
|
-
lines.push(`- author: @${result.author}`);
|
|
86
|
-
lines.push(`- reason: ${result.reason ?? "unknown"}`);
|
|
87
|
-
}
|
|
88
|
-
if (result.patch)
|
|
89
|
-
pushPatch(result.patch);
|
|
90
|
-
}
|
|
91
|
-
else if (result.applied) {
|
|
92
|
-
lines.push(`Applied suggestion from @${result.author}:`);
|
|
93
|
-
lines.push(` ${result.path} (${range})`);
|
|
94
|
-
if (result.commitSha)
|
|
95
|
-
lines.push(`Commit: ${result.commitSha}`);
|
|
96
|
-
if (result.patch)
|
|
97
|
-
pushPatch(result.patch);
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
lines.push(`Failed to apply suggestion ${result.threadId}:`);
|
|
101
|
-
lines.push(`- path: ${result.path} (${range})`);
|
|
102
|
-
lines.push(`- author: @${result.author}`);
|
|
103
|
-
lines.push(`- reason: ${result.reason ?? "unknown"}`);
|
|
104
|
-
if (result.patch)
|
|
105
|
-
pushPatch(result.patch);
|
|
106
|
-
}
|
|
107
|
-
if (result.postActionInstruction) {
|
|
87
|
+
lines.push("");
|
|
88
|
+
lines.push("## Suggested commit message");
|
|
89
|
+
lines.push("");
|
|
90
|
+
lines.push(result.commitMessage);
|
|
91
|
+
lines.push("");
|
|
92
|
+
lines.push(result.commitBody);
|
|
93
|
+
if (result.postActionInstructions.length > 0) {
|
|
108
94
|
lines.push("");
|
|
109
95
|
lines.push("## Instructions");
|
|
110
96
|
lines.push("");
|
|
111
|
-
|
|
97
|
+
result.postActionInstructions.forEach((inst, i) => {
|
|
98
|
+
lines.push(`${i + 1}. ${inst}`);
|
|
99
|
+
});
|
|
112
100
|
}
|
|
113
101
|
return lines.join("\n");
|
|
114
102
|
}
|
package/bin/cli/handlers.mjs
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import { runCommitSuggestion } from "../commands/commit-suggestion.mjs";
|
|
2
2
|
import { runIterate } from "../commands/iterate.mjs";
|
|
3
|
-
import { runMonitor, formatMonitorResult } from "../commands/monitor.mjs";
|
|
3
|
+
import { runMonitor, formatMonitorResult, formatMonitorJson } from "../commands/monitor.mjs";
|
|
4
4
|
import { runStatus, formatStatusTable } from "../commands/status.mjs";
|
|
5
5
|
import { getRepoInfo } from "../github/client.mjs";
|
|
6
6
|
import { loadConfig } from "../config/load.mjs";
|
|
7
|
+
import { detectAgentRuntime } from "../agent-runtime.mjs";
|
|
7
8
|
import { parseCommonArgs, getFlag, hasFlag, parseStatusPrNumbers, parseIntStrict, } from "./args.mjs";
|
|
8
9
|
import { parseDurationToMinutes, iterateActionToExitCode, deriveSimpleReady, } from "./exit-codes.mjs";
|
|
9
|
-
import { formatCommitSuggestionResult, formatIterateResult, projectIterateLean, } from "./formatters.mjs";
|
|
10
|
+
import { formatCommitSuggestionResult, formatIterateResult, projectIterateLean, projectIterateVerbose, } from "./formatters.mjs";
|
|
11
|
+
import { validateDurationFlag } from "./duration-flag.mjs";
|
|
10
12
|
export async function handleCommitSuggestion(args) {
|
|
11
13
|
const { prNumber, global: globalOpts, extra } = parseCommonArgs(args);
|
|
12
14
|
const threadId = getFlag(extra, "--thread-id");
|
|
13
15
|
if (!threadId) {
|
|
14
|
-
process.stderr.write("Usage: pr-shepherd commit-suggestion [PR] --thread-id ID
|
|
15
|
-
" (--message is required unless --dry-run is set)\n");
|
|
16
|
+
process.stderr.write("Usage: pr-shepherd commit-suggestion [PR] --thread-id ID --message MSG [--description DESC]\n");
|
|
16
17
|
process.exitCode = 1;
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
|
-
const dryRun = hasFlag(extra, "--dry-run");
|
|
20
20
|
const message = getFlag(extra, "--message") ?? undefined;
|
|
21
|
-
if (!
|
|
21
|
+
if (!message || message.trim() === "") {
|
|
22
22
|
process.stderr.write("--message is required and must be non-empty\n");
|
|
23
23
|
process.exitCode = 1;
|
|
24
24
|
return;
|
|
@@ -30,23 +30,20 @@ export async function handleCommitSuggestion(args) {
|
|
|
30
30
|
threadId,
|
|
31
31
|
message,
|
|
32
32
|
description,
|
|
33
|
-
dryRun,
|
|
34
33
|
});
|
|
35
34
|
process.stdout.write(globalOpts.format === "json"
|
|
36
35
|
? `${JSON.stringify(result, null, 2)}\n`
|
|
37
36
|
: `${formatCommitSuggestionResult(result)}\n`);
|
|
38
|
-
if (result.dryRun) {
|
|
39
|
-
process.exitCode = result.valid ? 0 : 1;
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
process.exitCode = result.applied ? 0 : 1;
|
|
43
|
-
}
|
|
44
37
|
}
|
|
45
38
|
export async function handleIterate(args) {
|
|
46
39
|
const { prNumber, global: globalOpts, extra } = parseCommonArgs(args);
|
|
40
|
+
const runtime = detectAgentRuntime();
|
|
47
41
|
const readyDelayStr = getFlag(extra, "--ready-delay");
|
|
42
|
+
const readyDelaySuffix = validateDurationFlag("pr-shepherd", "--ready-delay", readyDelayStr, hasFlag(extra, "--ready-delay"));
|
|
43
|
+
if (readyDelaySuffix === null)
|
|
44
|
+
return;
|
|
48
45
|
const cfg = loadConfig();
|
|
49
|
-
const readyDelaySeconds = parseDurationToMinutes(
|
|
46
|
+
const readyDelaySeconds = parseDurationToMinutes(readyDelaySuffix ?? "", cfg.watch.readyDelayMinutes) * 60;
|
|
50
47
|
const cooldownSecondsStr = getFlag(extra, "--cooldown-seconds");
|
|
51
48
|
const cooldownSeconds = cooldownSecondsStr
|
|
52
49
|
? parseIntStrict(cooldownSecondsStr, "--cooldown-seconds")
|
|
@@ -66,24 +63,28 @@ export async function handleIterate(args) {
|
|
|
66
63
|
noAutoMarkReady,
|
|
67
64
|
noAutoCancelActionable,
|
|
68
65
|
});
|
|
66
|
+
const projectionOpts = { runtime, readyDelaySuffix, retryInterval: cfg.watch.interval };
|
|
69
67
|
if (globalOpts.format === "json") {
|
|
70
|
-
const output = globalOpts.verbose
|
|
68
|
+
const output = globalOpts.verbose
|
|
69
|
+
? projectIterateVerbose(result, projectionOpts)
|
|
70
|
+
: projectIterateLean(result, projectionOpts);
|
|
71
71
|
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
72
72
|
}
|
|
73
73
|
else {
|
|
74
|
-
process.stdout.write(`${formatIterateResult(result, {
|
|
74
|
+
process.stdout.write(`${formatIterateResult(result, {
|
|
75
|
+
verbose: globalOpts.verbose,
|
|
76
|
+
...projectionOpts,
|
|
77
|
+
})}\n`);
|
|
75
78
|
}
|
|
76
79
|
process.exitCode = iterateActionToExitCode(result.action);
|
|
77
80
|
}
|
|
78
81
|
export async function handleMonitor(args) {
|
|
79
82
|
const { prNumber, global: globalOpts, extra } = parseCommonArgs(args);
|
|
83
|
+
const runtime = detectAgentRuntime();
|
|
80
84
|
const readyDelayStr = getFlag(extra, "--ready-delay");
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
process.stderr.write("pr-shepherd monitor: --ready-delay requires a value (e.g. --ready-delay 15m)\n");
|
|
84
|
-
process.exitCode = 1;
|
|
85
|
+
const readyDelaySuffix = validateDurationFlag("pr-shepherd monitor", "--ready-delay", readyDelayStr, hasFlag(extra, "--ready-delay"));
|
|
86
|
+
if (readyDelaySuffix === null)
|
|
85
87
|
return;
|
|
86
|
-
}
|
|
87
88
|
const remaining = [];
|
|
88
89
|
for (let i = 0; i < extra.length; i++) {
|
|
89
90
|
const a = extra[i];
|
|
@@ -108,7 +109,8 @@ export async function handleMonitor(args) {
|
|
|
108
109
|
result = await runMonitor({
|
|
109
110
|
...globalOpts,
|
|
110
111
|
prNumber,
|
|
111
|
-
readyDelaySuffix:
|
|
112
|
+
readyDelaySuffix: readyDelaySuffix ?? undefined,
|
|
113
|
+
runtime,
|
|
112
114
|
});
|
|
113
115
|
}
|
|
114
116
|
catch (err) {
|
|
@@ -117,8 +119,8 @@ export async function handleMonitor(args) {
|
|
|
117
119
|
return;
|
|
118
120
|
}
|
|
119
121
|
process.stdout.write(globalOpts.format === "json"
|
|
120
|
-
? `${JSON.stringify(result, null, 2)}\n`
|
|
121
|
-
: `${formatMonitorResult(result)}\n`);
|
|
122
|
+
? `${JSON.stringify(formatMonitorJson(result, { runtime }), null, 2)}\n`
|
|
123
|
+
: `${formatMonitorResult(result, { runtime })}\n`);
|
|
122
124
|
}
|
|
123
125
|
export async function handleStatus(args) {
|
|
124
126
|
const { global: globalOpts } = parseCommonArgs(args);
|