pr-shepherd 0.26.1 → 0.26.3
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 +1 -1
- package/bin/checks/classify.mjs +4 -3
- package/bin/cli/duration-flag.mjs +3 -2
- package/bin/cli/exit-codes.mjs +37 -11
- package/bin/cli/help-command-pages.mjs +3 -3
- package/bin/cli/help-top-page.mjs +3 -3
- package/bin/cli/iterate-formatter.mjs +8 -0
- package/bin/cli/iterate-lean.mjs +3 -0
- package/bin/cli/poll-handler.mjs +2 -2
- package/bin/commands/check-status.mjs +7 -2
- package/bin/commands/check.mjs +1 -0
- package/bin/commands/iterate/classify.mjs +36 -14
- package/bin/commands/iterate/index.mjs +2 -0
- package/bin/commands/iterate/render.mjs +1 -1
- package/bin/commands/poll.mjs +28 -13
- package/package.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ Direct CLI:
|
|
|
88
88
|
|
|
89
89
|
```sh
|
|
90
90
|
pr-shepherd 42 # poll until non-WAIT or timeout
|
|
91
|
-
pr-shepherd 42 --interval
|
|
91
|
+
pr-shepherd 42 --interval 60s --timeout 270s
|
|
92
92
|
pr-shepherd 42 --quiet-status # print only changed WAIT status snapshots
|
|
93
93
|
pr-shepherd 42 --ready-delay 15m
|
|
94
94
|
pr-shepherd iterate 42 # single tick
|
package/bin/checks/classify.mjs
CHANGED
|
@@ -20,7 +20,7 @@ export function classifyChecks(checks) {
|
|
|
20
20
|
const config = loadConfig();
|
|
21
21
|
const relevantEvents = new Set(config.checks.ciTriggerEvents);
|
|
22
22
|
const isIgnored = buildIgnoreMatcher(config.ignoreChecks ?? []);
|
|
23
|
-
return checks.
|
|
23
|
+
return checks.map((c) => isIgnored(c.name) ? { ...c, category: "ignored" } : classify(c, relevantEvents));
|
|
24
24
|
}
|
|
25
25
|
function buildIgnoreMatcher(patterns) {
|
|
26
26
|
if (patterns.length === 0)
|
|
@@ -51,7 +51,7 @@ function classify(check, relevantEvents) {
|
|
|
51
51
|
}
|
|
52
52
|
/** Compute a high-level CI verdict from a list of classified checks. */
|
|
53
53
|
export function getCiVerdict(classified) {
|
|
54
|
-
const relevant = classified.filter((c) => c.category !== "filtered" && c.category !== "skipped");
|
|
54
|
+
const relevant = classified.filter((c) => c.category !== "filtered" && c.category !== "skipped" && c.category !== "ignored");
|
|
55
55
|
const anyInProgress = relevant.some((c) => c.category === "in_progress");
|
|
56
56
|
const anyFailing = relevant.some((c) => c.category === "failing");
|
|
57
57
|
// When there are no relevant checks (e.g. docs-only PR where all checks are filtered/skipped),
|
|
@@ -59,5 +59,6 @@ export function getCiVerdict(classified) {
|
|
|
59
59
|
const allPassed = !anyInProgress && !anyFailing;
|
|
60
60
|
const hasChecks = relevant.length > 0;
|
|
61
61
|
const filteredNames = classified.filter((c) => c.category === "filtered").map((c) => c.name);
|
|
62
|
-
|
|
62
|
+
const ignoredNames = Array.from(new Set(classified.filter((c) => c.category === "ignored").map((c) => c.name)));
|
|
63
|
+
return { allPassed, hasChecks, anyInProgress, anyFailing, filteredNames, ignoredNames };
|
|
63
64
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseSecondsDurationParts } from "./exit-codes.mjs";
|
|
1
2
|
export function validateSecondsDurationFlag(command, flag, value, presentAsSeparateArg) {
|
|
2
3
|
if (value === null) {
|
|
3
4
|
if (presentAsSeparateArg) {
|
|
@@ -13,8 +14,8 @@ export function validateSecondsDurationFlag(command, flag, value, presentAsSepar
|
|
|
13
14
|
process.exitCode = 1;
|
|
14
15
|
return null;
|
|
15
16
|
}
|
|
16
|
-
if (
|
|
17
|
-
process.stderr.write(`${command}: invalid ${flag}: ${value}. Expected a duration like 30s, 5m, 1h, or bare seconds (e.g. 30).\n`);
|
|
17
|
+
if (!parseSecondsDurationParts(trimmed)) {
|
|
18
|
+
process.stderr.write(`${command}: invalid ${flag}: ${value}. Expected a duration like 30s, 4.5m, 1h, or bare seconds (e.g. 30).\n`);
|
|
18
19
|
process.exitCode = 1;
|
|
19
20
|
return null;
|
|
20
21
|
}
|
package/bin/cli/exit-codes.mjs
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import { loadConfig } from "../config/load.mjs";
|
|
2
|
+
const SECOND_DURATION_UNITS = new Set([
|
|
3
|
+
"s",
|
|
4
|
+
"sec",
|
|
5
|
+
"second",
|
|
6
|
+
"seconds",
|
|
7
|
+
"m",
|
|
8
|
+
"min",
|
|
9
|
+
"minute",
|
|
10
|
+
"minutes",
|
|
11
|
+
"h",
|
|
12
|
+
"hour",
|
|
13
|
+
"hours",
|
|
14
|
+
]);
|
|
2
15
|
export function parseDurationToMinutes(s, defaultMinutes) {
|
|
3
16
|
const m = /^(\d+)(m|min|minutes?|h|hours?)?$/.exec(s.trim());
|
|
4
17
|
if (!m)
|
|
@@ -9,19 +22,32 @@ export function parseDurationToMinutes(s, defaultMinutes) {
|
|
|
9
22
|
return n * 60;
|
|
10
23
|
return n;
|
|
11
24
|
}
|
|
25
|
+
export function parseSecondsDurationParts(s) {
|
|
26
|
+
const trimmed = s.trim();
|
|
27
|
+
const match = /^(\d+(?:\.\d+)?)([a-z]+)?$/.exec(trimmed);
|
|
28
|
+
if (!match)
|
|
29
|
+
return null;
|
|
30
|
+
const amount = match[1];
|
|
31
|
+
const explicitUnit = match[2];
|
|
32
|
+
if (!amount || (amount.includes(".") && !explicitUnit))
|
|
33
|
+
return null;
|
|
34
|
+
const unit = explicitUnit ?? "s";
|
|
35
|
+
if (!SECOND_DURATION_UNITS.has(unit))
|
|
36
|
+
return null;
|
|
37
|
+
const value = Number(amount);
|
|
38
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
39
|
+
return null;
|
|
40
|
+
return { value, unit };
|
|
41
|
+
}
|
|
12
42
|
export function parseDurationToSeconds(s, defaultSeconds) {
|
|
13
|
-
const
|
|
14
|
-
if (!
|
|
15
|
-
return defaultSeconds;
|
|
16
|
-
const n = parseInt(m[1], 10);
|
|
17
|
-
if (!Number.isFinite(n))
|
|
43
|
+
const parsed = parseSecondsDurationParts(s);
|
|
44
|
+
if (!parsed)
|
|
18
45
|
return defaultSeconds;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return n;
|
|
46
|
+
if (parsed.unit.startsWith("h"))
|
|
47
|
+
return parsed.value * 3600;
|
|
48
|
+
if (parsed.unit.startsWith("m"))
|
|
49
|
+
return parsed.value * 60;
|
|
50
|
+
return parsed.value;
|
|
25
51
|
}
|
|
26
52
|
export function statusToExitCode(status) {
|
|
27
53
|
switch (status) {
|
|
@@ -109,8 +109,8 @@ Usage:
|
|
|
109
109
|
pr-shepherd poll [PR] [poll-flags] [iterate-flags]
|
|
110
110
|
|
|
111
111
|
Poll flags:
|
|
112
|
-
--interval <duration> Sleep between WAIT ticks. Default:
|
|
113
|
-
--timeout <duration> Maximum wall-clock wait. Default: 5m.
|
|
112
|
+
--interval <duration> Sleep between WAIT ticks. Default: 60s.
|
|
113
|
+
--timeout <duration> Maximum wall-clock wait. Default: 4.5m.
|
|
114
114
|
--quiet-status During WAIT polling, print only changed status snapshots.
|
|
115
115
|
|
|
116
116
|
Forwarded iterate flags:
|
|
@@ -122,7 +122,7 @@ Forwarded iterate flags:
|
|
|
122
122
|
--verbose Include verbose iterate fields and detailed per-tick lines.
|
|
123
123
|
--help, -h Print this help and exit before GitHub, git, config, or log I/O.
|
|
124
124
|
|
|
125
|
-
Durations accept seconds, minutes, or hours: 30s,
|
|
125
|
+
Durations accept seconds, minutes, or hours: 30s, 4.5m, 1h, or bare seconds.
|
|
126
126
|
Each WAIT tick writes a single dot to stderr by default; --quiet-status prints only changed WAIT snapshots, and --verbose emits detailed per-tick lines.
|
|
127
127
|
|
|
128
128
|
Exit codes:
|
|
@@ -42,8 +42,8 @@ Iterate flags:
|
|
|
42
42
|
--no-auto-cancel-actionable Do not cancel in-progress runs before actionable fixes.
|
|
43
43
|
|
|
44
44
|
Poll flags:
|
|
45
|
-
--interval <duration>
|
|
46
|
-
--timeout <duration>
|
|
45
|
+
--interval <duration> Delay between WAIT ticks. Default: 60s.
|
|
46
|
+
--timeout <duration> Poll wall-clock cap. Default: 4.5m.
|
|
47
47
|
--quiet-status During WAIT polling, print only changed status snapshots.
|
|
48
48
|
|
|
49
49
|
Clean variants:
|
|
@@ -59,6 +59,6 @@ Exit codes for iterate and poll:
|
|
|
59
59
|
2 CANCEL
|
|
60
60
|
3 ESCALATE
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
Duration examples: 30s, 4.5m, 1h, or bare seconds.
|
|
63
63
|
|
|
64
64
|
Run 'pr-shepherd <command> --help' for command-specific details.`;
|
|
@@ -112,6 +112,10 @@ export function formatIterateResult(result, opts) {
|
|
|
112
112
|
const headerLines = [heading, "", baseLine, summaryLine];
|
|
113
113
|
if (requiredLine)
|
|
114
114
|
headerLines.push(requiredLine);
|
|
115
|
+
if (result.ignoredNames && result.ignoredNames.length > 0) {
|
|
116
|
+
const names = result.ignoredNames.map((n) => "`" + n + "`").join(", ");
|
|
117
|
+
headerLines.push(`**ignored** ${names}`);
|
|
118
|
+
}
|
|
115
119
|
const activityLine = formatActivityLine(result);
|
|
116
120
|
if (activityLine)
|
|
117
121
|
headerLines.push(activityLine);
|
|
@@ -133,6 +137,10 @@ export function formatIterateResult(result, opts) {
|
|
|
133
137
|
const cancelHeaderLines = [`${heading} — ${result.reason}`, "", baseLine, summaryLine];
|
|
134
138
|
if (requiredLine)
|
|
135
139
|
cancelHeaderLines.push(requiredLine);
|
|
140
|
+
if (result.ignoredNames && result.ignoredNames.length > 0) {
|
|
141
|
+
const ignoredStr = result.ignoredNames.map((n) => "`" + n + "`").join(", ");
|
|
142
|
+
cancelHeaderLines.push(`**ignored** ${ignoredStr}`);
|
|
143
|
+
}
|
|
136
144
|
if (activityLine)
|
|
137
145
|
cancelHeaderLines.push(activityLine);
|
|
138
146
|
return joinSections([
|
package/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -55,6 +55,9 @@ export function projectIterateLean(result, opts) {
|
|
|
55
55
|
...((result.inProgressChecks?.length ?? 0) > 0 && {
|
|
56
56
|
inProgressChecks: result.inProgressChecks,
|
|
57
57
|
}),
|
|
58
|
+
...((result.ignoredNames?.length ?? 0) > 0 && {
|
|
59
|
+
ignoredNames: result.ignoredNames,
|
|
60
|
+
}),
|
|
58
61
|
};
|
|
59
62
|
switch (result.action) {
|
|
60
63
|
case "wait":
|
package/bin/cli/poll-handler.mjs
CHANGED
|
@@ -5,8 +5,8 @@ import { parseDurationToSeconds } from "./exit-codes.mjs";
|
|
|
5
5
|
import { validateSecondsDurationFlag } from "./duration-flag.mjs";
|
|
6
6
|
import { parseIterateFlags } from "./iterate-flags.mjs";
|
|
7
7
|
import { emitIterateResult } from "./iterate-emitter.mjs";
|
|
8
|
-
const DEFAULT_POLL_INTERVAL_SECONDS =
|
|
9
|
-
const DEFAULT_POLL_TIMEOUT_SECONDS =
|
|
8
|
+
const DEFAULT_POLL_INTERVAL_SECONDS = 60;
|
|
9
|
+
const DEFAULT_POLL_TIMEOUT_SECONDS = 270;
|
|
10
10
|
export async function handlePoll(args) {
|
|
11
11
|
const { prNumber, global: globalOpts, extra } = parseCommonArgs(args);
|
|
12
12
|
const cfg = loadConfig();
|
|
@@ -13,13 +13,18 @@ export function computeStatus(verdict, unresolvedThreads, unresolvedComments, me
|
|
|
13
13
|
// is BLOCKED (review pending, insufficient approvals, branch-protection rule, etc.).
|
|
14
14
|
// Requires hasChecks so that a PR with zero relevant checks (CI never started, or all
|
|
15
15
|
// filtered/skipped) doesn't prematurely trigger READY before any check has reported.
|
|
16
|
+
// Exception: UNSTABLE with ignored checks — UNSTABLE means only non-required checks are
|
|
17
|
+
// pending/failing, and if those are all ignored the handoff is safe even with no other checks.
|
|
18
|
+
// BLOCKED is excluded from the ignoredNames extension: BLOCKED can mean required checks haven't
|
|
19
|
+
// started, and handing off prematurely there risks a broken merge attempt.
|
|
16
20
|
// blockingBotReviewInProgress is still excluded — a bot review is shepherd's problem, not a hand-off.
|
|
21
|
+
const hasRelevantPassingChecks = verdict.hasChecks || (mergeStatus.status === "UNSTABLE" && verdict.ignoredNames.length > 0);
|
|
17
22
|
if (verdict.allPassed &&
|
|
18
|
-
|
|
23
|
+
hasRelevantPassingChecks &&
|
|
19
24
|
unresolvedThreads === 0 &&
|
|
20
25
|
unresolvedComments === 0 &&
|
|
21
26
|
changesRequestedReviews === 0 &&
|
|
22
|
-
mergeStatus.status === "BLOCKED" &&
|
|
27
|
+
(mergeStatus.status === "BLOCKED" || mergeStatus.status === "UNSTABLE") &&
|
|
23
28
|
!mergeStatus.blockingBotReviewInProgress) {
|
|
24
29
|
return "READY";
|
|
25
30
|
}
|
package/bin/commands/check.mjs
CHANGED
|
@@ -124,6 +124,7 @@ export async function runCheck(opts) {
|
|
|
124
124
|
filtered,
|
|
125
125
|
filteredNames: verdict.filteredNames,
|
|
126
126
|
blockedByFilteredCheck,
|
|
127
|
+
...(verdict.ignoredNames.length > 0 && { ignoredNames: verdict.ignoredNames }),
|
|
127
128
|
},
|
|
128
129
|
threads: {
|
|
129
130
|
actionable: threadVisibility.activeThreads,
|
|
@@ -48,7 +48,7 @@ export function classifyReviewSummaries(summaries, approvals, minimizeApprovals,
|
|
|
48
48
|
surfacedApprovals: approvals,
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
|
-
export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds,
|
|
51
|
+
export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentIds, reviews, checks, prNumber, botUsernames = new Set(), ruleAutoResolveThreadIds = []) {
|
|
52
52
|
const allThreads = [...threads, ...resolutionOnlyThreads];
|
|
53
53
|
const replyThreadIds = dedupeIds(allThreads
|
|
54
54
|
.filter((t) => isHumanAuthor(t) && !isConfiguredBotAuthor(t, botUsernames))
|
|
@@ -60,21 +60,36 @@ export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentId
|
|
|
60
60
|
.map((t) => t.id),
|
|
61
61
|
...ruleAutoResolveThreadIds,
|
|
62
62
|
]);
|
|
63
|
+
// Bot/non-human CHANGES_REQUESTED reviews are auto-dismissed after the agent pushes a fix.
|
|
64
|
+
// Human reviews are left for the reviewer to re-review or dismiss themselves.
|
|
65
|
+
const dismissReviewIds = dedupeIds(reviews
|
|
66
|
+
.filter((r) => !isHumanAuthor(r) || isConfiguredBotAuthor(r, botUsernames))
|
|
67
|
+
.map((r) => r.id));
|
|
63
68
|
const hasReply = replyThreadIds.length > 0;
|
|
69
|
+
const hasDismiss = dismissReviewIds.length > 0;
|
|
70
|
+
// Mutations that require --message: replies (to human threads) and dismissals (of bot CR reviews).
|
|
71
|
+
const hasMessageMutations = hasReply || hasDismiss;
|
|
64
72
|
const hasResolveOrMinimize = resolveThreadIds.length > 0 || allCommentIds.length > 0;
|
|
65
|
-
if (
|
|
66
|
-
// Split:
|
|
73
|
+
if (hasMessageMutations && hasResolveOrMinimize) {
|
|
74
|
+
// Split: message-bearing mutations (replies + dismissals) ride in resolveArgv;
|
|
75
|
+
// resolve/minimize mutations go in resolveOnlyArgv so they can run without SHA or message.
|
|
67
76
|
const resolveArgv = buildPrShepherdCommand(["resolve", String(prNumber)]).argv;
|
|
68
|
-
|
|
77
|
+
if (replyThreadIds.length > 0) {
|
|
78
|
+
resolveArgv.push("--reply-thread-ids", replyThreadIds.join(","));
|
|
79
|
+
}
|
|
69
80
|
resolveArgv.push("--message", "$DISMISS_MESSAGE");
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
if (hasDismiss) {
|
|
82
|
+
resolveArgv.push("--dismiss-review-ids", dismissReviewIds.join(","));
|
|
83
|
+
}
|
|
84
|
+
// SHA is required when actionable thread fixes or failing checks are being addressed,
|
|
85
|
+
// or when bot CR reviews are being dismissed (post-push SHA gate).
|
|
86
|
+
const requiresHeadSha = threads.length > 0 || checks.length > 0 || hasDismiss;
|
|
73
87
|
const resolveCommand = {
|
|
74
88
|
argv: resolveArgv,
|
|
75
89
|
requiresHeadSha,
|
|
76
90
|
requiresDismissMessage: true,
|
|
77
|
-
replyThreadIds,
|
|
91
|
+
...(replyThreadIds.length > 0 ? { replyThreadIds } : undefined),
|
|
92
|
+
...(hasDismiss ? { dismissReviewIds } : undefined),
|
|
78
93
|
hasMutations: true,
|
|
79
94
|
};
|
|
80
95
|
const resolveOnlyArgv = buildPrShepherdCommand(["resolve", String(prNumber)]).argv;
|
|
@@ -93,7 +108,7 @@ export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentId
|
|
|
93
108
|
};
|
|
94
109
|
return { resolveCommand, resolveOnlyCommand };
|
|
95
110
|
}
|
|
96
|
-
// Single command:
|
|
111
|
+
// Single command: all mutations combined (or only one category present).
|
|
97
112
|
const argv = buildPrShepherdCommand(["resolve", String(prNumber)]).argv;
|
|
98
113
|
if (replyThreadIds.length > 0) {
|
|
99
114
|
argv.push("--reply-thread-ids", replyThreadIds.join(","));
|
|
@@ -105,16 +120,23 @@ export function buildResolveCommand(threads, resolutionOnlyThreads, allCommentId
|
|
|
105
120
|
if (allCommentIds.length > 0) {
|
|
106
121
|
argv.push("--minimize-comment-ids", allCommentIds.join(","));
|
|
107
122
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
123
|
+
if (hasDismiss) {
|
|
124
|
+
// Add --message when dismissing without a reply (replies already added it above).
|
|
125
|
+
if (!hasReply)
|
|
126
|
+
argv.push("--message", "$DISMISS_MESSAGE");
|
|
127
|
+
argv.push("--dismiss-review-ids", dismissReviewIds.join(","));
|
|
128
|
+
}
|
|
129
|
+
const hasMutations = hasMessageMutations || hasResolveOrMinimize;
|
|
130
|
+
// SHA is required when replying after actionable fixes/checks, or whenever dismissing
|
|
131
|
+
// (dismissal is a post-push operation that must race-check against a moving HEAD).
|
|
132
|
+
const requiresHeadSha = hasDismiss || (hasReply && (threads.length > 0 || checks.length > 0));
|
|
112
133
|
const resolveCommand = {
|
|
113
134
|
argv,
|
|
114
135
|
requiresHeadSha,
|
|
115
|
-
requiresDismissMessage:
|
|
136
|
+
requiresDismissMessage: hasMessageMutations,
|
|
116
137
|
...(replyThreadIds.length > 0 ? { replyThreadIds } : undefined),
|
|
117
138
|
...(resolveThreadIds.length > 0 ? { resolveThreadIds } : undefined),
|
|
139
|
+
...(hasDismiss ? { dismissReviewIds } : undefined),
|
|
118
140
|
hasMutations,
|
|
119
141
|
};
|
|
120
142
|
return { resolveCommand };
|
|
@@ -50,6 +50,7 @@ export async function runIterate(opts) {
|
|
|
50
50
|
branchProtection: report.branchProtection,
|
|
51
51
|
checks: buildRelevantChecks(report),
|
|
52
52
|
inProgressChecks: buildActiveChecks(report),
|
|
53
|
+
...(report.checks.ignoredNames?.length ? { ignoredNames: report.checks.ignoredNames } : {}),
|
|
53
54
|
activity: report.activity,
|
|
54
55
|
action: "cancel",
|
|
55
56
|
reason: report.mergeStatus.state === "MERGED" ? "merged" : "closed",
|
|
@@ -93,6 +94,7 @@ export async function runIterate(opts) {
|
|
|
93
94
|
branchProtection: report.branchProtection,
|
|
94
95
|
checks: buildRelevantChecks(report),
|
|
95
96
|
inProgressChecks: buildActiveChecks(report),
|
|
97
|
+
...(report.checks.ignoredNames?.length ? { ignoredNames: report.checks.ignoredNames } : {}),
|
|
96
98
|
activity: report.activity,
|
|
97
99
|
};
|
|
98
100
|
if (readyState.shouldCancel) {
|
|
@@ -79,7 +79,7 @@ resolveCommand, hasConflicts, prNumber, cancelledCount, firstLookThreads = [], f
|
|
|
79
79
|
instructions.push(`For each item under \`## Check annotations\`: inspect the referenced file range and decide whether the annotation requires a code change. These annotations are surfaced once per PR and do not need any resolve/minimize mutation.`);
|
|
80
80
|
}
|
|
81
81
|
if (changesRequestedReviews.length > 0) {
|
|
82
|
-
instructions.push(`For each bullet under \`## Changes-requested reviews\` above: read the review body and apply the requested changes
|
|
82
|
+
instructions.push(`For each bullet under \`## Changes-requested reviews\` above: read the review body and apply the requested changes.${(resolveCommand.dismissReviewIds?.length ?? 0) > 0 ? " Bot/non-human CR reviews listed in `--dismiss-review-ids` will be dismissed by the `resolve:` command after your push." : ""}`);
|
|
83
83
|
}
|
|
84
84
|
if (resolveOnlyCommand?.hasMutations)
|
|
85
85
|
instructions.push(`Run the \`resolve-only:\` command shown above — no substitutions needed.`);
|
package/bin/commands/poll.mjs
CHANGED
|
@@ -42,6 +42,20 @@ function writeQuietStatus(tick, elapsedSeconds, sleepSeconds, result) {
|
|
|
42
42
|
process.stderr.write(`[poll tick ${tick} / +${elapsedSeconds}s] WAIT ${result.status}/${result.mergeStateStatus}/${result.reviewDecision ?? "NO_REVIEW_DECISION"}${active}${commitSeg}${reviewRoundSeg}${reviewSeg} — sleeping ${sleepSeconds}s\n`);
|
|
43
43
|
}
|
|
44
44
|
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
45
|
+
const TIMER_DRIFT_TOLERANCE_MS = 500;
|
|
46
|
+
function writeWaitProgress(opts) {
|
|
47
|
+
const elapsedSeconds = Math.round(opts.elapsedMs / 1000);
|
|
48
|
+
const sleepSeconds = Math.round(opts.sleepMs / 1000);
|
|
49
|
+
if (!opts.quietStatus) {
|
|
50
|
+
writeTickProgress(opts.tick, elapsedSeconds, sleepSeconds, opts.verbose);
|
|
51
|
+
return opts.lastWaitSignature;
|
|
52
|
+
}
|
|
53
|
+
const signature = waitSignature(opts.result);
|
|
54
|
+
if (signature !== opts.lastWaitSignature) {
|
|
55
|
+
writeQuietStatus(opts.tick, elapsedSeconds, sleepSeconds, opts.result);
|
|
56
|
+
}
|
|
57
|
+
return signature;
|
|
58
|
+
}
|
|
45
59
|
export async function runPoll(opts) {
|
|
46
60
|
const { intervalSeconds, timeoutSeconds, ...iterateOpts } = opts;
|
|
47
61
|
const intervalMs = Math.min(intervalSeconds * 1000, MAX_TIMER_MS);
|
|
@@ -62,19 +76,20 @@ export async function runPoll(opts) {
|
|
|
62
76
|
const remainingMs = timeoutMs - elapsedMs;
|
|
63
77
|
if (remainingMs <= 0)
|
|
64
78
|
break;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
if (remainingMs + TIMER_DRIFT_TOLERANCE_MS < intervalMs)
|
|
80
|
+
break;
|
|
81
|
+
const nextSleepMs = intervalMs;
|
|
82
|
+
lastWaitSignature = writeWaitProgress({
|
|
83
|
+
tick,
|
|
84
|
+
elapsedMs,
|
|
85
|
+
sleepMs: nextSleepMs,
|
|
86
|
+
result: lastResult,
|
|
87
|
+
quietStatus,
|
|
88
|
+
verbose,
|
|
89
|
+
lastWaitSignature,
|
|
90
|
+
});
|
|
91
|
+
if (!quietStatus && !verbose)
|
|
92
|
+
dotsPrinted = true;
|
|
78
93
|
await sleep(nextSleepMs);
|
|
79
94
|
}
|
|
80
95
|
if (dotsPrinted)
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@ Poll dispatcher for iterating a PR to completion.
|
|
|
16
16
|
|
|
17
17
|
1. **Resolve the PR number** (`$N`): use the number or URL in `$ARGUMENTS`; otherwise infer it with `gh pr view --json number --jq .number`. If none is found, report an error and stop.
|
|
18
18
|
|
|
19
|
-
2. **Define the poll command once:** `pr-shepherd $N --interval
|
|
19
|
+
2. **Define the poll command once:** `pr-shepherd $N --interval 60s --timeout 4.5m`. Do not forward `$ARGUMENTS` as extra flags. Run `pr-shepherd --help` to inspect supported options.
|
|
20
20
|
|
|
21
21
|
3. **Loop:** Run the poll, print its full output, and follow its `## Instructions` section exactly. Then run the poll again. Repeat until the CLI emits `[CANCEL]` or `[ESCALATE]`, unless the human directs you to stop. Every other action (`[WAIT]`, `[MARK_READY]`, `[FIX_CODE]`) is non-terminal: do its instructions, then poll again. The poll already bounds each wait via `--interval`/`--timeout`; do not add manual `sleep`s between ticks.
|
|
22
22
|
|