pr-shepherd 0.56.1 → 0.56.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 +7 -4
- package/bin/api.d.mts +11 -1
- package/bin/api.mjs +25 -0
- package/bin/checks/classify.d.mts +5 -6
- package/bin/checks/classify.mjs +5 -7
- package/bin/checks/superseded.d.mts +4 -17
- package/bin/checks/superseded.mjs +67 -43
- package/bin/cli/args.mjs +3 -0
- package/bin/cli/fix-formatter.mjs +3 -0
- package/bin/cli/help-command-pages.d.mts +17 -2
- package/bin/cli/help-command-pages.mjs +16 -1
- package/bin/cli/help-iterate-poll-pages.d.mts +1 -1
- package/bin/cli/help-iterate-poll-pages.mjs +1 -1
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +2 -0
- package/bin/cli/help.d.mts +18 -3
- package/bin/cli/help.mjs +2 -0
- package/bin/cli/iterate-instructions.mjs +4 -1
- package/bin/cli/iterate-lean.mjs +3 -0
- package/bin/cli/iterate-merge-formatter.mjs +2 -0
- package/bin/cli/queue-removal-handler.d.mts +2 -0
- package/bin/cli/queue-removal-handler.mjs +86 -0
- package/bin/cli-parser.mjs +4 -0
- package/bin/commands/apply-queue-removal.d.mts +18 -0
- package/bin/commands/apply-queue-removal.mjs +44 -0
- package/bin/commands/check-fingerprint.mjs +2 -0
- package/bin/commands/check.mjs +13 -1
- package/bin/commands/iterate/check-evidence.d.mts +4 -0
- package/bin/commands/iterate/check-evidence.mjs +8 -0
- package/bin/commands/iterate/fix-code.mjs +20 -4
- package/bin/commands/iterate/index.mjs +15 -1
- package/bin/commands/iterate/merge-state.mjs +4 -1
- package/bin/commands/iterate/merge.d.mts +7 -1
- package/bin/commands/iterate/merge.mjs +59 -1
- package/bin/github/gql/poll-summary-check-contexts.gql +2 -0
- package/bin/github/poll-summary-checks.mjs +22 -18
- package/bin/github/poll-summary-fingerprint.mjs +7 -0
- package/bin/github/poll-summary-raw.d.mts +2 -0
- package/bin/github/poll-summary-readiness.mjs +11 -1
- package/bin/mcp/server.mjs +18 -0
- package/bin/state/queue-removal-ack.d.mts +19 -0
- package/bin/state/queue-removal-ack.mjs +66 -0
- package/bin/types/iterate.d.mts +4 -0
- package/bin/types/merge-queue.d.mts +2 -0
- package/package.json +2 -2
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- package/plugins/pr-shepherd/.mcp.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +3 -2
- package/plugins/pr-shepherd/skills/pr-shepherd/references/ci-failure-triage.md +1 -0
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { classifyChecks } from "../checks/classify.mjs";
|
|
2
|
+
import { parseCreatedAt } from "./batch-parser-helpers.mjs";
|
|
2
3
|
export function summarizePollSummaryChecks(raw) {
|
|
3
4
|
const rollups = summaryRollups(raw);
|
|
4
5
|
const counts = {};
|
|
5
|
-
for (const check of
|
|
6
|
-
additionalRelevantEvents: ["merge_group"],
|
|
7
|
-
})) {
|
|
6
|
+
for (const check of classifiedSummaryChecks(raw)) {
|
|
8
7
|
const key = {
|
|
9
8
|
passed: "passing",
|
|
10
9
|
failing: "failing",
|
|
@@ -17,33 +16,36 @@ export function summarizePollSummaryChecks(raw) {
|
|
|
17
16
|
counts[key] = (counts[key] ?? 0) + 1;
|
|
18
17
|
}
|
|
19
18
|
const summary = counts;
|
|
20
|
-
if (rollups.some((rollup) => rollup.contexts.pageInfo.hasPreviousPage)) {
|
|
19
|
+
if (rollups.some(({ rollup }) => rollup.contexts.pageInfo.hasPreviousPage)) {
|
|
21
20
|
summary.incomplete = true;
|
|
22
21
|
}
|
|
23
22
|
return summary;
|
|
24
23
|
}
|
|
25
24
|
/** Names of failing checks in the loaded summary rollup. */
|
|
26
25
|
export function failingSummaryCheckNames(raw) {
|
|
27
|
-
return
|
|
28
|
-
additionalRelevantEvents: ["merge_group"],
|
|
29
|
-
})
|
|
26
|
+
return classifiedSummaryChecks(raw)
|
|
30
27
|
.filter((check) => check.category === "failing")
|
|
31
28
|
.map((check) => check.name);
|
|
32
29
|
}
|
|
33
30
|
function summaryRollups(raw) {
|
|
34
31
|
return [
|
|
35
|
-
raw.commits.nodes[0]?.commit.statusCheckRollup,
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
{ rollup: raw.commits.nodes[0]?.commit.statusCheckRollup, scope: undefined },
|
|
33
|
+
{
|
|
34
|
+
rollup: raw.mergeQueueEntry?.headCommit?.statusCheckRollup,
|
|
35
|
+
scope: "merge_group",
|
|
36
|
+
},
|
|
37
|
+
].flatMap(({ rollup, scope }) => (rollup ? [{ rollup, scope }] : []));
|
|
38
38
|
}
|
|
39
|
-
function
|
|
40
|
-
|
|
39
|
+
function classifiedSummaryChecks(raw) {
|
|
40
|
+
// A successful check on the queue commit cannot cover a PR-head cancellation, or vice versa.
|
|
41
|
+
return summaryRollups(raw).flatMap(({ rollup, scope }) => classifyChecks(checkRuns(rollup, scope), scope === "merge_group" ? { additionalRelevantEvents: ["merge_group"] } : {}));
|
|
42
|
+
}
|
|
43
|
+
function checkRuns(rollup, scope) {
|
|
44
|
+
return rollup.contexts.nodes.map((context) => {
|
|
41
45
|
if (context.__typename === "StatusContext") {
|
|
42
46
|
return {
|
|
43
47
|
name: context.context,
|
|
44
|
-
status: context.state === "PENDING" || context.state === "EXPECTED"
|
|
45
|
-
? "IN_PROGRESS"
|
|
46
|
-
: "COMPLETED",
|
|
48
|
+
status: context.state === "PENDING" || context.state === "EXPECTED" ? "IN_PROGRESS" : "COMPLETED",
|
|
47
49
|
conclusion: context.state === "SUCCESS"
|
|
48
50
|
? "SUCCESS"
|
|
49
51
|
: context.state === "FAILURE" || context.state === "ERROR"
|
|
@@ -53,7 +55,7 @@ function checkRuns(rollups) {
|
|
|
53
55
|
detailsUrl: "",
|
|
54
56
|
event: null,
|
|
55
57
|
runId: null,
|
|
56
|
-
...(
|
|
58
|
+
...(scope && { scope }),
|
|
57
59
|
};
|
|
58
60
|
}
|
|
59
61
|
const run = context.checkSuite?.workflowRun;
|
|
@@ -70,7 +72,9 @@ function checkRuns(rollups) {
|
|
|
70
72
|
...(run?.workflow?.databaseId != null && {
|
|
71
73
|
workflowId: String(run.workflow.databaseId),
|
|
72
74
|
}),
|
|
73
|
-
...(
|
|
75
|
+
...(context.startedAt && { startedAtUnix: parseCreatedAt(context.startedAt) }),
|
|
76
|
+
...(context.completedAt && { completedAtUnix: parseCreatedAt(context.completedAt) }),
|
|
77
|
+
...(scope && { scope }),
|
|
74
78
|
};
|
|
75
|
-
})
|
|
79
|
+
});
|
|
76
80
|
}
|
|
@@ -8,6 +8,10 @@ import { createHash } from "node:crypto";
|
|
|
8
8
|
export function fingerprintRawSummaryPr(raw) {
|
|
9
9
|
if (!raw.updatedAt || !raw.headRefOid || !raw.baseRefOid)
|
|
10
10
|
return null;
|
|
11
|
+
const reviewTruncated = raw.comments.pageInfo.hasPreviousPage ||
|
|
12
|
+
raw.reviews.pageInfo.hasPreviousPage ||
|
|
13
|
+
raw.reviewThreads.pageInfo.hasPreviousPage ||
|
|
14
|
+
raw.reviewThreads.nodes.some((thread) => thread.comments.pageInfo.hasPreviousPage);
|
|
11
15
|
const evidence = {
|
|
12
16
|
number: raw.number,
|
|
13
17
|
state: raw.state,
|
|
@@ -19,6 +23,9 @@ export function fingerprintRawSummaryPr(raw) {
|
|
|
19
23
|
reviewDecision: raw.reviewDecision,
|
|
20
24
|
reviewRequests: raw.reviewRequests,
|
|
21
25
|
latestReviews: raw.latestReviews,
|
|
26
|
+
// Omitted bodies cannot participate in the hash. Bind truncated evidence
|
|
27
|
+
// to the PR revision instead, so updates require a fresh full review poll.
|
|
28
|
+
...(reviewTruncated && { reviewUpdatedAt: raw.updatedAt }),
|
|
22
29
|
comments: hideBodies(raw.comments),
|
|
23
30
|
reviews: hideBodies(raw.reviews),
|
|
24
31
|
reviewThreads: {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { summarizePollSummaryChecks } from "./poll-summary-checks.mjs";
|
|
2
|
+
import { parseBranchRules } from "./batch-parsers-rules.mjs";
|
|
3
|
+
import { rulesComplete } from "./fingerprint-fields.mjs";
|
|
2
4
|
/** Fresh compact evidence required before a READY receipt can be used. */
|
|
3
5
|
export function isCurrentSummaryReady(raw, checks, review, options = {}) {
|
|
4
6
|
const queued = options.allowQueuedProgress === true && raw.isInMergeQueue;
|
|
@@ -8,9 +10,17 @@ export function isCurrentSummaryReady(raw, checks, review, options = {}) {
|
|
|
8
10
|
const sourceChecks = queued
|
|
9
11
|
? summarizePollSummaryChecks({ ...raw, mergeQueueEntry: null })
|
|
10
12
|
: checks;
|
|
13
|
+
// The full one-PR check already surfaces review feedback. Certification
|
|
14
|
+
// need not reread historical conversations unless their resolution is a
|
|
15
|
+
// merge requirement. GitHub's CLEAN state proves that requirement is
|
|
16
|
+
// satisfied. BLOCKED is not conversation-specific, and queue progress
|
|
17
|
+
// alone is not that proof.
|
|
18
|
+
const requiresConversationResolution = parseBranchRules(raw.baseRef).requiresConversationResolution;
|
|
11
19
|
return (checks.incomplete !== true &&
|
|
12
20
|
sourceChecks.incomplete !== true &&
|
|
13
|
-
review.incomplete !== true
|
|
21
|
+
(review.incomplete !== true ||
|
|
22
|
+
raw.mergeStateStatus === "CLEAN" ||
|
|
23
|
+
(rulesComplete(raw.baseRef) && !requiresConversationResolution)) &&
|
|
14
24
|
raw.state === "OPEN" &&
|
|
15
25
|
!raw.isDraft &&
|
|
16
26
|
raw.mergeable !== "CONFLICTING" &&
|
package/bin/mcp/server.mjs
CHANGED
|
@@ -60,6 +60,12 @@ const appendJournalOperationSchema = z.object({
|
|
|
60
60
|
item: z.string(),
|
|
61
61
|
dryRun: z.boolean().optional(),
|
|
62
62
|
});
|
|
63
|
+
const acknowledgeQueueRemovalOperationSchema = z.object({
|
|
64
|
+
type: z.literal("acknowledge_queue_removal"),
|
|
65
|
+
requireSha: z.string().regex(/^[0-9a-f]{40}$/),
|
|
66
|
+
queueCommitOid: z.string().regex(/^[0-9a-f]{40}$/),
|
|
67
|
+
removedAtUnix: z.number().int().positive().safe(),
|
|
68
|
+
});
|
|
63
69
|
const applyInputSchema = z.object({
|
|
64
70
|
pr,
|
|
65
71
|
operations: z
|
|
@@ -67,6 +73,7 @@ const applyInputSchema = z.object({
|
|
|
67
73
|
reviewMutationsOperationSchema,
|
|
68
74
|
markFilesViewedOperationSchema,
|
|
69
75
|
appendJournalOperationSchema,
|
|
76
|
+
acknowledgeQueueRemovalOperationSchema,
|
|
70
77
|
]))
|
|
71
78
|
.min(1),
|
|
72
79
|
});
|
|
@@ -234,6 +241,17 @@ function formatApplyResult(result) {
|
|
|
234
241
|
return `${heading}\n\n${formatMarkFilesAsViewedResult(operation.result)}`;
|
|
235
242
|
case "append_journal":
|
|
236
243
|
return `${heading}\n\n${formatJournalResult(operation.result)}`;
|
|
244
|
+
case "acknowledge_queue_removal": {
|
|
245
|
+
const result = operation.result;
|
|
246
|
+
return [
|
|
247
|
+
heading,
|
|
248
|
+
"",
|
|
249
|
+
`PR: ${result.repo}#${result.pr}`,
|
|
250
|
+
`headSha: ${result.acknowledgment.headSha}`,
|
|
251
|
+
`queueCommitOid: ${result.acknowledgment.queueCommitOid}`,
|
|
252
|
+
`removedAtUnix: ${result.acknowledgment.removedAtUnix}`,
|
|
253
|
+
].join("\n");
|
|
254
|
+
}
|
|
237
255
|
}
|
|
238
256
|
})
|
|
239
257
|
.join("\n\n");
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** A caller's acknowledgment of one observed CI-driven native-stack queue removal. */
|
|
2
|
+
export interface QueueRemovalAcknowledgment {
|
|
3
|
+
headSha: string;
|
|
4
|
+
queueCommitOid: string;
|
|
5
|
+
removedAtUnix: number;
|
|
6
|
+
}
|
|
7
|
+
type StateKey = {
|
|
8
|
+
owner: string;
|
|
9
|
+
repo: string;
|
|
10
|
+
pr: number;
|
|
11
|
+
};
|
|
12
|
+
/** The only removal reasons that indicate a CI-driven queue ejection. */
|
|
13
|
+
export declare function isCiQueueRemovalReason(reason: string | null | undefined): boolean;
|
|
14
|
+
/** Missing, unreadable, or malformed acknowledgment state is treated as absent. */
|
|
15
|
+
export declare function readQueueRemovalAcknowledgment(key: StateKey): Promise<QueueRemovalAcknowledgment | null>;
|
|
16
|
+
/** Atomically store one acknowledgment. */
|
|
17
|
+
export declare function writeQueueRemovalAcknowledgment(key: StateKey, acknowledgment: QueueRemovalAcknowledgment): Promise<boolean>;
|
|
18
|
+
export declare function matchesQueueRemovalAcknowledgment(acknowledgment: QueueRemovalAcknowledgment | null, expected: Pick<QueueRemovalAcknowledgment, "headSha" | "queueCommitOid" | "removedAtUnix">): acknowledgment is QueueRemovalAcknowledgment;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** A caller's acknowledgment of one observed CI-driven native-stack queue removal. */
|
|
2
|
+
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { resolvePrStatePath } from "./base.mjs";
|
|
6
|
+
const FILE = "queue-removal-ack.json";
|
|
7
|
+
/** The only removal reasons that indicate a CI-driven queue ejection. */
|
|
8
|
+
export function isCiQueueRemovalReason(reason) {
|
|
9
|
+
return reason === "CI_FAILURE" || reason === "MERGE_QUEUE_POLICY_CHECK_FAILURE";
|
|
10
|
+
}
|
|
11
|
+
/** Missing, unreadable, or malformed acknowledgment state is treated as absent. */
|
|
12
|
+
export async function readQueueRemovalAcknowledgment(key) {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(await readFile(resolvePrStatePath(key, FILE), "utf8"));
|
|
15
|
+
return isAcknowledgment(parsed) ? parsed : null;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Atomically store one acknowledgment. */
|
|
22
|
+
export async function writeQueueRemovalAcknowledgment(key, acknowledgment) {
|
|
23
|
+
if (!isAcknowledgment(acknowledgment))
|
|
24
|
+
return false;
|
|
25
|
+
let tmp;
|
|
26
|
+
try {
|
|
27
|
+
const path = resolvePrStatePath(key, FILE);
|
|
28
|
+
tmp = `${path}.${randomUUID()}.tmp`;
|
|
29
|
+
await mkdir(dirname(path), { recursive: true });
|
|
30
|
+
await writeFile(tmp, `${JSON.stringify(acknowledgment)}\n`, "utf8");
|
|
31
|
+
await rename(tmp, path);
|
|
32
|
+
tmp = undefined;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
if (tmp !== undefined) {
|
|
40
|
+
try {
|
|
41
|
+
await unlink(tmp);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// A failed write may not have created the temporary file.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function matchesQueueRemovalAcknowledgment(acknowledgment, expected) {
|
|
50
|
+
return (acknowledgment !== null &&
|
|
51
|
+
acknowledgment.headSha === expected.headSha &&
|
|
52
|
+
acknowledgment.queueCommitOid === expected.queueCommitOid &&
|
|
53
|
+
acknowledgment.removedAtUnix === expected.removedAtUnix);
|
|
54
|
+
}
|
|
55
|
+
function isAcknowledgment(value) {
|
|
56
|
+
if (value === null || typeof value !== "object")
|
|
57
|
+
return false;
|
|
58
|
+
const candidate = value;
|
|
59
|
+
return (typeof candidate.headSha === "string" &&
|
|
60
|
+
/^[0-9a-f]{40}$/.test(candidate.headSha) &&
|
|
61
|
+
typeof candidate.queueCommitOid === "string" &&
|
|
62
|
+
/^[0-9a-f]{40}$/.test(candidate.queueCommitOid) &&
|
|
63
|
+
typeof candidate.removedAtUnix === "number" &&
|
|
64
|
+
Number.isSafeInteger(candidate.removedAtUnix) &&
|
|
65
|
+
candidate.removedAtUnix > 0);
|
|
66
|
+
}
|
package/bin/types/iterate.d.mts
CHANGED
|
@@ -124,6 +124,10 @@ interface FixRebaseAndPush {
|
|
|
124
124
|
protectedRuns: ProtectedRun[];
|
|
125
125
|
/** Requeue command emitted after merge-group remediation. */
|
|
126
126
|
requeue?: MergeCommandPlan;
|
|
127
|
+
/** Acknowledge an unrelated native-stack queue failure before fresh READY validation. */
|
|
128
|
+
queueRemovalAcknowledgment?: {
|
|
129
|
+
argv: string[];
|
|
130
|
+
};
|
|
127
131
|
/** First-look threads — previously hidden, surfaced for acknowledgment only. */
|
|
128
132
|
firstLookThreads: FirstLookThread[];
|
|
129
133
|
/** First-look comments — previously hidden, surfaced for acknowledgment only. */
|
|
@@ -10,6 +10,8 @@ export interface MergeQueueReport {
|
|
|
10
10
|
checksIncomplete?: true;
|
|
11
11
|
/** The current PR head is not a parent of the removed synthetic queue commit. */
|
|
12
12
|
headUpdatedAfterRemoval?: true;
|
|
13
|
+
/** The caller acknowledged this exact native-stack removal on the current head. */
|
|
14
|
+
removalAcknowledged?: true;
|
|
13
15
|
}
|
|
14
16
|
/**
|
|
15
17
|
* Raw counts of actionable work held back while the PR sits in the merge queue
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-shepherd",
|
|
3
|
-
"version": "0.56.
|
|
3
|
+
"version": "0.56.3",
|
|
4
4
|
"description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"automation",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"husky": "^9.1.7",
|
|
90
90
|
"knip": "^6.14.1",
|
|
91
91
|
"marked": "^18.0.11",
|
|
92
|
-
"oxfmt": "^0.
|
|
92
|
+
"oxfmt": "^0.70.0",
|
|
93
93
|
"oxlint": "^1.60.0",
|
|
94
94
|
"typescript": "^7.0.2",
|
|
95
95
|
"vitest": "^5.0.0"
|
|
@@ -8,7 +8,7 @@ allowed-tools: ["MCP", "Bash", "Read", "Grep", "Glob", "Edit", "Write"]
|
|
|
8
8
|
|
|
9
9
|
# pr-shepherd
|
|
10
10
|
|
|
11
|
-
Poll with the CLI. Use MCP `iterate` only when the CLI is unavailable. Stop at `[CANCEL]` or `[ESCALATE]`.
|
|
11
|
+
Poll with the CLI. Use MCP `iterate` only when the CLI is unavailable. Stop polling the selected pull request at `[CANCEL]` or `[ESCALATE]`.
|
|
12
12
|
|
|
13
13
|
## Create a PR
|
|
14
14
|
|
|
@@ -21,7 +21,8 @@ Poll with the CLI. Use MCP `iterate` only when the CLI is unavailable. Stop at `
|
|
|
21
21
|
## Dispatch
|
|
22
22
|
|
|
23
23
|
- Parse `$ARGUMENTS` for PR numbers, `owner/repo#N`, GitHub PR URLs, one `--stack PR`, and an optional `--merge`. Reject any other argument.
|
|
24
|
-
- A
|
|
24
|
+
- A user-supplied `--merge` explicitly authorizes merging or enqueueing the selected PR or stack. Run the emitted merge/enqueue commands without asking for another conversational confirmation; request runtime escalation when the host requires it.
|
|
25
|
+
- A request to merge, land, or enqueue the selected PR or stack also sets `--merge`. Creating or opening a PR without merge intent leaves merge mode off.
|
|
25
26
|
- A request to shepherd or merge a native stack, with an anchor PR and no literal `--stack`, uses that PR as the `--stack` selector. Otherwise infer the current branch PR.
|
|
26
27
|
- Follow the target repository's `AGENTS.md` while editing.
|
|
27
28
|
- CLI: turn `owner/repo#N` into `https://github.com/owner/repo/pull/N`. Pass other URLs and bare numbers through.
|
|
@@ -6,6 +6,7 @@ Apply when a step says `Playbook: "CI failure triage"`. For a GitHub Actions row
|
|
|
6
6
|
- `[rerun authorized]` plus a `rerun:` command means the viewer can rerun Actions (WRITE+) and this is the original attempt. Shepherd checked `repositoryPermission` and `run_attempt`.
|
|
7
7
|
- Run that printed command at most once. An `[attempt: N]` check never gets another rerun. A log excerpt on a later attempt is still investigation work. A later attempt with no usable evidence escalates when nothing else remains.
|
|
8
8
|
- A run in progress, `[conclusion: ACTION_REQUIRED]`, a check whose run id is not a GitHub Actions workflow, or a run with no attempt metadata never gets `[rerun authorized]`.
|
|
9
|
+
- A check with `scope: merge_group` never gets a rerun command. Rerunning cannot restore a removed queue entry and overwrites the failure evidence. If the failure belongs to this PR, fix the PR head. If it does not, follow the printed requeue instruction when a plan is present. Without `--merge`, report the failure without enqueueing. For native stacks, child sessions omit `--merge` and may still record the printed local acknowledgment after inspecting logs or an external provider URL. Complete fresh one-PR READY validation before returning to the aggregate selector with its original options; never enqueue a stack layer directly.
|
|
9
10
|
- Do not invent a handoff from `[FIX_CODE]`. Shepherd returns `[ESCALATE]` when no autonomous follow-up remains.
|
|
10
11
|
- Several bullets can share one run id (matrix jobs). The `rerun:` command is printed once, on the first bullet. Run it once.
|
|
11
12
|
|