pr-shepherd 0.13.0 → 0.14.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 -8
- package/README.md +13 -15
- package/bin/cli/fix-formatter.mjs +2 -1
- package/bin/cli/handlers.mjs +8 -5
- package/bin/cli/iterate-formatter.mjs +12 -6
- package/bin/cli/iterate-instructions.mjs +15 -11
- package/bin/cli/iterate-lean.mjs +6 -4
- package/bin/cli/runner.mjs +109 -0
- package/bin/cli-parser.mjs +4 -2
- package/bin/commands/commit-suggestion.mjs +4 -1
- package/bin/commands/iterate/classify.mjs +3 -2
- package/bin/commands/iterate/fix-code.mjs +3 -3
- package/bin/commands/iterate/index.mjs +1 -0
- package/bin/commands/iterate/render.mjs +18 -16
- package/bin/commands/monitor.mjs +7 -6
- package/bin/commands/resolve-instructions.mjs +16 -3
- package/bin/commands/resolve.mjs +1 -1
- package/bin/config/load.mjs +10 -0
- package/bin/config.json +3 -0
- package/bin/reporters/check-instructions.mjs +2 -1
- package/package.json +2 -2
- package/plugin/skills/monitor/SKILL.md +12 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +3 -13
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-shepherd",
|
|
3
3
|
"description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.14.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Jonathan Ong",
|
|
7
7
|
"email": "jonathanrichardong@gmail.com"
|
|
@@ -9,12 +9,6 @@
|
|
|
9
9
|
"homepage": "https://github.com/jonathanong/pr-shepherd",
|
|
10
10
|
"repository": "https://github.com/jonathanong/pr-shepherd",
|
|
11
11
|
"license": "MIT",
|
|
12
|
-
"keywords": [
|
|
13
|
-
"github",
|
|
14
|
-
"pull-request",
|
|
15
|
-
"ci",
|
|
16
|
-
"code-review",
|
|
17
|
-
"automation"
|
|
18
|
-
],
|
|
12
|
+
"keywords": ["github", "pull-request", "ci", "code-review", "automation"],
|
|
19
13
|
"skills": "./plugin/skills/"
|
|
20
14
|
}
|
package/README.md
CHANGED
|
@@ -20,9 +20,9 @@ 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
|
|
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 pr-shepherd command and explicit goal-friendly iterate instructions because Codex does not provide `/loop` scheduling in this workflow. Generated commands use `cli.runner` from `.pr-shepherdrc.yml`: `auto` (default), `npx`, `pnpm`, or `yarn`.
|
|
24
24
|
|
|
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)_:
|
|
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; examples use `npx` as the default spelling — a pnpm or yarn repo would emit `pnpm exec` or `yarn run` instead)_:
|
|
26
26
|
|
|
27
27
|
```
|
|
28
28
|
/pr-shepherd:monitor
|
|
@@ -32,14 +32,14 @@ At a high level, to start the monitor, the skill/command invokes a CLI that retu
|
|
|
32
32
|
# PR #123 [MONITOR]
|
|
33
33
|
|
|
34
34
|
Loop tag: `#pr-shepherd-loop:pr=123:`
|
|
35
|
-
Loop args: `4m`
|
|
36
35
|
|
|
37
36
|
## Loop prompt
|
|
38
37
|
|
|
39
38
|
#pr-shepherd-loop:pr=123:
|
|
40
39
|
|
|
41
|
-
**IMPORTANT — recurrence rules:** Do not
|
|
42
|
-
|
|
40
|
+
**IMPORTANT — dynamic recurrence rules:** Do not invoke `/loop` again from inside
|
|
41
|
+
this prompt. For nonterminal iterations, call `ScheduleWakeup` with `delaySeconds`
|
|
42
|
+
between 60 and 240 and this same prompt body.
|
|
43
43
|
|
|
44
44
|
Run in a single Bash call:
|
|
45
45
|
npx pr-shepherd 123
|
|
@@ -48,11 +48,10 @@ Run in a single Bash call:
|
|
|
48
48
|
|
|
49
49
|
## Instructions
|
|
50
50
|
|
|
51
|
-
1.
|
|
52
|
-
2. Otherwise, invoke the /loop skill with Loop args and the full ## Loop prompt body.
|
|
51
|
+
1. Invoke the /loop skill with the full ## Loop prompt body and no fixed interval.
|
|
53
52
|
```
|
|
54
53
|
|
|
55
|
-
Each iteration calls `
|
|
54
|
+
Each iteration calls `pr-shepherd <PR>` through the selected package runner, which provides actionable feedback directly to the agent:
|
|
56
55
|
|
|
57
56
|
```
|
|
58
57
|
> npx pr-shepherd 123
|
|
@@ -127,7 +126,7 @@ Some other workflow improvements:
|
|
|
127
126
|
|
|
128
127
|
Recommendations:
|
|
129
128
|
|
|
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
|
|
129
|
+
- 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 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).
|
|
131
130
|
- 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.
|
|
132
131
|
- 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.
|
|
133
132
|
- Avoid having automation edit comments, reviews, or threads in place because updated items get minimized. Instead, always make a new review, comment, thread, etc.
|
|
@@ -144,16 +143,15 @@ Recommendations:
|
|
|
144
143
|
|
|
145
144
|
### Monitor a PR
|
|
146
145
|
|
|
147
|
-
In Claude Code, creates a
|
|
146
|
+
In Claude Code, creates a fixed-interval loop (default every 4 minutes, configurable via `watch.interval`) that checks CI and review comments, fixes issues, and marks the PR ready for review when clean. The loop runs as a cron job that fires on schedule — agents end each turn cleanly and the cron fires again automatically. The loop stops automatically when the PR is merged or closed (auto-expires after 7 days).
|
|
148
147
|
|
|
149
|
-
In Codex, run `
|
|
148
|
+
In Codex, run `pr-shepherd monitor <PR>` once through the repo package runner to emit the goal-friendly recurrence prompt, then follow that prompt. Its reusable follow-up command is emitted by the CLI.
|
|
150
149
|
|
|
151
150
|
Claude Code:
|
|
152
151
|
|
|
153
152
|
```
|
|
154
153
|
/pr-shepherd:monitor # infer PR from current branch
|
|
155
154
|
/pr-shepherd:monitor 42
|
|
156
|
-
/pr-shepherd:monitor 42 every 8m
|
|
157
155
|
/pr-shepherd:monitor 42 --ready-delay 15m
|
|
158
156
|
```
|
|
159
157
|
|
|
@@ -193,11 +191,11 @@ See [docs/skills.md](docs/skills.md) for full argument reference.
|
|
|
193
191
|
|
|
194
192
|
## Iterate decision loop
|
|
195
193
|
|
|
196
|
-
On each tick
|
|
194
|
+
On each dynamic tick: fetch PR state in one GraphQL batch → classify CI, comments, and merge status → take one action (`fix_code`, `mark_ready`, `cancel`, `escalate`, `wait`, or `cooldown`). Claude and Codex both choose a fresh wait between 1 and 4 minutes for each nonterminal recurrence. See [docs/iterate-flow.md](docs/iterate-flow.md) for the decision table and [docs/flow.md](docs/flow.md) for the end-to-end flow diagram.
|
|
197
195
|
|
|
198
196
|
## Install
|
|
199
197
|
|
|
200
|
-
> **Note:** Skill and plugin install methods add the skill definitions only — they do not install the `pr-shepherd` CLI. The skills invoke `
|
|
198
|
+
> **Note:** Skill and plugin install methods add the skill definitions only — they do not install the `pr-shepherd` CLI. The skills invoke `pr-shepherd` through the repo package runner, so you also need the CLI available. If you're using `pr-shepherd` as development tooling for your repo, install it as a dev dependency so the selected runner resolves it without prompting:
|
|
201
199
|
>
|
|
202
200
|
> ```bash
|
|
203
201
|
> npm install --save-dev pr-shepherd
|
|
@@ -275,7 +273,7 @@ Or ask Codex to use the `pr-shepherd` skill, for example: `run pr-shepherd until
|
|
|
275
273
|
npx pr-shepherd 42
|
|
276
274
|
```
|
|
277
275
|
|
|
278
|
-
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). `
|
|
276
|
+
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). `pr-shepherd iterate 42` remains supported for existing workflows. There is no background `/loop` scheduler in Codex.
|
|
279
277
|
|
|
280
278
|
### Without the plugin
|
|
281
279
|
|
|
@@ -7,6 +7,7 @@ export function formatFixCodeResult(header, result, opts) {
|
|
|
7
7
|
const runtime = opts?.runtime ?? "claude";
|
|
8
8
|
const readyDelaySuffix = opts?.readyDelaySuffix;
|
|
9
9
|
const retryInterval = opts?.retryInterval;
|
|
10
|
+
const runner = opts?.runner;
|
|
10
11
|
const sections = [header];
|
|
11
12
|
if (result.fix.threads.length > 0) {
|
|
12
13
|
sections.push("## Review threads");
|
|
@@ -117,7 +118,7 @@ export function formatFixCodeResult(header, result, opts) {
|
|
|
117
118
|
}
|
|
118
119
|
sections.push(postFixLines.join("\n"));
|
|
119
120
|
sections.push("## Instructions");
|
|
120
|
-
sections.push(numberInstructions(adaptFixCodeInstructions(result.fix.instructions, result.pr, runtime, readyDelaySuffix, retryInterval)));
|
|
121
|
+
sections.push(numberInstructions(adaptFixCodeInstructions(result.fix.instructions, result.pr, runtime, readyDelaySuffix, retryInterval, runner)));
|
|
121
122
|
return joinSections(sections);
|
|
122
123
|
}
|
|
123
124
|
function blockquote(body) {
|
package/bin/cli/handlers.mjs
CHANGED
|
@@ -63,7 +63,12 @@ export async function handleIterate(args) {
|
|
|
63
63
|
noAutoMarkReady,
|
|
64
64
|
noAutoCancelActionable,
|
|
65
65
|
});
|
|
66
|
-
const projectionOpts = {
|
|
66
|
+
const projectionOpts = {
|
|
67
|
+
runtime,
|
|
68
|
+
readyDelaySuffix,
|
|
69
|
+
retryInterval: cfg.watch.interval,
|
|
70
|
+
runner: cfg.cli?.runner,
|
|
71
|
+
};
|
|
67
72
|
if (globalOpts.format === "json") {
|
|
68
73
|
const output = globalOpts.verbose
|
|
69
74
|
? projectIterateVerbose(result, projectionOpts)
|
|
@@ -71,10 +76,8 @@ export async function handleIterate(args) {
|
|
|
71
76
|
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
72
77
|
}
|
|
73
78
|
else {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
...projectionOpts,
|
|
77
|
-
})}\n`);
|
|
79
|
+
const text = formatIterateResult(result, { verbose: globalOpts.verbose, ...projectionOpts });
|
|
80
|
+
process.stdout.write(`${text}\n`);
|
|
78
81
|
}
|
|
79
82
|
process.exitCode = iterateActionToExitCode(result.action);
|
|
80
83
|
}
|
|
@@ -21,6 +21,7 @@ export function formatIterateResult(result, opts) {
|
|
|
21
21
|
const runtime = opts?.runtime ?? "claude";
|
|
22
22
|
const readyDelaySuffix = opts?.readyDelaySuffix;
|
|
23
23
|
const retryInterval = opts?.retryInterval;
|
|
24
|
+
const runner = opts?.runner;
|
|
24
25
|
const heading = `# PR #${result.pr} [${result.action.toUpperCase()}]`;
|
|
25
26
|
const reviewDecisionSeg = result.mergeStatus === "BLOCKED" && result.reviewDecision
|
|
26
27
|
? ` · **reviewDecision** \`${result.reviewDecision}\``
|
|
@@ -56,33 +57,38 @@ export function formatIterateResult(result, opts) {
|
|
|
56
57
|
return joinSections([
|
|
57
58
|
verbose ? header : heading,
|
|
58
59
|
adaptIterateLog(result.log, runtime),
|
|
59
|
-
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, retryInterval))}`,
|
|
60
|
+
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, retryInterval, runner))}`,
|
|
60
61
|
]);
|
|
61
62
|
case "wait":
|
|
62
63
|
return joinSections([
|
|
63
64
|
header,
|
|
64
65
|
adaptIterateLog(result.log, runtime),
|
|
65
|
-
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, retryInterval))}`,
|
|
66
|
+
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, retryInterval, runner))}`,
|
|
66
67
|
]);
|
|
67
68
|
case "mark_ready":
|
|
68
69
|
return joinSections([
|
|
69
70
|
header,
|
|
70
71
|
adaptIterateLog(result.log, runtime),
|
|
71
|
-
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, retryInterval))}`,
|
|
72
|
+
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, retryInterval, runner))}`,
|
|
72
73
|
]);
|
|
73
74
|
case "cancel":
|
|
74
75
|
return joinSections([
|
|
75
76
|
[`${heading} — ${result.reason}`, "", baseLine, summaryLine].join("\n"),
|
|
76
77
|
adaptIterateLog(result.log, runtime),
|
|
77
|
-
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, retryInterval))}`,
|
|
78
|
+
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, retryInterval, runner))}`,
|
|
78
79
|
]);
|
|
79
80
|
case "escalate":
|
|
80
81
|
return joinSections([
|
|
81
82
|
header,
|
|
82
83
|
result.escalate.humanMessage,
|
|
83
|
-
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, retryInterval))}`,
|
|
84
|
+
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, retryInterval, runner))}`,
|
|
84
85
|
]);
|
|
85
86
|
case "fix_code":
|
|
86
|
-
return formatFixCodeResult(header, result, {
|
|
87
|
+
return formatFixCodeResult(header, result, {
|
|
88
|
+
runtime,
|
|
89
|
+
readyDelaySuffix,
|
|
90
|
+
retryInterval,
|
|
91
|
+
runner,
|
|
92
|
+
});
|
|
87
93
|
}
|
|
88
94
|
}
|
|
@@ -1,48 +1,50 @@
|
|
|
1
1
|
import { FIX_INSTRUCTION_END_ITERATION, FIX_INSTRUCTION_STOP_AFTER_PUSH, FIX_INSTRUCTION_STOP_BEFORE_NEXT_TICK, } from "../commands/iterate/render.mjs";
|
|
2
|
+
import { buildPrShepherdCommand } from "./runner.mjs";
|
|
2
3
|
const DEFAULT_CODEX_RETRY_INTERVAL = "4m";
|
|
3
4
|
const VALID_RETRY_INTERVAL = /^\d+[smhd]$/;
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
const CLAUDE_DYNAMIC_WAKEUP = "Schedule the next dynamic wakeup with `ScheduleWakeup` using `delaySeconds` between 60 and 240 and the same loop prompt (skip if running under a fixed-interval cron loop — the next scheduled fire handles it), then end this iteration.";
|
|
6
|
+
export function buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, retryInterval, runner) {
|
|
7
|
+
const rerunCommand = buildCodexIterateCommand(result.pr, readyDelaySuffix, runner);
|
|
6
8
|
const retryDelay = buildCodexRetryDelay(retryInterval);
|
|
7
9
|
switch (result.action) {
|
|
8
10
|
case "cooldown":
|
|
9
11
|
return [
|
|
10
12
|
runtime === "codex"
|
|
11
13
|
? `Continue the active Codex goal — wait about ${retryDelay}, then rerun \`${rerunCommand}\` after CI starts reporting.`
|
|
12
|
-
:
|
|
14
|
+
: `CI still needs time to start reporting. ${CLAUDE_DYNAMIC_WAKEUP}`,
|
|
13
15
|
];
|
|
14
16
|
case "wait":
|
|
15
17
|
return [
|
|
16
18
|
runtime === "codex"
|
|
17
19
|
? `Continue the active Codex goal — wait about ${retryDelay}, then rerun \`${rerunCommand}\` to recheck.`
|
|
18
|
-
:
|
|
20
|
+
: CLAUDE_DYNAMIC_WAKEUP,
|
|
19
21
|
];
|
|
20
22
|
case "mark_ready":
|
|
21
23
|
return [
|
|
22
24
|
runtime === "codex"
|
|
23
25
|
? `The CLI already marked the PR ready for review. Continue the active Codex goal until the ready-delay completes — wait about ${retryDelay}, then rerun \`${rerunCommand}\` to recheck.`
|
|
24
|
-
:
|
|
26
|
+
: `The CLI already marked the PR ready for review. ${CLAUDE_DYNAMIC_WAKEUP}`,
|
|
25
27
|
];
|
|
26
28
|
case "cancel":
|
|
27
29
|
return [
|
|
28
30
|
runtime === "codex"
|
|
29
31
|
? "Stop — no recurring Codex monitor is running to cancel."
|
|
30
|
-
:
|
|
32
|
+
: `Stop — do not schedule another dynamic wakeup. If this loop was started with a fixed-interval \`/loop\` schedule, call \`CronList\`, find the job whose prompt contains \`#pr-shepherd-loop:pr=${result.pr}:\`, and cancel it with \`CronDelete\`.`,
|
|
31
33
|
"Stop.",
|
|
32
34
|
];
|
|
33
35
|
case "escalate":
|
|
34
36
|
return [
|
|
35
37
|
runtime === "codex"
|
|
36
38
|
? "Stop — no recurring Codex monitor is running to cancel."
|
|
37
|
-
:
|
|
39
|
+
: `Stop — do not schedule another dynamic wakeup. If this loop was started with a fixed-interval \`/loop\` schedule, call \`CronList\`, find the job whose prompt contains \`#pr-shepherd-loop:pr=${result.pr}:\`, and cancel it with \`CronDelete\`.`,
|
|
38
40
|
"Stop — the PR needs human direction before monitoring can resume.",
|
|
39
41
|
];
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
|
-
export function adaptFixCodeInstructions(instructions, pr, runtime, readyDelaySuffix, retryInterval) {
|
|
44
|
+
export function adaptFixCodeInstructions(instructions, pr, runtime, readyDelaySuffix, retryInterval, runner) {
|
|
43
45
|
if (runtime !== "codex")
|
|
44
46
|
return instructions;
|
|
45
|
-
const rerunCommand = buildCodexIterateCommand(pr, readyDelaySuffix);
|
|
47
|
+
const rerunCommand = buildCodexIterateCommand(pr, readyDelaySuffix, runner);
|
|
46
48
|
const retryDelay = buildCodexRetryDelay(retryInterval);
|
|
47
49
|
return instructions.map((instruction) => {
|
|
48
50
|
if (instruction === FIX_INSTRUCTION_STOP_AFTER_PUSH) {
|
|
@@ -60,9 +62,11 @@ export function adaptIterateLog(log, runtime) {
|
|
|
60
62
|
return log;
|
|
61
63
|
return log.replace(/\s+—\s+\d+s until auto-cancel/g, "");
|
|
62
64
|
}
|
|
63
|
-
export function buildCodexIterateCommand(pr, readyDelaySuffix) {
|
|
65
|
+
export function buildCodexIterateCommand(pr, readyDelaySuffix, runner) {
|
|
64
66
|
const suffix = readyDelaySuffix?.trim();
|
|
65
|
-
return
|
|
67
|
+
return buildPrShepherdCommand([String(pr), ...(suffix ? ["--ready-delay", suffix] : [])], {
|
|
68
|
+
runner,
|
|
69
|
+
}).text;
|
|
66
70
|
}
|
|
67
71
|
export function buildCodexRetryDelay(retryInterval) {
|
|
68
72
|
const interval = typeof retryInterval === "string" ? retryInterval.trim() : "";
|
package/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -8,7 +8,8 @@ export function projectIterateLean(result, opts) {
|
|
|
8
8
|
const runtime = opts?.runtime ?? "claude";
|
|
9
9
|
const readyDelaySuffix = opts?.readyDelaySuffix;
|
|
10
10
|
const retryInterval = opts?.retryInterval;
|
|
11
|
-
const
|
|
11
|
+
const runner = opts?.runner;
|
|
12
|
+
const simpleInstructions = (r) => buildSimpleIterateInstructions(r, runtime, readyDelaySuffix, retryInterval, runner);
|
|
12
13
|
const base = {
|
|
13
14
|
action: result.action,
|
|
14
15
|
pr: result.pr,
|
|
@@ -101,7 +102,7 @@ export function projectIterateLean(result, opts) {
|
|
|
101
102
|
}),
|
|
102
103
|
resolveCommand: result.fix.resolveCommand,
|
|
103
104
|
...(result.fix.instructions.length > 0 && {
|
|
104
|
-
instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, runtime, readyDelaySuffix, retryInterval),
|
|
105
|
+
instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, runtime, readyDelaySuffix, retryInterval, runner),
|
|
105
106
|
}),
|
|
106
107
|
},
|
|
107
108
|
};
|
|
@@ -134,12 +135,13 @@ export function projectIterateVerbose(result, opts) {
|
|
|
134
135
|
const runtime = opts?.runtime ?? "claude";
|
|
135
136
|
const readyDelaySuffix = opts?.readyDelaySuffix;
|
|
136
137
|
const retryInterval = opts?.retryInterval;
|
|
138
|
+
const runner = opts?.runner;
|
|
137
139
|
if (result.action === "fix_code") {
|
|
138
140
|
return {
|
|
139
141
|
...result,
|
|
140
142
|
fix: {
|
|
141
143
|
...result.fix,
|
|
142
|
-
instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, runtime, readyDelaySuffix, retryInterval),
|
|
144
|
+
instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, runtime, readyDelaySuffix, retryInterval, runner),
|
|
143
145
|
},
|
|
144
146
|
};
|
|
145
147
|
}
|
|
@@ -149,6 +151,6 @@ export function projectIterateVerbose(result, opts) {
|
|
|
149
151
|
return {
|
|
150
152
|
...result,
|
|
151
153
|
...log,
|
|
152
|
-
instructions: buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, retryInterval),
|
|
154
|
+
instructions: buildSimpleIterateInstructions(result, runtime, readyDelaySuffix, retryInterval, runner),
|
|
153
155
|
};
|
|
154
156
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
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);
|
|
7
|
+
return { argv, text: renderShellCommand(argv) };
|
|
8
|
+
}
|
|
9
|
+
export function resolveCliRunner(runner, cwd = process.cwd()) {
|
|
10
|
+
const configured = parseCliRunner(runner);
|
|
11
|
+
return configured === "auto" ? detectPackageRunner(cwd) : configured;
|
|
12
|
+
}
|
|
13
|
+
export function parseCliRunner(runner) {
|
|
14
|
+
if (runner === undefined)
|
|
15
|
+
return "auto";
|
|
16
|
+
if (typeof runner !== "string") {
|
|
17
|
+
throw new Error(`Invalid config: cli.runner must be one of "auto", "npx", "pnpm", or "yarn", got ${JSON.stringify(runner)}`);
|
|
18
|
+
}
|
|
19
|
+
const value = runner.trim();
|
|
20
|
+
if (value === "auto" || value === "npx" || value === "pnpm" || value === "yarn")
|
|
21
|
+
return value;
|
|
22
|
+
throw new Error(`Invalid config: cli.runner must be one of "auto", "npx", "pnpm", or "yarn", got ${JSON.stringify(runner)}`);
|
|
23
|
+
}
|
|
24
|
+
export function renderShellCommand(argv) {
|
|
25
|
+
return argv.map(renderShellArg).join(" ");
|
|
26
|
+
}
|
|
27
|
+
function baseArgvForRunner(runner) {
|
|
28
|
+
switch (runner) {
|
|
29
|
+
case "npx":
|
|
30
|
+
return ["npx", "pr-shepherd"];
|
|
31
|
+
case "pnpm":
|
|
32
|
+
return ["pnpm", "exec", "pr-shepherd"];
|
|
33
|
+
case "yarn":
|
|
34
|
+
return ["yarn", "run", "pr-shepherd"];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const runnerCache = new Map();
|
|
38
|
+
export function __resetRunnerCache() {
|
|
39
|
+
runnerCache.clear();
|
|
40
|
+
}
|
|
41
|
+
function detectPackageRunner(startDir) {
|
|
42
|
+
const cached = runnerCache.get(startDir);
|
|
43
|
+
if (cached)
|
|
44
|
+
return cached;
|
|
45
|
+
const home = homedir();
|
|
46
|
+
const repoRoot = findRepoRoot(startDir);
|
|
47
|
+
let current = startDir;
|
|
48
|
+
while (true) {
|
|
49
|
+
const atBoundary = current === repoRoot || current === home || current === dirname(current);
|
|
50
|
+
// Read signals for this directory unless it is home without being the repo root.
|
|
51
|
+
// This lets dotfiles repos (repoRoot === home) detect their own package manager
|
|
52
|
+
// while still preventing home's lockfiles from influencing unrelated projects.
|
|
53
|
+
if (!atBoundary || current === repoRoot) {
|
|
54
|
+
const packageManager = readPackageManager(current);
|
|
55
|
+
if (packageManager?.startsWith("pnpm@"))
|
|
56
|
+
return cacheRunner(startDir, "pnpm");
|
|
57
|
+
if (packageManager?.startsWith("yarn@"))
|
|
58
|
+
return cacheRunner(startDir, "yarn");
|
|
59
|
+
if (packageManager?.startsWith("npm@"))
|
|
60
|
+
return cacheRunner(startDir, "npx");
|
|
61
|
+
if (isFile(join(current, "pnpm-lock.yaml")))
|
|
62
|
+
return cacheRunner(startDir, "pnpm");
|
|
63
|
+
if (isFile(join(current, "yarn.lock")))
|
|
64
|
+
return cacheRunner(startDir, "yarn");
|
|
65
|
+
if (isFile(join(current, "package-lock.json")))
|
|
66
|
+
return cacheRunner(startDir, "npx");
|
|
67
|
+
}
|
|
68
|
+
if (atBoundary)
|
|
69
|
+
return cacheRunner(startDir, "npx");
|
|
70
|
+
current = dirname(current);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function cacheRunner(startDir, runner) {
|
|
74
|
+
runnerCache.set(startDir, runner);
|
|
75
|
+
return runner;
|
|
76
|
+
}
|
|
77
|
+
function readPackageManager(packageDir) {
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
|
|
80
|
+
return typeof parsed.packageManager === "string" ? parsed.packageManager.trim() : null;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function findRepoRoot(startDir) {
|
|
87
|
+
let current = startDir;
|
|
88
|
+
while (true) {
|
|
89
|
+
if (statSync(join(current, ".git"), { throwIfNoEntry: false }))
|
|
90
|
+
return current;
|
|
91
|
+
if (current === dirname(current))
|
|
92
|
+
return null;
|
|
93
|
+
current = dirname(current);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function isFile(path) {
|
|
97
|
+
return statSync(path, { throwIfNoEntry: false })?.isFile() === true;
|
|
98
|
+
}
|
|
99
|
+
function renderShellArg(arg) {
|
|
100
|
+
if (/^\$[A-Z_][A-Z0-9_]*$/.test(arg))
|
|
101
|
+
return `"${arg}"`;
|
|
102
|
+
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(arg))
|
|
103
|
+
return arg;
|
|
104
|
+
if (!/["$`\\]/.test(arg))
|
|
105
|
+
return `"${arg}"`;
|
|
106
|
+
if (!arg.includes("'"))
|
|
107
|
+
return `'${arg}'`;
|
|
108
|
+
throw new Error(`Unexpected character in shell arg: ${JSON.stringify(arg)}`);
|
|
109
|
+
}
|
package/bin/cli-parser.mjs
CHANGED
|
@@ -31,6 +31,7 @@ import { formatFetchResult, formatMutateResult } from "./cli/formatters.mjs";
|
|
|
31
31
|
import { handleCommitSuggestion, handleIterate, handleMonitor, handleStatus, } from "./cli/handlers.mjs";
|
|
32
32
|
import { setupLog } from "./log/setup.mjs";
|
|
33
33
|
import { detectAgentRuntime } from "./agent-runtime.mjs";
|
|
34
|
+
import { loadConfig } from "./config/load.mjs";
|
|
34
35
|
// ---------------------------------------------------------------------------
|
|
35
36
|
// Entry
|
|
36
37
|
// ---------------------------------------------------------------------------
|
|
@@ -92,10 +93,11 @@ function readVersion() {
|
|
|
92
93
|
async function handleCheck(args) {
|
|
93
94
|
const { prNumber, global: globalOpts } = parseCommonArgs(args);
|
|
94
95
|
const runtime = detectAgentRuntime();
|
|
96
|
+
const cfg = loadConfig();
|
|
95
97
|
const report = await runCheck({ ...globalOpts, prNumber, autoResolve: false });
|
|
96
98
|
const output = globalOpts.format === "json"
|
|
97
|
-
? formatJson(report, { runtime })
|
|
98
|
-
: formatText(report, { runtime });
|
|
99
|
+
? formatJson(report, { runtime, runner: cfg.cli?.runner })
|
|
100
|
+
: formatText(report, { runtime, runner: cfg.cli?.runner });
|
|
99
101
|
process.stdout.write(`${output}\n`);
|
|
100
102
|
process.exitCode = statusToExitCode(report.status);
|
|
101
103
|
}
|
|
@@ -5,6 +5,8 @@ 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
|
+
import { buildPrShepherdCommand } from "../cli/runner.mjs";
|
|
8
10
|
const execFile = promisify(execFileCb);
|
|
9
11
|
export async function runCommitSuggestion(opts) {
|
|
10
12
|
if (!opts.threadId) {
|
|
@@ -91,11 +93,12 @@ export async function runCommitSuggestion(opts) {
|
|
|
91
93
|
`-m ${sq(commitMessageArg)}`,
|
|
92
94
|
...commitBodyArg.split("\n\n").map((p) => `-m ${sq(p)}`),
|
|
93
95
|
].join(" ");
|
|
96
|
+
const resolveCommand = buildPrShepherdCommand(["resolve", String(prNumber), "--resolve-thread-ids", opts.threadId], { runner: loadConfig().cli?.runner }).text;
|
|
94
97
|
const postActionInstructions = [
|
|
95
98
|
`Apply the patch to \`${filePath}\`: run \`git apply\` with the diff shown above, or edit the file directly using the line range (${range}).`,
|
|
96
99
|
`Stage the file: \`git add -- ${quotedPath}\``,
|
|
97
100
|
`Commit: \`${commitCmd}\``,
|
|
98
|
-
`Resolve the thread on GitHub:
|
|
101
|
+
`Resolve the thread on GitHub: \`${resolveCommand}\``,
|
|
99
102
|
`Push when ready: \`git push\` (or \`git push --force-with-lease\` after rebasing).`,
|
|
100
103
|
];
|
|
101
104
|
return {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildPrShepherdCommand } from "../../cli/runner.mjs";
|
|
1
2
|
export function classifyReviewSummaries(summaries, approvals, minimizeApprovals) {
|
|
2
3
|
// First-look and seen summaries go into the minimize mutation; edited summaries do NOT —
|
|
3
4
|
// they are already minimized server-side (body changed after minimize was applied).
|
|
@@ -20,8 +21,8 @@ export function classifyReviewSummaries(summaries, approvals, minimizeApprovals)
|
|
|
20
21
|
surfacedApprovals: approvals,
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
|
-
export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, reviews, checks, prNumber) {
|
|
24
|
-
const argv = ["
|
|
24
|
+
export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, reviews, checks, prNumber, runner) {
|
|
25
|
+
const argv = buildPrShepherdCommand(["resolve", String(prNumber)], { runner }).argv;
|
|
25
26
|
const threadIds = [...threads.map((t) => t.id), ...resolutionOnlyThreads.map((t) => t.id)];
|
|
26
27
|
if (threadIds.length > 0) {
|
|
27
28
|
argv.push("--resolve-thread-ids", threadIds.join(","));
|
|
@@ -6,7 +6,7 @@ import { buildFixInstructions } from "./render.mjs";
|
|
|
6
6
|
import { applyStallGuard } from "./stall.mjs";
|
|
7
7
|
import { tryCancelRun, buildInProgressRunIds } from "./helpers.mjs";
|
|
8
8
|
export async function handleFixCode(ctx) {
|
|
9
|
-
const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, } = ctx;
|
|
9
|
+
const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, cliRunner, } = ctx;
|
|
10
10
|
const failingChecks = report.checks.failing;
|
|
11
11
|
const stored = await readFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber });
|
|
12
12
|
const isNewSha = stored?.headSha !== headSha;
|
|
@@ -57,7 +57,7 @@ export async function handleFixCode(ctx) {
|
|
|
57
57
|
? buildInProgressRunIds(report, cancelledSet)
|
|
58
58
|
: [];
|
|
59
59
|
const allCommentIds = [...actionableComments.map((c) => c.id), ...reviewSummaryIds];
|
|
60
|
-
const resolveCommand = buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, changesRequestedReviews, checks, prNumber);
|
|
60
|
+
const resolveCommand = buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, changesRequestedReviews, checks, prNumber, cliRunner);
|
|
61
61
|
if (baseLookup.isFallback && (resolveCommand.requiresHeadSha || hasConflicts)) {
|
|
62
62
|
const fallbackEscalateBase = {
|
|
63
63
|
triggers: ["base-branch-unknown"],
|
|
@@ -77,7 +77,7 @@ export async function handleFixCode(ctx) {
|
|
|
77
77
|
}
|
|
78
78
|
const firstLookThreads = report.threads.firstLook;
|
|
79
79
|
const firstLookComments = report.comments.firstLook;
|
|
80
|
-
const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prNumber, cancelled.length, firstLookThreads, firstLookComments, firstLookSummaries, editedSummaries, inProgressRunIds, resolutionOnlyThreads);
|
|
80
|
+
const instructions = buildFixInstructions(threads, actionableComments, checks, changesRequestedReviews, baseLookup.branch, resolveCommand, hasConflicts, prNumber, cancelled.length, firstLookThreads, firstLookComments, firstLookSummaries, editedSummaries, inProgressRunIds, resolutionOnlyThreads, cliRunner);
|
|
81
81
|
return applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, {
|
|
82
82
|
...base,
|
|
83
83
|
baseBranch: baseLookup.branch,
|
|
@@ -1,34 +1,36 @@
|
|
|
1
|
+
import { buildPrShepherdCommand, renderShellCommand } from "../../cli/runner.mjs";
|
|
1
2
|
export const FIX_INSTRUCTION_STOP_AFTER_PUSH = "Stop this iteration — CI needs time to run on the new push before the next tick.";
|
|
2
3
|
export const FIX_INSTRUCTION_STOP_BEFORE_NEXT_TICK = "Stop this iteration before the next tick.";
|
|
3
4
|
export const FIX_INSTRUCTION_END_ITERATION = "End this iteration.";
|
|
4
5
|
/**
|
|
5
|
-
* Render a resolve command as a shell snippet. Wraps `$DISMISS_MESSAGE
|
|
6
|
-
* argv entries
|
|
7
|
-
*
|
|
6
|
+
* Render a resolve command as a shell snippet. Wraps `$DISMISS_MESSAGE`, `$HEAD_SHA`, and
|
|
7
|
+
* whitespace-bearing argv entries for placeholder substitution. `$HEAD_SHA` is appended separately
|
|
8
|
+
* when `requiresHeadSha` is set.
|
|
8
9
|
*/
|
|
9
10
|
export function renderResolveCommand(rc) {
|
|
10
|
-
const
|
|
11
|
-
if (arg === "$DISMISS_MESSAGE")
|
|
12
|
-
return true;
|
|
13
|
-
if (/["$`\\]/.test(arg)) {
|
|
14
|
-
throw new Error(`Unexpected character in argv arg that needsQuoting can't handle: ${JSON.stringify(arg)}`);
|
|
15
|
-
}
|
|
16
|
-
return /\s/.test(arg);
|
|
17
|
-
};
|
|
18
|
-
const parts = rc.argv.map((a) => (needsQuoting(a) ? `"${a}"` : a));
|
|
11
|
+
const parts = [...rc.argv];
|
|
19
12
|
if (rc.requiresHeadSha) {
|
|
20
|
-
parts.push("--require-sha",
|
|
13
|
+
parts.push("--require-sha", "$HEAD_SHA");
|
|
21
14
|
}
|
|
22
|
-
return parts
|
|
15
|
+
return renderShellCommand(parts);
|
|
23
16
|
}
|
|
24
|
-
export function buildFixInstructions(threads, actionableComments, checks, reviews, baseBranch, resolveCommand, hasConflicts, prNumber, cancelledCount, firstLookThreads = [], firstLookComments = [], firstLookSummaries = [], editedSummaries = [], inProgressRunIds = [], resolutionOnlyThreads = []) {
|
|
17
|
+
export function buildFixInstructions(threads, actionableComments, checks, reviews, baseBranch, resolveCommand, hasConflicts, prNumber, cancelledCount, firstLookThreads = [], firstLookComments = [], firstLookSummaries = [], editedSummaries = [], inProgressRunIds = [], resolutionOnlyThreads = [], runner) {
|
|
25
18
|
const instructions = [];
|
|
26
19
|
if (inProgressRunIds.length > 0) {
|
|
27
20
|
instructions.push(`Cancel in-progress CI runs first: for each ID under \`## In-progress runs\`, run \`gh run cancel <id>\`. Do this before applying any code fixes — the push at the end of this iteration will supersede those runs anyway, so letting them continue burns CI minutes for results no one will read. If \`gh\` reports a run is already completed, ignore it and continue with the next ID.`);
|
|
28
21
|
}
|
|
29
22
|
const hasSuggestions = threads.some((t) => t.suggestion);
|
|
30
23
|
if (hasSuggestions) {
|
|
31
|
-
|
|
24
|
+
const commitSuggestionCommand = buildPrShepherdCommand([
|
|
25
|
+
"commit-suggestion",
|
|
26
|
+
String(prNumber),
|
|
27
|
+
"--thread-id",
|
|
28
|
+
"<id>",
|
|
29
|
+
"--message",
|
|
30
|
+
"<one-sentence headline>",
|
|
31
|
+
"--format=json",
|
|
32
|
+
], { runner }).text;
|
|
33
|
+
instructions.push(`For each thread marked \`[suggestion]\` under \`## Review threads\`: run \`${commitSuggestionCommand}\` to retrieve the patch and suggested commit. The CLI does not mutate the working tree — apply the patch yourself (run \`git apply\` with the diff shown, or edit the file directly using the line range), then stage the listed file and run the suggested \`git commit\` from the \`## Instructions\` section. Include the thread ID in \`--resolve-thread-ids\` in the \`resolve:\` command below (the thread is not auto-resolved). If the patch fails to apply, fall through to the manual-edit step. Do not retry the same command.`);
|
|
32
34
|
}
|
|
33
35
|
if (threads.length > 0 || actionableComments.length > 0) {
|
|
34
36
|
const suggestionFallback = hasSuggestions
|
package/bin/commands/monitor.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getCurrentPrNumber } from "../github/client.mjs";
|
|
2
2
|
import { loadConfig } from "../config/load.mjs";
|
|
3
3
|
import { joinSections } from "../util/markdown.mjs";
|
|
4
|
+
import { buildPrShepherdCommand } from "../cli/runner.mjs";
|
|
4
5
|
export async function runMonitor(opts) {
|
|
5
6
|
const config = loadConfig();
|
|
6
7
|
const prNumber = opts.prNumber ?? (await getCurrentPrNumber());
|
|
@@ -18,8 +19,8 @@ export async function runMonitor(opts) {
|
|
|
18
19
|
// block depend on this exact string — don't change the format.
|
|
19
20
|
const loopTag = `#pr-shepherd-loop:pr=${prNumber}:`;
|
|
20
21
|
const loopArgs = interval;
|
|
21
|
-
const reusableCommand = buildIterateCommand(prNumber, opts.readyDelaySuffix);
|
|
22
|
-
const loopPrompt = buildLoopPrompt(prNumber, loopTag, reusableCommand, loopArgs, opts.runtime ?? "claude");
|
|
22
|
+
const reusableCommand = buildIterateCommand(prNumber, opts.readyDelaySuffix, config.cli?.runner);
|
|
23
|
+
const loopPrompt = buildLoopPrompt(prNumber, loopTag, reusableCommand, loopArgs, opts.runtime ?? "claude", config.cli?.runner);
|
|
23
24
|
return {
|
|
24
25
|
prNumber,
|
|
25
26
|
loopTag,
|
|
@@ -72,11 +73,11 @@ function validateReadyDelaySuffix(readyDelaySuffix) {
|
|
|
72
73
|
}
|
|
73
74
|
return trimmed;
|
|
74
75
|
}
|
|
75
|
-
function buildIterateCommand(prNumber, readyDelaySuffix) {
|
|
76
|
+
function buildIterateCommand(prNumber, readyDelaySuffix, runner) {
|
|
76
77
|
const validatedDelay = validateReadyDelaySuffix(readyDelaySuffix);
|
|
77
|
-
return
|
|
78
|
+
return buildPrShepherdCommand([String(prNumber), ...(validatedDelay ? ["--ready-delay", validatedDelay] : [])], { runner }).text;
|
|
78
79
|
}
|
|
79
|
-
function buildLoopPrompt(prNumber, loopTag, iterateCmd, loopArgs, runtime = "claude") {
|
|
80
|
+
function buildLoopPrompt(prNumber, loopTag, iterateCmd, loopArgs, runtime = "claude", runner) {
|
|
80
81
|
if (runtime === "codex") {
|
|
81
82
|
return [
|
|
82
83
|
loopTag,
|
|
@@ -85,7 +86,7 @@ function buildLoopPrompt(prNumber, loopTag, iterateCmd, loopArgs, runtime = "cla
|
|
|
85
86
|
"- Run the command below once and follow its `## Instructions` exactly.",
|
|
86
87
|
`- If the output tells you to continue the active Codex goal, wait about the configured interval (${loopArgs}) and rerun the reusable command from the monitor output.`,
|
|
87
88
|
"- Stop only when Shepherd emits `[CANCEL]` because the ready-delay completed or the PR was merged/closed, or when Shepherd emits `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures).",
|
|
88
|
-
|
|
89
|
+
`- Do not call \`/loop\`, \`ScheduleWakeup\`, \`CronCreate\`, or \`${buildPrShepherdCommand(["monitor", String(prNumber)], { runner }).text}\`; Codex recurrence is explicit \`iterate\` command cycles.`,
|
|
89
90
|
"",
|
|
90
91
|
"Run in a single Bash call:",
|
|
91
92
|
` ${iterateCmd}`,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { buildPrShepherdCommand } from "../cli/runner.mjs";
|
|
1
2
|
/**
|
|
2
3
|
* Build the numbered triage/fix/resolve instruction steps for the agent to follow.
|
|
3
4
|
* Steps are conditionally emitted based on what the fetch returned (mirrors
|
|
4
5
|
* `buildFixInstructions` in `commands/iterate/render.mts`).
|
|
5
6
|
*/
|
|
6
|
-
export function buildFetchInstructions(prNumber, result) {
|
|
7
|
+
export function buildFetchInstructions(prNumber, result, runner) {
|
|
7
8
|
const { actionableThreads, resolutionOnlyThreads, firstLookThreads, actionableComments, firstLookComments, changesRequestedReviews, reviewSummaries, commitSuggestionsEnabled, } = result;
|
|
8
9
|
const firstLookTotal = firstLookThreads.length + firstLookComments.length;
|
|
9
10
|
const total = actionableThreads.length +
|
|
@@ -30,7 +31,16 @@ export function buildFetchInstructions(prNumber, result) {
|
|
|
30
31
|
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.`);
|
|
31
32
|
}
|
|
32
33
|
if (hasSuggestions) {
|
|
33
|
-
|
|
34
|
+
const commitSuggestionCommand = buildPrShepherdCommand([
|
|
35
|
+
"commit-suggestion",
|
|
36
|
+
String(prNumber),
|
|
37
|
+
"--thread-id",
|
|
38
|
+
"<id>",
|
|
39
|
+
"--message",
|
|
40
|
+
"<one-sentence headline>",
|
|
41
|
+
"--format=json",
|
|
42
|
+
], { runner }).text;
|
|
43
|
+
instructions.push(`For each Actionable thread marked \`[suggestion]\` in \`## Actionable Review Threads\` above: run \`${commitSuggestionCommand}\` to retrieve the patch and suggested commit. The CLI does not mutate the working tree — apply the patch yourself (run \`git apply\` with the diff shown, or edit the file directly using the line range), then stage the listed file and run the suggested \`git commit\` from the \`## Instructions\` section. Include the thread ID in \`--resolve-thread-ids\` in the resolve command below (the thread is not auto-resolved). If the patch fails to apply (drift since the suggestion was written), fall through to the manual fix step. Do not retry the same \`commit-suggestion\` invocation.`);
|
|
34
44
|
}
|
|
35
45
|
if (hasCodeItems) {
|
|
36
46
|
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,7 +60,10 @@ export function buildFetchInstructions(prNumber, result) {
|
|
|
50
60
|
: reviewSummaries.length > 0
|
|
51
61
|
? ` Review-summary IDs (\`PRR_…\` from \`## Review summaries\`) go into \`--minimize-comment-ids\`.`
|
|
52
62
|
: "";
|
|
53
|
-
|
|
63
|
+
const resolveCommand = `${buildPrShepherdCommand(["resolve", String(prNumber)], {
|
|
64
|
+
runner,
|
|
65
|
+
}).text} [--resolve-thread-ids <ids>] [--minimize-comment-ids <ids>] [--dismiss-review-ids <ids> --message "<reason>"]`;
|
|
66
|
+
instructions.push(`Run \`${resolveCommand}\` with only the non-empty flag subsets. Skip the command entirely if all three ID lists are empty.${requireShaHint}${dismissNote}`);
|
|
54
67
|
instructions.push(`For any large decisions or rejections you made this iteration, add or update a \`## Shepherd Journal\` section in the PR description (\`gh pr edit ${prNumber} --body …\`) summarizing each decision. For threads and comments, use the markdown link shown in each item's bullet above; for reviews, reference the review ID.`);
|
|
55
68
|
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".`);
|
|
56
69
|
return instructions;
|
package/bin/commands/resolve.mjs
CHANGED
|
@@ -126,7 +126,7 @@ export async function runResolveFetch(opts) {
|
|
|
126
126
|
reviewSummaries: cfg.resolve.fetchReviewSummaries ? data.reviewSummaries : [],
|
|
127
127
|
commitSuggestionsEnabled: cfg.actions.commitSuggestions,
|
|
128
128
|
};
|
|
129
|
-
return { ...result, instructions: buildFetchInstructions(prNumber, result) };
|
|
129
|
+
return { ...result, instructions: buildFetchInstructions(prNumber, result, cfg.cli?.runner) };
|
|
130
130
|
}
|
|
131
131
|
export async function runResolveMutate(opts) {
|
|
132
132
|
const repo = await getRepoInfo();
|
package/bin/config/load.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { join, dirname } from "node:path";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { parse } from "yaml";
|
|
5
5
|
import builtins from "../config.json" with { type: "json" };
|
|
6
|
+
import { parseCliRunner } from "../cli/runner.mjs";
|
|
6
7
|
const RC_FILENAME = ".pr-shepherdrc.yml";
|
|
7
8
|
function findRcFile(startDir) {
|
|
8
9
|
const home = homedir();
|
|
@@ -51,6 +52,15 @@ export function loadConfig() {
|
|
|
51
52
|
const raw = readFileSync(rcPath, "utf8");
|
|
52
53
|
const parsed = (parse(raw) ?? {});
|
|
53
54
|
const config = deepMerge(defaults, parsed);
|
|
55
|
+
// Validate cli and watch at load time so misconfigurations are caught once
|
|
56
|
+
// with the rc file path in context, rather than during instruction rendering.
|
|
57
|
+
if (config.cli === null || Array.isArray(config.cli) || typeof config.cli !== "object") {
|
|
58
|
+
throw new Error(`Invalid config: cli must be a plain object, got ${JSON.stringify(config.cli)}`);
|
|
59
|
+
}
|
|
60
|
+
config.cli.runner = parseCliRunner(config.cli.runner);
|
|
61
|
+
if (typeof config.watch?.interval !== "string" || !/^\d+[smhd]$/.test(config.watch.interval)) {
|
|
62
|
+
throw new Error(`Invalid config: watch.interval must be a duration string like "4m" or "1h", got ${JSON.stringify(config.watch?.interval)}`);
|
|
63
|
+
}
|
|
54
64
|
configCache.set(cwd, config);
|
|
55
65
|
return config;
|
|
56
66
|
}
|
package/bin/config.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildPrShepherdCommand } from "../cli/runner.mjs";
|
|
1
2
|
/**
|
|
2
3
|
* Build the numbered instruction steps for the agent to follow after a `check` run.
|
|
3
4
|
* All rebase policy, CI budget policy, and ready-to-merge gating live here so the
|
|
@@ -59,7 +60,7 @@ export function buildCheckInstructions(report, opts) {
|
|
|
59
60
|
// 5. Continuous monitoring pointer (suppressed only when truly ready to merge)
|
|
60
61
|
if (!isReady) {
|
|
61
62
|
instructions.push(runtime === "codex"
|
|
62
|
-
? `This is a one-shot check. For follow-up monitoring, run
|
|
63
|
+
? `This is a one-shot check. For follow-up monitoring, run \`${buildPrShepherdCommand([String(report.pr)], { runner: opts?.runner }).text}\`.`
|
|
63
64
|
: "This is a one-shot check. For continuous monitoring that acts on these signals automatically, use `/pr-shepherd:monitor`.");
|
|
64
65
|
}
|
|
65
66
|
return instructions;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-shepherd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jonathan Ong",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@types/node": "^25.6.0",
|
|
30
30
|
"@vitest/coverage-v8": "^4.1.4",
|
|
31
31
|
"husky": "^9.1.7",
|
|
32
|
-
"oxfmt": "^0.
|
|
32
|
+
"oxfmt": "^0.47.0",
|
|
33
33
|
"oxlint": "^1.60.0",
|
|
34
34
|
"typescript": "^6.0.3",
|
|
35
35
|
"vitest": "^4.1.4"
|
|
@@ -4,7 +4,18 @@ description: "Start continuous CI monitoring — marks PR ready for review when
|
|
|
4
4
|
argument-hint: "[PR number or URL]"
|
|
5
5
|
user-invocable: true
|
|
6
6
|
allowed-tools:
|
|
7
|
-
[
|
|
7
|
+
[
|
|
8
|
+
"Bash",
|
|
9
|
+
"Read",
|
|
10
|
+
"Grep",
|
|
11
|
+
"Edit",
|
|
12
|
+
"Write",
|
|
13
|
+
"Glob",
|
|
14
|
+
"Skill",
|
|
15
|
+
"ScheduleWakeup",
|
|
16
|
+
"CronList",
|
|
17
|
+
"CronDelete",
|
|
18
|
+
]
|
|
8
19
|
---
|
|
9
20
|
|
|
10
21
|
# pr-shepherd monitor — Continuous PR Monitor
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-shepherd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Autonomous PR CI monitor and review-comment resolver for Codex.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Jonathan Ong",
|
|
@@ -10,14 +10,7 @@
|
|
|
10
10
|
"homepage": "https://github.com/jonathanong/pr-shepherd",
|
|
11
11
|
"repository": "https://github.com/jonathanong/pr-shepherd",
|
|
12
12
|
"license": "MIT",
|
|
13
|
-
"keywords": [
|
|
14
|
-
"github",
|
|
15
|
-
"pull-request",
|
|
16
|
-
"ci",
|
|
17
|
-
"code-review",
|
|
18
|
-
"automation",
|
|
19
|
-
"codex"
|
|
20
|
-
],
|
|
13
|
+
"keywords": ["github", "pull-request", "ci", "code-review", "automation", "codex"],
|
|
21
14
|
"skills": "../skills/",
|
|
22
15
|
"interface": {
|
|
23
16
|
"displayName": "pr-shepherd",
|
|
@@ -25,10 +18,7 @@
|
|
|
25
18
|
"longDescription": "Use pr-shepherd from Codex to check pull requests, resolve review feedback, and keep explicit iterate cycles running until a PR is ready, merged, closed, or escalated.",
|
|
26
19
|
"developerName": "Jonathan Ong",
|
|
27
20
|
"category": "Coding",
|
|
28
|
-
"capabilities": [
|
|
29
|
-
"Interactive",
|
|
30
|
-
"Write"
|
|
31
|
-
],
|
|
21
|
+
"capabilities": ["Interactive", "Write"],
|
|
32
22
|
"websiteURL": "https://github.com/jonathanong/pr-shepherd",
|
|
33
23
|
"privacyPolicyURL": "https://github.com/jonathanong/pr-shepherd",
|
|
34
24
|
"termsOfServiceURL": "https://github.com/jonathanong/pr-shepherd",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pr-shepherd
|
|
3
|
-
description: 'Codex-only skill for checking, updating, monitoring, or resolving a GitHub pull request with pr-shepherd. Use for requests like "check this PR", "use pr-shepherd", "iterate PR #123", "resolve this PR''s comments", or "run pr-shepherd until this PR is ready". For open-ended requests, create a Codex goal and run explicit `npx --no-install pr-shepherd PR_NUMBER` cycles
|
|
3
|
+
description: 'Codex-only skill for checking, updating, monitoring, or resolving a GitHub pull request with pr-shepherd. Use for requests like "check this PR", "use pr-shepherd", "iterate PR #123", "resolve this PR''s comments", or "run pr-shepherd until this PR is ready". For open-ended requests, create a Codex goal and run explicit `npx --no-install pr-shepherd PR_NUMBER` cycles, picking a fresh sleep/timeout between 1 and 4 minutes before each rerun until Shepherd emits `[CANCEL]` for ready-delay completion or merged/closed, or `[ESCALATE]` including repeated unchanged CI failures.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# pr-shepherd
|
|
@@ -19,7 +19,7 @@ Codex-only workflow for getting actionable PR updates from `pr-shepherd`.
|
|
|
19
19
|
2. Decide whether this is one cycle or an open-ended goal.
|
|
20
20
|
- For one-off requests such as "check this PR", "run pr-shepherd once", or "resolve this PR's comments", run one explicit CLI command.
|
|
21
21
|
- For requests such as "continue", "until ready", "until this PR is ready", or "keep iterating", create a Codex goal before the first recurring cycle with this objective:
|
|
22
|
-
`Run npx --no-install pr-shepherd PR_NUMBER cycles
|
|
22
|
+
`Run npx --no-install pr-shepherd PR_NUMBER cycles, picking a fresh sleep/timeout between 1 and 4 minutes before each rerun, until Shepherd emits [CANCEL] for ready-delay completion or PR #PR_NUMBER is merged/closed, or pr-shepherd escalates, including repeated unchanged CI failures.`
|
|
23
23
|
|
|
24
24
|
3. Verify the CLI is available.
|
|
25
25
|
- In the pr-shepherd source checkout, before any `npx --no-install pr-shepherd` invocation, verify `bin/` and `node_modules/` exist. If either is missing, run:
|
|
@@ -41,7 +41,7 @@ Codex-only workflow for getting actionable PR updates from `pr-shepherd`.
|
|
|
41
41
|
|
|
42
42
|
6. Do not call `/loop`, `ScheduleWakeup`, `CronCreate`, or `npx pr-shepherd monitor` for recurrence. Codex does explicit `pr-shepherd PR_NUMBER` cycles.
|
|
43
43
|
|
|
44
|
-
7. For open-ended goal requests, complete the CLI-provided instructions for the current cycle. If the output says to continue the active Codex goal,
|
|
44
|
+
7. For open-ended goal requests, complete the CLI-provided instructions for the current cycle. If the output says to continue the active Codex goal, pick a fresh sleep/timeout between 1 and 4 minutes, wait that long, and run another explicit `pr-shepherd PR_NUMBER` cycle.
|
|
45
45
|
|
|
46
46
|
8. Do not stop an open-ended goal only because the output is `[WAIT]`, `[COOLDOWN]`, `[MARK_READY]`, or a post-fix CI wait. These are nonterminal Codex recurrence states.
|
|
47
47
|
|