pr-shepherd 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +6 -12
- package/bin/cli/args.mjs +2 -0
- package/bin/cli/default-iterate.mjs +4 -3
- package/bin/cli/duration-flag.mjs +22 -0
- package/bin/cli/exit-codes.mjs +14 -0
- package/bin/cli/fix-formatter.mjs +1 -2
- package/bin/cli/handlers.mjs +19 -38
- package/bin/cli/help.mjs +40 -0
- package/bin/cli/iterate-emitter.mjs +19 -0
- package/bin/cli/iterate-flags.mjs +21 -0
- package/bin/cli/iterate-formatter.mjs +50 -13
- package/bin/cli/iterate-instructions.mjs +10 -16
- package/bin/cli/iterate-lean.mjs +9 -12
- package/bin/cli/poll-handler.mjs +42 -0
- package/bin/cli-parser.iterate-fix.test-support.mjs +0 -4
- package/bin/cli-parser.iterate-fixtures.mjs +4 -1
- package/bin/cli-parser.iterate.test-support.mjs +0 -4
- package/bin/cli-parser.mjs +30 -5
- package/bin/commands/check-terminal-report.mjs +1 -0
- package/bin/commands/check.mjs +1 -0
- package/bin/commands/check.test-support.mjs +1 -0
- package/bin/commands/commit-suggestion.apply.test-support.mjs +1 -0
- package/bin/commands/commit-suggestion.test-support.mjs +1 -0
- package/bin/commands/iterate/check-instructions.mjs +19 -17
- package/bin/commands/iterate/escalate.mjs +17 -21
- package/bin/commands/iterate/fix-code.mjs +22 -11
- package/bin/commands/iterate/index.mjs +2 -0
- package/bin/commands/iterate/render.mjs +50 -45
- package/bin/commands/iterate-test-support.mjs +1 -0
- package/bin/commands/poll.mjs +32 -0
- package/bin/commands/poll.test-support.mjs +77 -0
- package/bin/commands/resolve-instructions.mjs +2 -5
- package/bin/github/batch-parser-helpers.mjs +38 -0
- package/bin/github/batch-parsers.mjs +12 -38
- package/bin/github/gql/batch-pr.gql +9 -0
- package/bin/state/seen-comments.mjs +29 -15
- package/package.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +13 -2
- package/bin/agent-runtime.mjs +0 -7
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ 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 emits
|
|
23
|
+
The CLI emits unified recheck instructions. Generated commands use `cli.runner` from `.pr-shepherdrc.yml`: `auto` (default), `npx`, `pnpm`, `yarn`, or `bun`.
|
|
24
24
|
|
|
25
25
|
At a high level, the skill invokes `pr-shepherd <PR>` through the selected package runner, which provides actionable feedback directly to the agent:
|
|
26
26
|
|
|
@@ -97,7 +97,7 @@ Some other workflow improvements:
|
|
|
97
97
|
|
|
98
98
|
Recommendations:
|
|
99
99
|
|
|
100
|
-
- Run `pr-shepherd` on all your PRs before you go to sleep so that you wake up to reviewable PRs. Keep an active goal cycling the reusable command until Shepherd emits `[CANCEL]` for ready-delay completion or merged/closed, or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures).
|
|
100
|
+
- Run `pr-shepherd` on all your PRs before you go to sleep so that you wake up to reviewable PRs. Keep an active goal cycling the reusable command until Shepherd emits `[CANCEL]` for ready-delay completion or merged/closed, or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures).
|
|
101
101
|
- 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.
|
|
102
102
|
- 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.
|
|
103
103
|
- Avoid having automation edit comments, reviews, or threads in place because updated items get minimized. Instead, always make a new review, comment, thread, etc.
|
|
@@ -134,7 +134,7 @@ npx pr-shepherd iterate 42 # legacy-compatible spelling
|
|
|
134
134
|
|
|
135
135
|
## Iterate decision loop
|
|
136
136
|
|
|
137
|
-
On each tick: fetch PR state in one GraphQL batch → classify CI, comments, and merge status → take one action (`fix_code`, `mark_ready`, `cancel`, `escalate`, or `wait`).
|
|
137
|
+
On each tick: fetch PR state in one GraphQL batch → classify CI, comments, and merge status → take one action (`fix_code`, `mark_ready`, `cancel`, `escalate`, or `wait`). 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.
|
|
138
138
|
|
|
139
139
|
## Cleaning state
|
|
140
140
|
|
|
@@ -216,13 +216,7 @@ npm install --save-dev pr-shepherd
|
|
|
216
216
|
|
|
217
217
|
The plugin only installs the skill; it does not install the CLI into target repositories. To install the CLI globally instead, use `npm install -g pr-shepherd`.
|
|
218
218
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
```bash
|
|
222
|
-
export AGENT=codex
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
Then iterate a PR from Codex with the target repository's package runner:
|
|
219
|
+
Iterate a PR from Codex with the target repository's package runner:
|
|
226
220
|
|
|
227
221
|
```bash
|
|
228
222
|
<runner> pr-shepherd iterate 42
|
|
@@ -230,7 +224,7 @@ Then iterate a PR from Codex with the target repository's package runner:
|
|
|
230
224
|
|
|
231
225
|
For example, a repo like `~/filaments` that declares `packageManager: "pnpm@..."` and has `pnpm-lock.yaml` should use `pnpm exec pr-shepherd iterate 42`. For Bun repos (with `bun.lock` or `bun.lockb`), use `bunx pr-shepherd iterate 42`. For npm repos, use `npx pr-shepherd iterate 42`.
|
|
232
226
|
|
|
233
|
-
Or ask Codex to use the `pr-shepherd` skill, for example: `run pr-shepherd until this PR is ready`. Follow the output's `## Instructions`.
|
|
227
|
+
Or ask Codex to use the `pr-shepherd` skill, for example: `run pr-shepherd until this PR is ready`. Follow the output's `## Instructions`. Continue until Shepherd emits `[CANCEL]` or `[ESCALATE]` (including `stall-timeout` for repeated unchanged CI failures). `pr-shepherd iterate 42` remains supported for existing workflows.
|
|
234
228
|
|
|
235
229
|
### As a global CLI
|
|
236
230
|
|
|
@@ -255,7 +249,7 @@ actions:
|
|
|
255
249
|
autoMarkReady: false # disable to stay draft until you manually promote
|
|
256
250
|
```
|
|
257
251
|
|
|
258
|
-
Environment variables: `GH_TOKEN` / `GITHUB_TOKEN` (auth; falls back to `gh auth token`, then `GITHUB_PERSONAL_ACCESS_TOKEN`), `PR_SHEPHERD_STATE_DIR` (override loop-state and log base dir), `PR_SHEPHERD_LOG_DISABLED=1` (disable the per-worktree debug log)
|
|
252
|
+
Environment variables: `GH_TOKEN` / `GITHUB_TOKEN` (auth; falls back to `gh auth token`, then `GITHUB_PERSONAL_ACCESS_TOKEN`), `PR_SHEPHERD_STATE_DIR` (override loop-state and log base dir), `PR_SHEPHERD_LOG_DISABLED=1` (disable the per-worktree debug log).
|
|
259
253
|
|
|
260
254
|
See [docs/configuration.md](docs/configuration.md) for full semantics and deprecated-key migration.
|
|
261
255
|
|
package/bin/cli/args.mjs
CHANGED
|
@@ -15,6 +15,8 @@ const FLAGS_WITH_VALUES = new Set([
|
|
|
15
15
|
"--resolve-thread-ids",
|
|
16
16
|
"--minimize-comment-ids",
|
|
17
17
|
"--dismiss-review-ids",
|
|
18
|
+
"--interval",
|
|
19
|
+
"--timeout",
|
|
18
20
|
]);
|
|
19
21
|
// Boolean flags that do NOT consume the next argument. Any --flag not in this
|
|
20
22
|
// set and not in FLAGS_WITH_VALUES is treated conservatively as value-taking
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parsePrNumber } from "./args.mjs";
|
|
2
|
+
import { USAGE } from "./help.mjs";
|
|
2
3
|
const DEFAULT_ITERATE_FLAGS_WITH_VALUES = new Set(["--format", "--ready-delay", "--stall-timeout"]);
|
|
3
4
|
const DEFAULT_ITERATE_BOOLEAN_FLAGS = new Set([
|
|
4
5
|
"--verbose",
|
|
@@ -6,6 +7,8 @@ const DEFAULT_ITERATE_BOOLEAN_FLAGS = new Set([
|
|
|
6
7
|
"--no-auto-cancel-actionable",
|
|
7
8
|
]);
|
|
8
9
|
export function isDefaultIterateInvocation(subcommand) {
|
|
10
|
+
if (subcommand === "--help" || subcommand === "-h")
|
|
11
|
+
return false;
|
|
9
12
|
return (subcommand === undefined ||
|
|
10
13
|
parsePrNumber(subcommand) !== null ||
|
|
11
14
|
isDefaultIterateFlag(subcommand));
|
|
@@ -42,8 +45,6 @@ function isDefaultIterateFlag(arg) {
|
|
|
42
45
|
}
|
|
43
46
|
function writeDefaultUsageError(arg) {
|
|
44
47
|
process.stderr.write(`Unknown subcommand: ${arg}\n`);
|
|
45
|
-
process.stderr.write(
|
|
46
|
-
" pr-shepherd <resolve|commit-suggestion|iterate|log-file> [options]\n" +
|
|
47
|
-
" pr-shepherd --version | -v\n");
|
|
48
|
+
process.stderr.write(`${USAGE.top}\n`);
|
|
48
49
|
process.exitCode = 1;
|
|
49
50
|
}
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
export function validateSecondsDurationFlag(command, flag, value, presentAsSeparateArg) {
|
|
2
|
+
if (value === null) {
|
|
3
|
+
if (presentAsSeparateArg) {
|
|
4
|
+
process.stderr.write(`${command}: ${flag} requires a value (e.g. ${flag} 30s)\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} 30s)\n`);
|
|
13
|
+
process.exitCode = 1;
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
if (!/^[1-9]\d*(?:s|sec|seconds?|m|min|minutes?|h|hours?)?$/.test(trimmed)) {
|
|
17
|
+
process.stderr.write(`${command}: invalid ${flag}: ${value}. Expected a duration like 30s, 5m, 1h, or bare seconds (e.g. 30).\n`);
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return trimmed;
|
|
22
|
+
}
|
|
1
23
|
export function validateDurationFlag(command, flag, value, presentAsSeparateArg) {
|
|
2
24
|
if (value === null) {
|
|
3
25
|
if (presentAsSeparateArg) {
|
package/bin/cli/exit-codes.mjs
CHANGED
|
@@ -9,6 +9,20 @@ export function parseDurationToMinutes(s, defaultMinutes) {
|
|
|
9
9
|
return n * 60;
|
|
10
10
|
return n;
|
|
11
11
|
}
|
|
12
|
+
export function parseDurationToSeconds(s, defaultSeconds) {
|
|
13
|
+
const m = /^(\d+)(s|sec|seconds?|m|min|minutes?|h|hours?)?$/.exec(s.trim());
|
|
14
|
+
if (!m)
|
|
15
|
+
return defaultSeconds;
|
|
16
|
+
const n = parseInt(m[1], 10);
|
|
17
|
+
if (!Number.isFinite(n))
|
|
18
|
+
return defaultSeconds;
|
|
19
|
+
const unit = m[2] ?? "s";
|
|
20
|
+
if (unit.startsWith("h"))
|
|
21
|
+
return n * 3600;
|
|
22
|
+
if (unit.startsWith("m"))
|
|
23
|
+
return n * 60;
|
|
24
|
+
return n;
|
|
25
|
+
}
|
|
12
26
|
export function statusToExitCode(status) {
|
|
13
27
|
switch (status) {
|
|
14
28
|
case "MERGED":
|
|
@@ -4,7 +4,6 @@ import { renderSuggestionBlock, renderLineRange } from "./suggestion-renderer.mj
|
|
|
4
4
|
import { renderThreadBullet, renderReviewBullet, renderThreadResolutionStatusTag, renderAuthor, buildFirstLookBullets, } from "./list-formatters.mjs";
|
|
5
5
|
import { adaptFixCodeInstructions, numberInstructions } from "./iterate-instructions.mjs";
|
|
6
6
|
export function formatFixCodeResult(header, result, opts) {
|
|
7
|
-
const runtime = opts?.runtime ?? "claude";
|
|
8
7
|
const readyDelaySuffix = opts?.readyDelaySuffix;
|
|
9
8
|
const runner = opts?.runner;
|
|
10
9
|
const sections = [header];
|
|
@@ -110,7 +109,7 @@ export function formatFixCodeResult(header, result, opts) {
|
|
|
110
109
|
}
|
|
111
110
|
sections.push(postFixLines.join("\n"));
|
|
112
111
|
sections.push("## Instructions");
|
|
113
|
-
sections.push(numberInstructions(adaptFixCodeInstructions(result.fix.instructions, result.pr,
|
|
112
|
+
sections.push(numberInstructions(adaptFixCodeInstructions(result.fix.instructions, result.pr, readyDelaySuffix, runner)));
|
|
114
113
|
return joinSections(sections);
|
|
115
114
|
}
|
|
116
115
|
function blockquote(body) {
|
package/bin/cli/handlers.mjs
CHANGED
|
@@ -2,16 +2,16 @@ import { runCommitSuggestion } from "../commands/commit-suggestion.mjs";
|
|
|
2
2
|
import { runIterate } from "../commands/iterate/index.mjs";
|
|
3
3
|
import { runClean } from "../commands/clean.mjs";
|
|
4
4
|
import { loadConfig } from "../config/load.mjs";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
5
|
+
import { parseCommonArgs, getFlag } from "./args.mjs";
|
|
6
|
+
import { USAGE } from "./help.mjs";
|
|
7
|
+
import { formatCommitSuggestionResult, formatCleanResult } from "./formatters.mjs";
|
|
8
|
+
import { parseIterateFlags } from "./iterate-flags.mjs";
|
|
9
|
+
import { emitIterateResult } from "./iterate-emitter.mjs";
|
|
10
10
|
const CLEAN_VARIANTS = new Set(["pr", "branch", "current", "repo", "all"]);
|
|
11
11
|
export async function handleClean(args) {
|
|
12
12
|
const variant = args[0];
|
|
13
13
|
if (!variant || !CLEAN_VARIANTS.has(variant)) {
|
|
14
|
-
process.stderr.write(
|
|
14
|
+
process.stderr.write(`${USAGE.clean}\n`);
|
|
15
15
|
process.exitCode = 1;
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
@@ -66,7 +66,7 @@ export async function handleCommitSuggestion(args) {
|
|
|
66
66
|
const { prNumber, global: globalOpts, extra } = parseCommonArgs(args);
|
|
67
67
|
const threadId = getFlag(extra, "--thread-id");
|
|
68
68
|
if (!threadId) {
|
|
69
|
-
process.stderr.write("
|
|
69
|
+
process.stderr.write(`${USAGE["commit-suggestion"]}\n`);
|
|
70
70
|
process.exitCode = 1;
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
@@ -90,41 +90,22 @@ export async function handleCommitSuggestion(args) {
|
|
|
90
90
|
}
|
|
91
91
|
export async function handleIterate(args) {
|
|
92
92
|
const { prNumber, global: globalOpts, extra } = parseCommonArgs(args);
|
|
93
|
-
const runtime = detectAgentRuntime();
|
|
94
|
-
const readyDelayStr = getFlag(extra, "--ready-delay");
|
|
95
|
-
const readyDelaySuffix = validateDurationFlag("pr-shepherd", "--ready-delay", readyDelayStr, hasFlag(extra, "--ready-delay"));
|
|
96
|
-
if (readyDelaySuffix === null)
|
|
97
|
-
return;
|
|
98
93
|
const cfg = loadConfig();
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const stallTimeoutStr = getFlag(extra, "--stall-timeout");
|
|
103
|
-
const stallTimeoutSeconds = stallTimeoutStr
|
|
104
|
-
? parseDurationToMinutes(stallTimeoutStr, cfg.iterate.stallTimeoutMinutes) * 60
|
|
105
|
-
: cfg.iterate.stallTimeoutMinutes * 60;
|
|
94
|
+
const flags = parseIterateFlags(extra, cfg);
|
|
95
|
+
if (flags.readyDelaySuffix === null)
|
|
96
|
+
return;
|
|
106
97
|
const result = await runIterate({
|
|
107
98
|
...globalOpts,
|
|
108
99
|
prNumber,
|
|
109
|
-
readyDelaySeconds,
|
|
110
|
-
stallTimeoutSeconds,
|
|
111
|
-
noAutoMarkReady,
|
|
112
|
-
noAutoCancelActionable,
|
|
100
|
+
readyDelaySeconds: flags.readyDelaySeconds,
|
|
101
|
+
stallTimeoutSeconds: flags.stallTimeoutSeconds,
|
|
102
|
+
noAutoMarkReady: flags.noAutoMarkReady,
|
|
103
|
+
noAutoCancelActionable: flags.noAutoCancelActionable,
|
|
113
104
|
});
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
105
|
+
emitIterateResult(result, {
|
|
106
|
+
format: globalOpts.format,
|
|
107
|
+
verbose: globalOpts.verbose ?? false,
|
|
108
|
+
readyDelaySuffix: flags.readyDelaySuffix ?? undefined,
|
|
117
109
|
runner: cfg.cli?.runner,
|
|
118
|
-
};
|
|
119
|
-
if (globalOpts.format === "json") {
|
|
120
|
-
const output = globalOpts.verbose
|
|
121
|
-
? projectIterateVerbose(result, projectionOpts)
|
|
122
|
-
: projectIterateLean(result, projectionOpts);
|
|
123
|
-
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
const text = formatIterateResult(result, { verbose: globalOpts.verbose, ...projectionOpts });
|
|
127
|
-
process.stdout.write(`${text}\n`);
|
|
128
|
-
}
|
|
129
|
-
process.exitCode = iterateActionToExitCode(result.action);
|
|
110
|
+
});
|
|
130
111
|
}
|
package/bin/cli/help.mjs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { hasFlag } from "./args.mjs";
|
|
2
|
+
export const USAGE = {
|
|
3
|
+
top: "Usage:\n" +
|
|
4
|
+
" pr-shepherd --version | -v\n" +
|
|
5
|
+
" pr-shepherd [PR] [--format text|json] [--ready-delay Nm]\n" +
|
|
6
|
+
" [--stall-timeout <duration>] [--no-auto-mark-ready]\n" +
|
|
7
|
+
" [--no-auto-cancel-actionable]\n" +
|
|
8
|
+
" pr-shepherd resolve [PR] [--fetch] [--resolve-thread-ids A,B] [--minimize-comment-ids X,Y]\n" +
|
|
9
|
+
" [--dismiss-review-ids Q] [--message MSG] [--require-sha SHA]\n" +
|
|
10
|
+
" pr-shepherd commit-suggestion [PR] --thread-id ID --message MSG [--description DESC]\n" +
|
|
11
|
+
" [--format text|json]\n" +
|
|
12
|
+
" pr-shepherd iterate [PR] [--format text|json] [--ready-delay Nm]\n" +
|
|
13
|
+
" [--stall-timeout <duration>] [--no-auto-mark-ready]\n" +
|
|
14
|
+
" [--no-auto-cancel-actionable]\n" +
|
|
15
|
+
" pr-shepherd poll [PR] [--interval 30s] [--timeout 5m] [--format text|json] [--ready-delay Nm]\n" +
|
|
16
|
+
" [--stall-timeout <duration>] [--no-auto-mark-ready]\n" +
|
|
17
|
+
" [--no-auto-cancel-actionable]\n" +
|
|
18
|
+
" pr-shepherd clean <pr|branch|current|repo|all> [value] [--dry-run] [--format text|json]\n" +
|
|
19
|
+
" pr-shepherd log-file [--format text|json]",
|
|
20
|
+
resolve: "Usage: pr-shepherd resolve [PR] [--fetch] [--resolve-thread-ids A,B]\n" +
|
|
21
|
+
" [--minimize-comment-ids X,Y] [--dismiss-review-ids Q]\n" +
|
|
22
|
+
" [--message MSG] [--require-sha SHA]",
|
|
23
|
+
"commit-suggestion": "Usage: pr-shepherd commit-suggestion [PR] --thread-id ID --message MSG\n" +
|
|
24
|
+
" [--description DESC] [--format text|json]",
|
|
25
|
+
iterate: "Usage: pr-shepherd iterate [PR] [--format text|json] [--ready-delay Nm]\n" +
|
|
26
|
+
" [--stall-timeout <duration>] [--no-auto-mark-ready]\n" +
|
|
27
|
+
" [--no-auto-cancel-actionable]",
|
|
28
|
+
poll: "Usage: pr-shepherd poll [PR] [--interval 30s] [--timeout 5m] [--format text|json]\n" +
|
|
29
|
+
" [--ready-delay Nm] [--stall-timeout <duration>]\n" +
|
|
30
|
+
" [--no-auto-mark-ready] [--no-auto-cancel-actionable]",
|
|
31
|
+
clean: "Usage: pr-shepherd clean <pr|branch|current|repo|all> [value] [--dry-run] [--format text|json]",
|
|
32
|
+
"log-file": "Usage: pr-shepherd log-file [--format text|json]",
|
|
33
|
+
};
|
|
34
|
+
/** Prints usage for `key` to stdout and returns true if `--help` or `-h` is in args. */
|
|
35
|
+
export function maybePrintHelp(args, key) {
|
|
36
|
+
if (!hasFlag(args, "--help") && !hasFlag(args, "-h"))
|
|
37
|
+
return false;
|
|
38
|
+
process.stdout.write(`${USAGE[key]}\n`);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { iterateActionToExitCode } from "./exit-codes.mjs";
|
|
2
|
+
import { formatIterateResult, projectIterateLean, projectIterateVerbose } from "./formatters.mjs";
|
|
3
|
+
export function emitIterateResult(result, opts) {
|
|
4
|
+
const projectionOpts = {
|
|
5
|
+
readyDelaySuffix: opts.readyDelaySuffix,
|
|
6
|
+
runner: opts.runner,
|
|
7
|
+
};
|
|
8
|
+
if (opts.format === "json") {
|
|
9
|
+
const output = opts.verbose
|
|
10
|
+
? projectIterateVerbose(result, projectionOpts)
|
|
11
|
+
: projectIterateLean(result, projectionOpts);
|
|
12
|
+
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
const text = formatIterateResult(result, { verbose: opts.verbose, ...projectionOpts });
|
|
16
|
+
process.stdout.write(`${text}\n`);
|
|
17
|
+
}
|
|
18
|
+
process.exitCode = iterateActionToExitCode(result.action);
|
|
19
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getFlag, hasFlag } from "./args.mjs";
|
|
2
|
+
import { parseDurationToMinutes } from "./exit-codes.mjs";
|
|
3
|
+
import { validateDurationFlag } from "./duration-flag.mjs";
|
|
4
|
+
export function parseIterateFlags(extra, cfg) {
|
|
5
|
+
const readyDelayStr = getFlag(extra, "--ready-delay");
|
|
6
|
+
const readyDelaySuffix = validateDurationFlag("pr-shepherd", "--ready-delay", readyDelayStr, hasFlag(extra, "--ready-delay"));
|
|
7
|
+
const readyDelaySeconds = parseDurationToMinutes(readyDelaySuffix ?? "", cfg.watch.readyDelayMinutes) * 60;
|
|
8
|
+
const noAutoMarkReady = hasFlag(extra, "--no-auto-mark-ready");
|
|
9
|
+
const noAutoCancelActionable = hasFlag(extra, "--no-auto-cancel-actionable");
|
|
10
|
+
const stallTimeoutStr = getFlag(extra, "--stall-timeout");
|
|
11
|
+
const stallTimeoutSeconds = stallTimeoutStr
|
|
12
|
+
? parseDurationToMinutes(stallTimeoutStr, cfg.iterate.stallTimeoutMinutes) * 60
|
|
13
|
+
: cfg.iterate.stallTimeoutMinutes * 60;
|
|
14
|
+
return {
|
|
15
|
+
readyDelaySuffix,
|
|
16
|
+
readyDelaySeconds,
|
|
17
|
+
stallTimeoutSeconds,
|
|
18
|
+
noAutoMarkReady,
|
|
19
|
+
noAutoCancelActionable,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -17,7 +17,6 @@ import { adaptIterateLog, buildSimpleIterateInstructions, numberInstructions, }
|
|
|
17
17
|
*/
|
|
18
18
|
export function formatIterateResult(result, opts) {
|
|
19
19
|
const verbose = opts?.verbose ?? false;
|
|
20
|
-
const runtime = opts?.runtime ?? "claude";
|
|
21
20
|
const readyDelaySuffix = opts?.readyDelaySuffix;
|
|
22
21
|
const runner = opts?.runner;
|
|
23
22
|
const heading = `# PR #${result.pr} [${result.action.toUpperCase()}]`;
|
|
@@ -27,7 +26,14 @@ export function formatIterateResult(result, opts) {
|
|
|
27
26
|
const baseLine = `**status** \`${result.status}\` · **merge** \`${result.mergeStateStatus}\`${reviewDecisionSeg} · **state** \`${result.state}\` · **repo** \`${result.repo}\``;
|
|
28
27
|
let summaryLine;
|
|
29
28
|
if (verbose) {
|
|
30
|
-
|
|
29
|
+
let verboseBranch = "";
|
|
30
|
+
if (result.mergeStatus === "BEHIND" && result.baseBranch) {
|
|
31
|
+
verboseBranch = ` · **branch** behind \`origin/${result.baseBranch}\``;
|
|
32
|
+
}
|
|
33
|
+
else if (result.mergeStatus === "CONFLICTS" && result.baseBranch) {
|
|
34
|
+
verboseBranch = ` · **branch** conflicts with \`origin/${result.baseBranch}\``;
|
|
35
|
+
}
|
|
36
|
+
summaryLine = `**summary** ${result.summary.passing} passing, ${result.summary.skipped} skipped, ${result.summary.filtered} filtered, ${result.summary.inProgress} inProgress · **remainingSeconds** ${result.remainingSeconds} · **blockingBotReviewInProgress** ${result.blockingBotReviewInProgress} · **isDraft** ${result.isDraft} · **shouldCancel** ${result.shouldCancel}${verboseBranch}`;
|
|
31
37
|
}
|
|
32
38
|
else {
|
|
33
39
|
const counts = [`${result.summary.passing} passing`];
|
|
@@ -45,37 +51,68 @@ export function formatIterateResult(result, opts) {
|
|
|
45
51
|
segs.push(`**blockingBotReviewInProgress**`);
|
|
46
52
|
if (result.isDraft)
|
|
47
53
|
segs.push(`**isDraft**`);
|
|
54
|
+
if (result.mergeStatus === "BEHIND" && result.baseBranch) {
|
|
55
|
+
segs.push(`**branch** behind \`origin/${result.baseBranch}\``);
|
|
56
|
+
}
|
|
57
|
+
else if (result.mergeStatus === "CONFLICTS" && result.baseBranch) {
|
|
58
|
+
segs.push(`**branch** conflicts with \`origin/${result.baseBranch}\``);
|
|
59
|
+
}
|
|
48
60
|
summaryLine = segs.join(" · ");
|
|
49
61
|
}
|
|
50
|
-
const
|
|
62
|
+
const bp = result.branchProtection;
|
|
63
|
+
const requiredParts = [];
|
|
64
|
+
if (bp) {
|
|
65
|
+
if (bp.requiresApprovingReviews && bp.requiredApprovingReviewCount > 0) {
|
|
66
|
+
requiredParts.push(`approvals \`${bp.requiredApprovingReviewCount}\``);
|
|
67
|
+
}
|
|
68
|
+
if (bp.requiresConversationResolution) {
|
|
69
|
+
requiredParts.push("conversation-resolution required");
|
|
70
|
+
}
|
|
71
|
+
if (bp.requiresStatusChecks) {
|
|
72
|
+
if (bp.requiredStatusCheckContexts.length > 0) {
|
|
73
|
+
requiredParts.push(`checks: ${bp.requiredStatusCheckContexts.map((c) => `\`${c}\``).join(", ")}`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
requiredParts.push("status checks required");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const requiredLine = requiredParts.length > 0 ? `**required** ${requiredParts.join(", ")}` : null;
|
|
81
|
+
const headerLines = [heading, "", baseLine, summaryLine];
|
|
82
|
+
if (requiredLine)
|
|
83
|
+
headerLines.push(requiredLine);
|
|
84
|
+
const header = headerLines.join("\n");
|
|
51
85
|
switch (result.action) {
|
|
52
86
|
case "wait":
|
|
53
87
|
return joinSections([
|
|
54
88
|
header,
|
|
55
|
-
adaptIterateLog(result.log
|
|
56
|
-
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result,
|
|
89
|
+
adaptIterateLog(result.log),
|
|
90
|
+
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, readyDelaySuffix, runner))}`,
|
|
57
91
|
]);
|
|
58
92
|
case "mark_ready":
|
|
59
93
|
return joinSections([
|
|
60
94
|
header,
|
|
61
|
-
adaptIterateLog(result.log
|
|
62
|
-
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result,
|
|
95
|
+
adaptIterateLog(result.log),
|
|
96
|
+
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, readyDelaySuffix, runner))}`,
|
|
63
97
|
]);
|
|
64
|
-
case "cancel":
|
|
98
|
+
case "cancel": {
|
|
99
|
+
const cancelHeaderLines = [`${heading} — ${result.reason}`, "", baseLine, summaryLine];
|
|
100
|
+
if (requiredLine)
|
|
101
|
+
cancelHeaderLines.push(requiredLine);
|
|
65
102
|
return joinSections([
|
|
66
|
-
|
|
67
|
-
adaptIterateLog(result.log
|
|
68
|
-
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result,
|
|
103
|
+
cancelHeaderLines.join("\n"),
|
|
104
|
+
adaptIterateLog(result.log),
|
|
105
|
+
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, readyDelaySuffix, runner))}`,
|
|
69
106
|
]);
|
|
107
|
+
}
|
|
70
108
|
case "escalate":
|
|
71
109
|
return joinSections([
|
|
72
110
|
header,
|
|
73
111
|
result.escalate.humanMessage,
|
|
74
|
-
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result,
|
|
112
|
+
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, readyDelaySuffix, runner))}`,
|
|
75
113
|
]);
|
|
76
114
|
case "fix_code":
|
|
77
115
|
return formatFixCodeResult(header, result, {
|
|
78
|
-
runtime,
|
|
79
116
|
readyDelaySuffix,
|
|
80
117
|
runner,
|
|
81
118
|
});
|
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FIX_INSTRUCTION_STOP } from "../commands/iterate/render.mjs";
|
|
2
2
|
import { buildPrShepherdCommand } from "./runner.mjs";
|
|
3
|
-
function buildRecheckInstruction(
|
|
4
|
-
|
|
5
|
-
return `Pick a fresh sleep/timeout between 30 seconds and 4 minutes, wait that long, then rerun \`${rerunCommand}\` to ${purpose}.`;
|
|
6
|
-
}
|
|
7
|
-
return `Single-shot continuation: schedule one session-only follow-up task to run \`${rerunCommand}\` to ${purpose} once after a fresh delay between 30 seconds and 4 minutes, then end this turn. Do not sleep or rerun inline.`;
|
|
3
|
+
function buildRecheckInstruction(rerunCommand, purpose) {
|
|
4
|
+
return `Recheck: rerun \`${rerunCommand}\` to ${purpose} once after a fresh 30s–4m delay.`;
|
|
8
5
|
}
|
|
9
|
-
export function buildSimpleIterateInstructions(result,
|
|
6
|
+
export function buildSimpleIterateInstructions(result, readyDelaySuffix, runner) {
|
|
10
7
|
const rerunCommand = buildIterateCommand(result.pr, readyDelaySuffix, runner);
|
|
11
8
|
switch (result.action) {
|
|
12
9
|
case "wait":
|
|
13
|
-
return [buildRecheckInstruction(
|
|
10
|
+
return [buildRecheckInstruction(rerunCommand, "continue the active goal")];
|
|
14
11
|
case "mark_ready":
|
|
15
12
|
return [
|
|
16
|
-
`The CLI already marked the PR ready for review. ${buildRecheckInstruction(
|
|
13
|
+
`The CLI already marked the PR ready for review. ${buildRecheckInstruction(rerunCommand, "recheck")}`,
|
|
17
14
|
];
|
|
18
15
|
case "cancel":
|
|
19
16
|
return ["Stop — the active goal is complete."];
|
|
@@ -23,19 +20,16 @@ export function buildSimpleIterateInstructions(result, runtime, readyDelaySuffix
|
|
|
23
20
|
];
|
|
24
21
|
}
|
|
25
22
|
}
|
|
26
|
-
export function adaptFixCodeInstructions(instructions, pr,
|
|
23
|
+
export function adaptFixCodeInstructions(instructions, pr, readyDelaySuffix, runner) {
|
|
27
24
|
const rerunCommand = buildIterateCommand(pr, readyDelaySuffix, runner);
|
|
28
25
|
return instructions.map((instruction) => {
|
|
29
|
-
if (instruction ===
|
|
30
|
-
return
|
|
31
|
-
}
|
|
32
|
-
if (instruction === FIX_INSTRUCTION_STOP_BEFORE_NEXT_TICK) {
|
|
33
|
-
return buildRecheckInstruction(runtime, rerunCommand, "recheck");
|
|
26
|
+
if (instruction === FIX_INSTRUCTION_STOP) {
|
|
27
|
+
return `${instruction} ${buildRecheckInstruction(rerunCommand, "recheck")}`;
|
|
34
28
|
}
|
|
35
29
|
return instruction;
|
|
36
30
|
});
|
|
37
31
|
}
|
|
38
|
-
export function adaptIterateLog(log
|
|
32
|
+
export function adaptIterateLog(log) {
|
|
39
33
|
return log.replace(/\s+—\s+\d+s until auto-cancel/g, "");
|
|
40
34
|
}
|
|
41
35
|
export function buildIterateCommand(pr, readyDelaySuffix, runner) {
|
package/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -5,10 +5,9 @@ import { adaptIterateLog, adaptFixCodeInstructions, buildSimpleIterateInstructio
|
|
|
5
5
|
* outside the state where they are meaningful.
|
|
6
6
|
*/
|
|
7
7
|
export function projectIterateLean(result, opts) {
|
|
8
|
-
const runtime = opts?.runtime ?? "claude";
|
|
9
8
|
const readyDelaySuffix = opts?.readyDelaySuffix;
|
|
10
9
|
const runner = opts?.runner;
|
|
11
|
-
const simpleInstructions = (r) => buildSimpleIterateInstructions(r,
|
|
10
|
+
const simpleInstructions = (r) => buildSimpleIterateInstructions(r, readyDelaySuffix, runner);
|
|
12
11
|
const base = {
|
|
13
12
|
action: result.action,
|
|
14
13
|
pr: result.pr,
|
|
@@ -32,26 +31,27 @@ export function projectIterateLean(result, opts) {
|
|
|
32
31
|
remainingSeconds: result.remainingSeconds,
|
|
33
32
|
}),
|
|
34
33
|
...(result.baseBranch && { baseBranch: result.baseBranch }),
|
|
34
|
+
...(result.branchProtection !== null && { branchProtection: result.branchProtection }),
|
|
35
35
|
};
|
|
36
36
|
switch (result.action) {
|
|
37
37
|
case "wait":
|
|
38
38
|
return {
|
|
39
39
|
...base,
|
|
40
|
-
log: adaptIterateLog(result.log
|
|
40
|
+
log: adaptIterateLog(result.log),
|
|
41
41
|
instructions: simpleInstructions(result),
|
|
42
42
|
};
|
|
43
43
|
case "cancel":
|
|
44
44
|
return {
|
|
45
45
|
...base,
|
|
46
46
|
reason: result.reason,
|
|
47
|
-
log: adaptIterateLog(result.log
|
|
47
|
+
log: adaptIterateLog(result.log),
|
|
48
48
|
instructions: simpleInstructions(result),
|
|
49
49
|
};
|
|
50
50
|
case "mark_ready":
|
|
51
51
|
// drop markedReady — always true, redundant with action discriminator
|
|
52
52
|
return {
|
|
53
53
|
...base,
|
|
54
|
-
log: adaptIterateLog(result.log
|
|
54
|
+
log: adaptIterateLog(result.log),
|
|
55
55
|
instructions: simpleInstructions(result),
|
|
56
56
|
};
|
|
57
57
|
case "fix_code":
|
|
@@ -94,7 +94,7 @@ export function projectIterateLean(result, opts) {
|
|
|
94
94
|
}),
|
|
95
95
|
resolveCommand: result.fix.resolveCommand,
|
|
96
96
|
...(result.fix.instructions.length > 0 && {
|
|
97
|
-
instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr,
|
|
97
|
+
instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, readyDelaySuffix, runner),
|
|
98
98
|
}),
|
|
99
99
|
},
|
|
100
100
|
};
|
|
@@ -124,7 +124,6 @@ export function projectIterateLean(result, opts) {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
export function projectIterateVerbose(result, opts) {
|
|
127
|
-
const runtime = opts?.runtime ?? "claude";
|
|
128
127
|
const readyDelaySuffix = opts?.readyDelaySuffix;
|
|
129
128
|
const runner = opts?.runner;
|
|
130
129
|
if (result.action === "fix_code") {
|
|
@@ -132,16 +131,14 @@ export function projectIterateVerbose(result, opts) {
|
|
|
132
131
|
...result,
|
|
133
132
|
fix: {
|
|
134
133
|
...result.fix,
|
|
135
|
-
instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr,
|
|
134
|
+
instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, readyDelaySuffix, runner),
|
|
136
135
|
},
|
|
137
136
|
};
|
|
138
137
|
}
|
|
139
|
-
const log = "log" in result && typeof result.log === "string"
|
|
140
|
-
? { log: adaptIterateLog(result.log, runtime) }
|
|
141
|
-
: {};
|
|
138
|
+
const log = "log" in result && typeof result.log === "string" ? { log: adaptIterateLog(result.log) } : {};
|
|
142
139
|
return {
|
|
143
140
|
...result,
|
|
144
141
|
...log,
|
|
145
|
-
instructions: buildSimpleIterateInstructions(result,
|
|
142
|
+
instructions: buildSimpleIterateInstructions(result, readyDelaySuffix, runner),
|
|
146
143
|
};
|
|
147
144
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { runPoll } from "../commands/poll.mjs";
|
|
2
|
+
import { loadConfig } from "../config/load.mjs";
|
|
3
|
+
import { parseCommonArgs, getFlag, hasFlag } from "./args.mjs";
|
|
4
|
+
import { parseDurationToSeconds } from "./exit-codes.mjs";
|
|
5
|
+
import { validateSecondsDurationFlag } from "./duration-flag.mjs";
|
|
6
|
+
import { parseIterateFlags } from "./iterate-flags.mjs";
|
|
7
|
+
import { emitIterateResult } from "./iterate-emitter.mjs";
|
|
8
|
+
const DEFAULT_POLL_INTERVAL_SECONDS = 30;
|
|
9
|
+
const DEFAULT_POLL_TIMEOUT_SECONDS = 300;
|
|
10
|
+
export async function handlePoll(args) {
|
|
11
|
+
const { prNumber, global: globalOpts, extra } = parseCommonArgs(args);
|
|
12
|
+
const cfg = loadConfig();
|
|
13
|
+
const flags = parseIterateFlags(extra, cfg);
|
|
14
|
+
if (flags.readyDelaySuffix === null)
|
|
15
|
+
return;
|
|
16
|
+
const intervalStr = getFlag(extra, "--interval");
|
|
17
|
+
const intervalSuffix = validateSecondsDurationFlag("pr-shepherd poll", "--interval", intervalStr, hasFlag(extra, "--interval"));
|
|
18
|
+
if (intervalSuffix === null)
|
|
19
|
+
return;
|
|
20
|
+
const intervalSeconds = parseDurationToSeconds(intervalSuffix ?? "", DEFAULT_POLL_INTERVAL_SECONDS);
|
|
21
|
+
const timeoutStr = getFlag(extra, "--timeout");
|
|
22
|
+
const timeoutSuffix = validateSecondsDurationFlag("pr-shepherd poll", "--timeout", timeoutStr, hasFlag(extra, "--timeout"));
|
|
23
|
+
if (timeoutSuffix === null)
|
|
24
|
+
return;
|
|
25
|
+
const timeoutSeconds = parseDurationToSeconds(timeoutSuffix ?? "", DEFAULT_POLL_TIMEOUT_SECONDS);
|
|
26
|
+
const result = await runPoll({
|
|
27
|
+
...globalOpts,
|
|
28
|
+
prNumber,
|
|
29
|
+
readyDelaySeconds: flags.readyDelaySeconds,
|
|
30
|
+
stallTimeoutSeconds: flags.stallTimeoutSeconds,
|
|
31
|
+
noAutoMarkReady: flags.noAutoMarkReady,
|
|
32
|
+
noAutoCancelActionable: flags.noAutoCancelActionable,
|
|
33
|
+
intervalSeconds,
|
|
34
|
+
timeoutSeconds,
|
|
35
|
+
});
|
|
36
|
+
emitIterateResult(result, {
|
|
37
|
+
format: globalOpts.format,
|
|
38
|
+
verbose: globalOpts.verbose ?? false,
|
|
39
|
+
readyDelaySuffix: flags.readyDelaySuffix ?? undefined,
|
|
40
|
+
runner: cfg.cli?.runner,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -32,15 +32,11 @@ export function registerHooks() {
|
|
|
32
32
|
beforeEach(() => {
|
|
33
33
|
vi.clearAllMocks();
|
|
34
34
|
process.exitCode = undefined;
|
|
35
|
-
delete process.env.AGENT;
|
|
36
|
-
delete process.env.CODEX_CI;
|
|
37
35
|
stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
38
36
|
stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
39
37
|
});
|
|
40
38
|
afterEach(() => {
|
|
41
39
|
process.exitCode = undefined;
|
|
42
|
-
delete process.env.AGENT;
|
|
43
|
-
delete process.env.CODEX_CI;
|
|
44
40
|
stdoutSpy.mockRestore();
|
|
45
41
|
stderrSpy.mockRestore();
|
|
46
42
|
});
|