pr-shepherd 0.16.4 → 0.18.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 +21 -13
- package/bin/cli/args.mjs +2 -0
- package/bin/cli/clean-formatter.mjs +20 -0
- package/bin/cli/default-iterate.mjs +1 -1
- package/bin/cli/duration-flag.mjs +22 -0
- package/bin/cli/exit-codes.mjs +14 -0
- package/bin/cli/fix-formatter.mjs +4 -12
- package/bin/cli/formatters.mjs +6 -10
- package/bin/cli/handlers.mjs +72 -36
- 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 +13 -17
- package/bin/cli/iterate-lean.mjs +9 -12
- package/bin/cli/list-formatters.mjs +21 -1
- package/bin/cli/poll-handler.mjs +42 -0
- package/bin/cli/runner.mjs +13 -3
- package/bin/cli-parser.clean.test-support.mjs +45 -0
- package/bin/cli-parser.iterate-fix.test-support.mjs +0 -4
- package/bin/cli-parser.iterate-fixtures.mjs +5 -2
- package/bin/cli-parser.iterate.test-support.mjs +0 -4
- package/bin/cli-parser.mjs +13 -2
- 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/clean.mjs +156 -0
- package/bin/commands/clean.test-support.mjs +48 -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/classify.mjs +37 -9
- package/bin/commands/iterate/escalate.mjs +26 -28
- package/bin/commands/iterate/fix-code.mjs +28 -6
- package/bin/commands/iterate/index.mjs +2 -0
- package/bin/commands/iterate/render.mjs +51 -40
- 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/comments/resolve.mjs +70 -11
- package/bin/github/batch-parser-helpers.mjs +38 -0
- package/bin/github/batch-parsers.mjs +12 -38
- package/bin/github/client.mjs +10 -1
- package/bin/github/gql/batch-pr.gql +9 -0
- package/bin/state/base.mjs +2 -1
- 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 +16 -5
- package/bin/agent-runtime.mjs +0 -7
package/bin/comments/resolve.mjs
CHANGED
|
@@ -1,9 +1,48 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
import { graphqlWithRateLimit } from "../github/client.mjs";
|
|
2
3
|
import { isRateLimitMessage, rateLimitFromError, rateLimitFromGraphQlResult, } from "./rate-limit.mjs";
|
|
3
4
|
import { setPendingOps } from "./pending-ops.mjs";
|
|
4
5
|
import { waitForSha } from "./sha-poll.mjs";
|
|
6
|
+
const COMMENTED_DISMISS_ERROR_PATTERNS = [
|
|
7
|
+
/can\s*not\s+dismiss[\s\S]*?commented pull request review/i,
|
|
8
|
+
];
|
|
9
|
+
function dedupeIds(ids) {
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
const out = [];
|
|
12
|
+
for (const id of ids) {
|
|
13
|
+
if (seen.has(id))
|
|
14
|
+
continue;
|
|
15
|
+
seen.add(id);
|
|
16
|
+
out.push(id);
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
function isCommentedDismissError(message) {
|
|
21
|
+
return COMMENTED_DISMISS_ERROR_PATTERNS.some((pattern) => pattern.test(message));
|
|
22
|
+
}
|
|
23
|
+
function dismissReviewNonDismissibleMessage(id) {
|
|
24
|
+
return `Not dismissed: ${id} is a COMMENTED review. Use --minimize-comment-ids instead; --dismiss-review-ids is only for CHANGES_REQUESTED reviews.`;
|
|
25
|
+
}
|
|
5
26
|
export async function applyResolveOptions(pr, repo, opts) {
|
|
6
|
-
|
|
27
|
+
const resolveThreadIds = dedupeIds(opts.resolveThreadIds ?? []);
|
|
28
|
+
const minimizeCommentIds = opts.minimizeCommentIds ?? [];
|
|
29
|
+
const dismissReviewIds = dedupeIds(opts.dismissReviewIds ?? []);
|
|
30
|
+
const minimizeCommentIdSet = new Set(minimizeCommentIds);
|
|
31
|
+
const filteredDismissReviewIds = dismissReviewIds.filter((id) => !minimizeCommentIdSet.has(id));
|
|
32
|
+
const overlappingDismissIds = dismissReviewIds.filter((id) => minimizeCommentIdSet.has(id));
|
|
33
|
+
const result = {
|
|
34
|
+
resolvedThreads: [],
|
|
35
|
+
minimizedComments: [],
|
|
36
|
+
dismissedReviews: [],
|
|
37
|
+
errors: [],
|
|
38
|
+
};
|
|
39
|
+
if (overlappingDismissIds.length > 0) {
|
|
40
|
+
result.skippedDismissals = [];
|
|
41
|
+
for (const id of overlappingDismissIds) {
|
|
42
|
+
result.skippedDismissals.push(id);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (filteredDismissReviewIds.length > 0 && !opts.dismissMessage) {
|
|
7
46
|
throw new Error("--message is required when dismissing reviews");
|
|
8
47
|
}
|
|
9
48
|
if (opts.requireSha) {
|
|
@@ -11,13 +50,7 @@ export async function applyResolveOptions(pr, repo, opts) {
|
|
|
11
50
|
// before reviewers see the fix.
|
|
12
51
|
await waitForSha(pr, repo, opts.requireSha);
|
|
13
52
|
}
|
|
14
|
-
|
|
15
|
-
resolvedThreads: [],
|
|
16
|
-
minimizedComments: [],
|
|
17
|
-
dismissedReviews: [],
|
|
18
|
-
errors: [],
|
|
19
|
-
};
|
|
20
|
-
await bulkApply(opts.resolveThreadIds ?? [], opts.minimizeCommentIds ?? [], opts.dismissReviewIds ?? [], opts.dismissMessage ?? "", result);
|
|
53
|
+
await bulkApply(resolveThreadIds, minimizeCommentIds, filteredDismissReviewIds, opts.dismissMessage ?? "", result);
|
|
21
54
|
return result;
|
|
22
55
|
}
|
|
23
56
|
export async function autoResolveOutdated(threadIds) {
|
|
@@ -65,13 +98,15 @@ async function bulkApplyChunk(resolveIds, minimizeIds, dismissIds, dismissMessag
|
|
|
65
98
|
if (resolveIds.length === 0 && minimizeIds.length === 0 && dismissIds.length === 0)
|
|
66
99
|
return false;
|
|
67
100
|
const doc = buildBulkMutation(resolveIds, minimizeIds, dismissIds, dismissMessage);
|
|
68
|
-
let data;
|
|
101
|
+
let data = {};
|
|
102
|
+
let graphQlErrors = [];
|
|
69
103
|
let rateLimitStop;
|
|
70
104
|
let suppressCurrentChunkErrors = false;
|
|
71
105
|
try {
|
|
72
106
|
const resp = await graphqlWithRateLimit(doc, {});
|
|
73
107
|
data = resp.data;
|
|
74
|
-
|
|
108
|
+
graphQlErrors = (resp.errors ?? []);
|
|
109
|
+
const graphQlErrorMessages = graphQlErrors.map((e) => e.message);
|
|
75
110
|
suppressCurrentChunkErrors = graphQlErrorMessages.some(isRateLimitMessage);
|
|
76
111
|
rateLimitStop = rateLimitFromGraphQlResult(graphQlErrorMessages, {
|
|
77
112
|
rateLimit: resp.rateLimit,
|
|
@@ -109,12 +144,27 @@ async function bulkApplyChunk(resolveIds, minimizeIds, dismissIds, dismissMessag
|
|
|
109
144
|
else if (!suppressCurrentChunkErrors)
|
|
110
145
|
result.errors.push(`${minimizeIds[i]}: minimize returned null or comment not minimized`);
|
|
111
146
|
}
|
|
147
|
+
const singleDismiss = dismissIds.length === 1;
|
|
148
|
+
const commentedDismissErrorIndexes = new Set();
|
|
149
|
+
let hasUnmappedCommentedDismissError = false;
|
|
150
|
+
for (const error of graphQlErrors) {
|
|
151
|
+
if (!isCommentedDismissError(error.message))
|
|
152
|
+
continue;
|
|
153
|
+
const alias = dismissErrorAliasIndex(error);
|
|
154
|
+
if (alias === undefined) {
|
|
155
|
+
hasUnmappedCommentedDismissError = true;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
commentedDismissErrorIndexes.add(alias);
|
|
159
|
+
}
|
|
112
160
|
for (let i = 0; i < dismissIds.length; i++) {
|
|
113
161
|
const d = data[`d${i}`];
|
|
114
162
|
if (d?.pullRequestReview != null)
|
|
115
163
|
result.dismissedReviews.push(dismissIds[i]);
|
|
116
164
|
else if (!suppressCurrentChunkErrors)
|
|
117
|
-
result.errors.push(
|
|
165
|
+
result.errors.push(commentedDismissErrorIndexes.has(i) || (singleDismiss && hasUnmappedCommentedDismissError)
|
|
166
|
+
? dismissReviewNonDismissibleMessage(dismissIds[i])
|
|
167
|
+
: `${dismissIds[i]}: dismiss returned null`);
|
|
118
168
|
}
|
|
119
169
|
if (rateLimitStop) {
|
|
120
170
|
result.errors.push(`rate limit: ${rateLimitStop.message}`);
|
|
@@ -123,3 +173,12 @@ async function bulkApplyChunk(resolveIds, minimizeIds, dismissIds, dismissMessag
|
|
|
123
173
|
}
|
|
124
174
|
return false;
|
|
125
175
|
}
|
|
176
|
+
function dismissErrorAliasIndex(error) {
|
|
177
|
+
if (!Array.isArray(error.path))
|
|
178
|
+
return undefined;
|
|
179
|
+
const alias = error.path.find((part) => typeof part === "string" && /^d\d+$/.test(part));
|
|
180
|
+
if (typeof alias !== "string")
|
|
181
|
+
return undefined;
|
|
182
|
+
const parsed = Number.parseInt(alias.slice(1), 10);
|
|
183
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
184
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function mapAuthorType(typeName) {
|
|
2
|
+
if (typeName === "User" || typeName === "Bot")
|
|
3
|
+
return typeName;
|
|
4
|
+
return "Unknown";
|
|
5
|
+
}
|
|
6
|
+
export function parseCreatedAt(iso) {
|
|
7
|
+
const ms = new Date(iso).getTime();
|
|
8
|
+
return Number.isFinite(ms) ? Math.floor(ms / 1000) : 0;
|
|
9
|
+
}
|
|
10
|
+
export function extractRunId(url) {
|
|
11
|
+
if (!url)
|
|
12
|
+
return null;
|
|
13
|
+
const m = /\/runs\/(\d+)/.exec(url);
|
|
14
|
+
return m ? (m[1] ?? null) : null;
|
|
15
|
+
}
|
|
16
|
+
export function extractCheckRunSummary(title, summary) {
|
|
17
|
+
const t = title?.trim();
|
|
18
|
+
if (t)
|
|
19
|
+
return t;
|
|
20
|
+
const firstLine = summary
|
|
21
|
+
?.split("\n")
|
|
22
|
+
?.find((l) => l.trim() !== "")
|
|
23
|
+
?.trim();
|
|
24
|
+
return firstLine || undefined;
|
|
25
|
+
}
|
|
26
|
+
export function mapStatusContextState(state) {
|
|
27
|
+
switch (state) {
|
|
28
|
+
case "SUCCESS":
|
|
29
|
+
return { status: "COMPLETED", conclusion: "SUCCESS" };
|
|
30
|
+
case "FAILURE":
|
|
31
|
+
case "ERROR":
|
|
32
|
+
return { status: "COMPLETED", conclusion: "FAILURE" };
|
|
33
|
+
case "PENDING":
|
|
34
|
+
case "EXPECTED":
|
|
35
|
+
default:
|
|
36
|
+
return { status: "IN_PROGRESS", conclusion: null };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mapAuthorType, parseCreatedAt, extractRunId, extractCheckRunSummary, mapStatusContextState, } from "./batch-parser-helpers.mjs";
|
|
1
2
|
export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes, rawReviewSummaryNodes, rawApprovedReviewNodes, rawCheckNodes) {
|
|
2
3
|
const reviewRequests = (raw.reviewRequests?.nodes ?? []).flatMap((n) => {
|
|
3
4
|
const login = n.requestedReviewer?.login ?? n.requestedReviewer?.name;
|
|
@@ -92,6 +93,16 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
92
93
|
}
|
|
93
94
|
return [];
|
|
94
95
|
});
|
|
96
|
+
const rawProtection = raw.baseRef?.branchProtectionRule ?? null;
|
|
97
|
+
const branchProtection = rawProtection
|
|
98
|
+
? {
|
|
99
|
+
requiresApprovingReviews: rawProtection.requiresApprovingReviews,
|
|
100
|
+
requiredApprovingReviewCount: rawProtection.requiredApprovingReviewCount,
|
|
101
|
+
requiresConversationResolution: rawProtection.requiresConversationResolution,
|
|
102
|
+
requiresStatusChecks: rawProtection.requiresStatusChecks,
|
|
103
|
+
requiredStatusCheckContexts: rawProtection.requiredStatusCheckContexts ?? [],
|
|
104
|
+
}
|
|
105
|
+
: null;
|
|
95
106
|
return {
|
|
96
107
|
nodeId: raw.id,
|
|
97
108
|
number: raw.number,
|
|
@@ -112,43 +123,6 @@ export function parseRawPr(raw, rawThreadPages, rawCommentNodes, rawReviewNodes,
|
|
|
112
123
|
reviewSummaries,
|
|
113
124
|
approvedReviews,
|
|
114
125
|
checks,
|
|
126
|
+
branchProtection,
|
|
115
127
|
};
|
|
116
128
|
}
|
|
117
|
-
function mapAuthorType(typeName) {
|
|
118
|
-
if (typeName === "User" || typeName === "Bot")
|
|
119
|
-
return typeName;
|
|
120
|
-
return "Unknown";
|
|
121
|
-
}
|
|
122
|
-
function parseCreatedAt(iso) {
|
|
123
|
-
const ms = new Date(iso).getTime();
|
|
124
|
-
return Number.isFinite(ms) ? Math.floor(ms / 1000) : 0;
|
|
125
|
-
}
|
|
126
|
-
function extractRunId(url) {
|
|
127
|
-
if (!url)
|
|
128
|
-
return null;
|
|
129
|
-
const m = /\/runs\/(\d+)/.exec(url);
|
|
130
|
-
return m ? (m[1] ?? null) : null;
|
|
131
|
-
}
|
|
132
|
-
function extractCheckRunSummary(title, summary) {
|
|
133
|
-
const t = title?.trim();
|
|
134
|
-
if (t)
|
|
135
|
-
return t;
|
|
136
|
-
const firstLine = summary
|
|
137
|
-
?.split("\n")
|
|
138
|
-
?.find((l) => l.trim() !== "")
|
|
139
|
-
?.trim();
|
|
140
|
-
return firstLine || undefined;
|
|
141
|
-
}
|
|
142
|
-
function mapStatusContextState(state) {
|
|
143
|
-
switch (state) {
|
|
144
|
-
case "SUCCESS":
|
|
145
|
-
return { status: "COMPLETED", conclusion: "SUCCESS" };
|
|
146
|
-
case "FAILURE":
|
|
147
|
-
case "ERROR":
|
|
148
|
-
return { status: "COMPLETED", conclusion: "FAILURE" };
|
|
149
|
-
case "PENDING":
|
|
150
|
-
case "EXPECTED":
|
|
151
|
-
default:
|
|
152
|
-
return { status: "IN_PROGRESS", conclusion: null };
|
|
153
|
-
}
|
|
154
|
-
}
|
package/bin/github/client.mjs
CHANGED
|
@@ -34,7 +34,16 @@ export async function getCurrentPrNumber() {
|
|
|
34
34
|
if (branch === "HEAD")
|
|
35
35
|
return null;
|
|
36
36
|
const repo = await getRepoInfo();
|
|
37
|
-
|
|
37
|
+
return getPrNumberForBranch(branch, repo.owner, repo.name);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Returns the PR number for a given branch, or null if no open PR is found. */
|
|
44
|
+
export async function getPrNumberForBranch(branch, owner, repo) {
|
|
45
|
+
try {
|
|
46
|
+
const result = await httpGraphql(PR_NUMBER_BY_BRANCH_QUERY, { owner, repo, branch });
|
|
38
47
|
return result.data.repository?.pullRequests.nodes[0]?.number ?? null;
|
|
39
48
|
}
|
|
40
49
|
catch {
|
|
@@ -24,6 +24,15 @@ query BatchPr(
|
|
|
24
24
|
nameWithOwner
|
|
25
25
|
}
|
|
26
26
|
baseRefName
|
|
27
|
+
baseRef {
|
|
28
|
+
branchProtectionRule {
|
|
29
|
+
requiresApprovingReviews
|
|
30
|
+
requiredApprovingReviewCount
|
|
31
|
+
requiresConversationResolution
|
|
32
|
+
requiresStatusChecks
|
|
33
|
+
requiredStatusCheckContexts
|
|
34
|
+
}
|
|
35
|
+
}
|
|
27
36
|
# Not paginated — capped at 50; PRs with more pending reviewers truncate silently.
|
|
28
37
|
reviewRequests(last: 50) {
|
|
29
38
|
nodes {
|
package/bin/state/base.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
export function resolveStateBase() {
|
|
4
|
-
|
|
4
|
+
const envDir = process.env["PR_SHEPHERD_STATE_DIR"];
|
|
5
|
+
return envDir ? envDir : join(tmpdir(), "pr-shepherd-state");
|
|
5
6
|
}
|
|
@@ -33,30 +33,35 @@ export function classifyItem(id, body, map) {
|
|
|
33
33
|
* Returns an empty Set if the directory does not yet exist.
|
|
34
34
|
*/
|
|
35
35
|
export async function loadSeenSet(key) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const entries = await readdir(dir);
|
|
39
|
-
return new Set(entries.filter((e) => e.endsWith(".json")).map((e) => e.slice(0, -5)));
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
return new Set();
|
|
43
|
-
}
|
|
36
|
+
const map = await loadSeenMap(key);
|
|
37
|
+
return new Set(map.keys());
|
|
44
38
|
}
|
|
45
39
|
/**
|
|
46
40
|
* Read the seen/ directory and return a Map from ID to SeenMarker.
|
|
47
41
|
* Used when the caller needs the stored bodyHash to detect in-place edits.
|
|
48
42
|
* Returns an empty Map if the directory does not yet exist.
|
|
43
|
+
*
|
|
44
|
+
* Map keys are the stored `id` field when present (guarding against
|
|
45
|
+
* case-insensitive filesystem collisions), falling back to the filename for
|
|
46
|
+
* legacy markers that predate this field.
|
|
49
47
|
*/
|
|
50
48
|
export async function loadSeenMap(key) {
|
|
51
49
|
const map = new Map();
|
|
52
50
|
try {
|
|
53
51
|
const dir = resolveDir(key);
|
|
54
52
|
const entries = await readdir(dir);
|
|
55
|
-
const
|
|
56
|
-
for (const id of ids) {
|
|
53
|
+
for (const entry of entries.filter((e) => e.endsWith(".json"))) {
|
|
57
54
|
try {
|
|
58
|
-
const raw = await readFile(join(dir,
|
|
59
|
-
|
|
55
|
+
const raw = await readFile(join(dir, entry), "utf8");
|
|
56
|
+
const marker = JSON.parse(raw);
|
|
57
|
+
// Prefer the stored id field; fall back to filename stem for legacy markers.
|
|
58
|
+
const mapKey = typeof marker.id === "string" ? marker.id : entry.slice(0, -5);
|
|
59
|
+
// Hash-based markers (those with an id field) take priority over legacy
|
|
60
|
+
// filename-based markers so that a stale legacy file cannot overwrite a
|
|
61
|
+
// newer hash-based entry that maps to the same key.
|
|
62
|
+
if (!map.has(mapKey) || typeof marker.id === "string") {
|
|
63
|
+
map.set(mapKey, marker);
|
|
64
|
+
}
|
|
60
65
|
}
|
|
61
66
|
catch {
|
|
62
67
|
// unreadable or malformed — skip
|
|
@@ -82,7 +87,7 @@ export async function hasSeen(key, id) {
|
|
|
82
87
|
* Write (or update) a "seen" marker for this id, storing the body hash so
|
|
83
88
|
* in-place edits can be detected on future fetches.
|
|
84
89
|
*
|
|
85
|
-
* - First call (no existing marker): creates `{ seenAt: now, bodyHash }`.
|
|
90
|
+
* - First call (no existing marker): creates `{ seenAt: now, bodyHash, id }`.
|
|
86
91
|
* - Subsequent call, hash unchanged: no-op (skips the write).
|
|
87
92
|
* - Subsequent call, hash changed: updates `bodyHash`, preserves original `seenAt`.
|
|
88
93
|
*
|
|
@@ -106,7 +111,10 @@ export async function markSeen(key, id, body) {
|
|
|
106
111
|
return;
|
|
107
112
|
const seenAt = existing?.seenAt ?? Date.now();
|
|
108
113
|
tmp = `${path}.${randomUUID()}.tmp`;
|
|
109
|
-
|
|
114
|
+
// Store `id` in the payload so loadSeenMap can key by the original ID
|
|
115
|
+
// rather than the filename, guarding against case-insensitive filesystems
|
|
116
|
+
// (e.g. macOS APFS) where IDs differing only in case would collide.
|
|
117
|
+
await writeFile(tmp, JSON.stringify({ seenAt, bodyHash: newHash, id }), "utf8");
|
|
110
118
|
await rename(tmp, path);
|
|
111
119
|
tmp = undefined;
|
|
112
120
|
}
|
|
@@ -153,5 +161,11 @@ function resolvePath(key, id) {
|
|
|
153
161
|
if (!SAFE_SEGMENT.test(id)) {
|
|
154
162
|
throw new Error(`Invalid state key segment "id": ${id}`);
|
|
155
163
|
}
|
|
156
|
-
|
|
164
|
+
// Hash the ID to produce a case-insensitive filename. On case-insensitive
|
|
165
|
+
// filesystems (macOS APFS), IDs that differ only in case (e.g. base64 IDs
|
|
166
|
+
// from GitHub like `ChG7F` vs `ChG7f`) would otherwise share the same file,
|
|
167
|
+
// causing seen-markers to overwrite each other and items to re-surface every
|
|
168
|
+
// tick. SHA-256 is case-sensitive so distinct IDs get distinct files.
|
|
169
|
+
const hash = createHash("sha256").update(id, "utf8").digest("hex");
|
|
170
|
+
return join(resolveDir(key), `${hash}.json`);
|
|
157
171
|
}
|
package/package.json
CHANGED
|
@@ -29,12 +29,12 @@ One-tick dispatcher for iterating a PR to completion.
|
|
|
29
29
|
If `MERGED` or `CLOSED`, output: `PR #N is already merged/closed. Nothing to do.` and stop.
|
|
30
30
|
|
|
31
31
|
3. **Select the package runner** from the target repository root:
|
|
32
|
-
- Prefer `package.json` `packageManager`: `pnpm@...` → `pnpm exec`, `yarn@...` → `yarn run`, `npm@...` → `npx`.
|
|
33
|
-
- If `packageManager` is absent, use lockfiles: `pnpm-lock.yaml` → `pnpm exec`, `yarn.lock` → `yarn run`, `package-lock.json` or no signal → `npx`.
|
|
32
|
+
- Prefer `package.json` `packageManager`: `pnpm@...` → `pnpm exec`, `yarn@...` → `yarn run`, `bun@...` → `bunx`, `npm@...` → `npx`.
|
|
33
|
+
- If `packageManager` is absent, use lockfiles: `pnpm-lock.yaml` → `pnpm exec`, `yarn.lock` → `yarn run`, `bun.lock` / `bun.lockb` → `bunx`, `package-lock.json` or no signal → `npx`.
|
|
34
34
|
|
|
35
35
|
4. **Run one iterate tick:**
|
|
36
36
|
|
|
37
|
-
If the package is missing in the target repository, tell the user to install pr-shepherd with the matching dev-dependency command: `pnpm add -D pr-shepherd`, `yarn add -D pr-shepherd`, or `npm install --save-dev pr-shepherd`.
|
|
37
|
+
If the package is missing in the target repository, tell the user to install pr-shepherd with the matching dev-dependency command: `pnpm add -D pr-shepherd`, `yarn add -D pr-shepherd`, `bun add -d pr-shepherd`, or `npm install --save-dev pr-shepherd`.
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
40
|
<runner> pr-shepherd <N>
|
|
@@ -42,7 +42,18 @@ One-tick dispatcher for iterating a PR to completion.
|
|
|
42
42
|
|
|
43
43
|
Print the full output. Follow the `## Instructions` section exactly.
|
|
44
44
|
|
|
45
|
-
5. **
|
|
45
|
+
5. **Persistence:** Do not stop polling/iterating unless directed by the human or until the CLI returns a terminal state (`[CANCEL]` or `[ESCALATE]`). Every other action — `[WAIT]`, `[MARK_READY]`, `[FIX_CODE]` — is non-terminal and must be followed by another tick via one of the strategies below.
|
|
46
|
+
|
|
47
|
+
6. **Stop conditions (terminal states):**
|
|
46
48
|
- Stop when the CLI emits `[CANCEL]` (ready-delay completed, or PR merged/closed).
|
|
47
49
|
- Stop when the CLI emits `[ESCALATE]`, including `stall-timeout` for repeated unchanged CI failures.
|
|
48
|
-
-
|
|
50
|
+
- **Do NOT merge the pull request** unless the human has explicitly requested or allowed it.
|
|
51
|
+
|
|
52
|
+
7. **Non-terminal actions** (`[WAIT]`, `[MARK_READY]`, `[FIX_CODE]`) — follow the `## Instructions` in the output. Pick one iteration strategy:
|
|
53
|
+
- **Blocking poll** — rerun as `<runner> pr-shepherd poll <N> [--interval <duration>] [--timeout <duration>]` (defaults: interval 30s, timeout 5m). Holds the agent turn until the action is non-WAIT or the timeout fires. Simplest when the agent cannot reliably schedule its own follow-up.
|
|
54
|
+
- **Scheduled wakeup + one tick** — schedule a single session-only follow-up task to rerun `<runner> pr-shepherd <N>` after a fresh 30s–4m delay, then end the turn.
|
|
55
|
+
- **Inline sleep + rerun** — sleep inline for a fresh 30s–4m delay, then rerun.
|
|
56
|
+
|
|
57
|
+
**Never write a custom polling loop** (shell `while`/`until` loops, script files that loop over pr-shepherd output, etc.). Custom loops poll only for terminal states and silently skip `[FIX_CODE]` handling — actionable review threads, failing checks, and resolve commands get missed. Use `pr-shepherd poll` for WAIT-state waiting; it exits on any non-WAIT action so the caller handles `[FIX_CODE]` and other actionable outputs normally.
|
|
58
|
+
|
|
59
|
+
Do not combine strategies (e.g., poll AND schedule a wakeup). Remember step 5 — every non-terminal output requires a follow-up tick.
|