pr-shepherd 0.55.1 → 0.56.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 +12 -9
- package/bin/api.d.mts +5 -0
- package/bin/api.mjs +12 -1
- package/bin/checks/job-log.d.mts +9 -0
- package/bin/checks/job-log.mjs +30 -0
- package/bin/checks/jobs-types.d.mts +14 -0
- package/bin/checks/jobs-types.mjs +7 -0
- package/bin/checks/related-jobs.d.mts +11 -0
- package/bin/checks/related-jobs.mjs +32 -0
- package/bin/checks/triage-budget.d.mts +17 -0
- package/bin/checks/triage-budget.mjs +49 -0
- package/bin/checks/triage.d.mts +6 -3
- package/bin/checks/triage.mjs +89 -61
- package/bin/cli/api-usage-formatter.mjs +2 -2
- package/bin/cli/fix-formatter.mjs +2 -0
- package/bin/cli/help-command-pages.d.mts +1 -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.d.mts +1 -1
- package/bin/cli/iterate-checks-formatter.mjs +2 -0
- package/bin/cli/iterate-instructions.mjs +1 -1
- package/bin/cli/related-jobs-format.d.mts +3 -0
- package/bin/cli/related-jobs-format.mjs +16 -0
- package/bin/commands/check-execution-context.d.mts +12 -0
- package/bin/commands/check-execution-context.mjs +38 -0
- package/bin/commands/check-fingerprint.mjs +14 -8
- package/bin/commands/check-unreported.d.mts +3 -2
- package/bin/commands/check-unreported.mjs +4 -4
- package/bin/commands/check.d.mts +2 -1
- package/bin/commands/check.mjs +21 -6
- package/bin/commands/commit-suggestion-instruction.mjs +2 -1
- package/bin/commands/iterate/check-instructions.d.mts +9 -5
- package/bin/commands/iterate/check-instructions.mjs +15 -35
- package/bin/commands/iterate/escalate.mjs +3 -0
- package/bin/commands/iterate/fix-code.mjs +6 -2
- package/bin/commands/iterate/helpers.mjs +1 -0
- package/bin/commands/iterate/index.mjs +20 -8
- package/bin/commands/iterate/native-stack-rebase.mjs +3 -2
- package/bin/commands/iterate/render.mjs +13 -5
- package/bin/commands/iterate/stale-ancestry.d.mts +2 -1
- package/bin/commands/iterate/stale-ancestry.mjs +3 -2
- package/bin/commands/iterate/unreported-required.mjs +1 -1
- package/bin/commands/playbook-pointer.d.mts +2 -0
- package/bin/commands/playbook-pointer.mjs +4 -0
- package/bin/commands/poll-quota.d.mts +2 -0
- package/bin/commands/poll-quota.mjs +18 -25
- package/bin/commands/poll-rate-limit-wait.mjs +13 -8
- package/bin/commands/poll-summary-instructions.mjs +1 -1
- package/bin/commands/ready-delay.d.mts +2 -0
- package/bin/commands/ready-delay.mjs +18 -0
- package/bin/commands/resolve-mutate.mjs +8 -9
- package/bin/commands/shepherd-journal.d.mts +1 -1
- package/bin/commands/shepherd-journal.mjs +3 -2
- package/bin/commands/stack-drain.mjs +2 -1
- package/bin/github/batch-raw-types.d.mts +2 -0
- package/bin/github/batch-receipt-evidence.d.mts +5 -0
- package/bin/github/batch-receipt-evidence.mjs +61 -0
- package/bin/github/batch.d.mts +4 -0
- package/bin/github/batch.mjs +42 -8
- package/bin/github/errors.d.mts +3 -0
- package/bin/github/errors.mjs +15 -6
- package/bin/github/gql/batch-pr-page.gql +1 -0
- package/bin/github/gql/batch-pr.gql +1 -0
- package/bin/github/gql/poll-summary-annotation-probe.gql +8 -0
- package/bin/github/gql/reply-thread-comments.gql +25 -0
- package/bin/github/gql/reply-thread-transcripts.gql +31 -0
- package/bin/github/merge-queue-checks.d.mts +2 -1
- package/bin/github/merge-queue-checks.mjs +18 -8
- package/bin/github/merge-target-rules.d.mts +2 -1
- package/bin/github/merge-target-rules.mjs +4 -4
- package/bin/github/poll-summary-annotation-probe.d.mts +2 -0
- package/bin/github/poll-summary-annotation-probe.mjs +7 -0
- package/bin/github/queries.d.mts +5 -0
- package/bin/github/queries.mjs +5 -0
- package/bin/github/rate-limit-kind.d.mts +13 -0
- package/bin/github/rate-limit-kind.mjs +25 -0
- package/bin/github/reply-thread-transcripts.d.mts +3 -0
- package/bin/github/reply-thread-transcripts.mjs +89 -0
- package/bin/github/rest-http.mjs +1 -0
- package/bin/github/rest-text.d.mts +2 -1
- package/bin/github/rest-text.mjs +5 -2
- package/bin/github/thread-comments.d.mts +5 -1
- package/bin/github/thread-comments.mjs +30 -6
- package/bin/mcp/server.mjs +32 -5
- package/bin/quota-warning.mjs +2 -2
- package/bin/reporters/agent.mjs +1 -0
- package/bin/threads/transcript.d.mts +1 -0
- package/bin/threads/transcript.mjs +4 -1
- package/bin/types/check-classification.d.mts +10 -0
- package/bin/types/report.d.mts +4 -1
- 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 +57 -70
- package/plugins/pr-shepherd/skills/pr-shepherd/references/branch-update.md +9 -0
- package/plugins/pr-shepherd/skills/pr-shepherd/references/ci-failure-triage.md +21 -0
- package/plugins/pr-shepherd/skills/pr-shepherd/references/journal.md +7 -0
- package/plugins/pr-shepherd/skills/pr-shepherd/references/review-mutations.md +9 -0
- package/plugins/pr-shepherd/skills/pr-shepherd/references/stack-merge.md +7 -0
- package/plugins/pr-shepherd/skills/pr-shepherd/references/suggestion-patches.md +10 -0
package/bin/cli/help.d.mts
CHANGED
|
@@ -212,7 +212,7 @@ Flags:
|
|
|
212
212
|
|
|
213
213
|
PR may be a number or GitHub pull request URL. When omitted, the current branch PR is inferred.
|
|
214
214
|
Exit code: 0 on success; nonzero on failure (sysexits.h — see docs/exit-codes.md).`;
|
|
215
|
-
readonly iterate: "pr-shepherd iterate\n\nRun one iterate tick for a pull request. The no-subcommand form polls; use this subcommand for a single tick.\nThe output contains one action and an action-specific ## Instructions section.\n\nUsage:\n pr-shepherd iterate [PR] [iterate-flags]\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Legacy no-op; workflow runs are never cancelled.\n --merge Shepherd through readiness, then emit a merge or merge-queue command.\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields.\n --help, -h Print this help and exit before GitHub, git, config, or log I/O.\n\nDurations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number is minutes; decimals are allowed only with an explicit unit (4.5m).\n\nActions:\n WAIT No immediate action; continue with the next poll.\n MARK_READY Draft PR was marked ready; continue with the next poll.\n READY Clean PR is inside the ready-delay.
|
|
215
|
+
readonly iterate: "pr-shepherd iterate\n\nRun one iterate tick for a pull request. The no-subcommand form polls; use this subcommand for a single tick.\nThe output contains one action and an action-specific ## Instructions section.\n\nUsage:\n pr-shepherd iterate [PR] [iterate-flags]\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Legacy no-op; workflow runs are never cancelled.\n --merge Shepherd through readiness, then emit a merge or merge-queue command.\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields.\n --help, -h Print this help and exit before GitHub, git, config, or log I/O.\n\nDurations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number is minutes; decimals are allowed only with an explicit unit (4.5m).\n\nActions:\n WAIT No immediate action; continue with the next poll.\n MARK_READY Draft PR was marked ready; continue with the next poll.\n READY Clean PR is inside the ready-delay. Schedule the rerun for when remainingSeconds elapses. Do not invent unrelated work. Already-owned later work may continue.\n FIX_CODE Agent action is required; follow the instructions, then continue polling.\n CANCEL Stop polling: merged/closed or ready-delay elapsed.\n ESCALATE Stop polling until a human provides direction.\n MERGE Run the emitted merge/queue command, then continue monitoring.\n\nExit codes:\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT or READY\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\n 15 MERGE\n A command/validation/GitHub failure exits with a sysexits.h code instead (see docs/exit-codes.md).";
|
|
216
216
|
readonly poll: "pr-shepherd poll\n\nRun iterate repeatedly for one PR, or read compact summaries for an explicit PR set or native\nGitHub stack. Aggregate mode returns when any row needs work, every row is terminal, or timeout.\nPoll exits as soon as iterate returns READY, MARK_READY, CANCEL, or ESCALATE, or when timeout\nreturns the last WAIT result. FIX_CODE starts a --debounce settle window (default:\npoll.debounceSeconds; built-in 1m): poll keeps\niterating at --interval, then runs one more tick after the window and returns that result.\nWith --until-terminal or --merge, poll also continues through MARK_READY.\n\nUsage:\n pr-shepherd poll [PR ...] [poll-flags] [iterate-flags]\n pr-shepherd poll --stack PR [poll-flags] [iterate-flags]\n\nPoll flags:\n --stack PR Select all entries in PR's native GitHub stack, bottom to top.\n --interval <duration> Sleep between WAIT ticks. Bare number = seconds. Default: poll.intervalSeconds (built-in 60s). Stack and multi-PR polls multiply that by poll.stackIntervalFactor (built-in 2) unless this flag is set.\n --timeout <duration> Maximum wall-clock wait for WAIT ticks. Bare number = seconds. Default: poll.timeoutSeconds (built-in 4.5m).\n --debounce <duration> Settle window after first FIX_CODE or stack SHEPHERD before returning. Bare number = seconds. Default: poll.debounceSeconds (built-in 60s). 0 disables.\n --quiet-status Print only changed WAIT snapshots. Overrides poll.quietStatus.\n --no-quiet-status Print every WAIT snapshot. Overrides poll.quietStatus.\n --until-terminal Continue through WAIT/MARK_READY until READY/FIX_CODE/MERGE/CANCEL/ESCALATE or stack SHEPHERD.\n\nForwarded iterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Legacy no-op; workflow runs are never cancelled.\n --merge Shepherd through readiness, then emit a merge or merge-queue command.\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed per-tick lines.\n --help, -h Print this help and exit before GitHub, git, config, or log I/O.\n\nDurations accept s/m/h suffixes: 30s, 4.5m, 1h. A bare number uses each flag's default unit (seconds\nfor --interval/--timeout/--debounce, minutes for --ready-delay/--stall-timeout); decimals are allowed only with\nan explicit unit (4.5m).\nEach WAIT tick writes a stderr line naming what it is waiting on by default; poll.quietStatus can change that default, --quiet-status/--no-quiet-status override it, and --verbose emits detailed per-tick lines.\nFIX_CODE debounce writes a remaining-seconds line to stderr. --timeout does not cut an in-flight debounce short.\nWith --until-terminal, --timeout is ignored for WAIT ticks and polling continues until READY, FIX_CODE, MERGE, CANCEL, or ESCALATE. With --merge, --timeout still bounds WAIT ticks; it only continues through MARK_READY while polling remains within that timeout.\n\nExit codes: same as iterate (the final tick's action/reason decides the code).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT or READY (including a WAIT returned by --timeout)\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\n 15 MERGE\n 16 SHEPHERD (--stack only: run the listed one-PR sessions, then rerun the selector)\n A command/validation/GitHub failure exits with a sysexits.h code instead (see docs/exit-codes.md).";
|
|
217
217
|
readonly clean: `pr-shepherd clean
|
|
218
218
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { renderRelatedJobLines } from "./related-jobs-format.mjs";
|
|
1
2
|
import { renderCheckAnnotation } from "./fix-formatter-extra.mjs";
|
|
2
3
|
export function formatRelevantChecks(checks) {
|
|
3
4
|
if (checks.length === 0)
|
|
@@ -14,6 +15,7 @@ function formatRelevantCheck(check) {
|
|
|
14
15
|
const lines = [`- \`${workflow}${job}\` [conclusion: ${check.conclusion}]`];
|
|
15
16
|
appendCheckFields(lines, check);
|
|
16
17
|
appendLogExcerpt(lines, check.logExcerpt);
|
|
18
|
+
lines.push(...renderRelatedJobLines(check.relatedJobs));
|
|
17
19
|
appendAnnotations(lines, check.annotations, check.logExcerpt);
|
|
18
20
|
return lines;
|
|
19
21
|
}
|
|
@@ -7,7 +7,7 @@ import { buildPrShepherdCommand } from "./runner.mjs";
|
|
|
7
7
|
export function buildSimpleIterateInstructions(result) {
|
|
8
8
|
switch (result.action) {
|
|
9
9
|
case "ready": {
|
|
10
|
-
const sentence = `PR #${result.pr} is ready. Ready-delay has ${result.remainingSeconds}s left.
|
|
10
|
+
const sentence = `PR #${result.pr} is ready. Ready-delay has ${result.remainingSeconds}s left. Rerun this command when the timer elapses. Do not invent unrelated work.`;
|
|
11
11
|
return [
|
|
12
12
|
result.quotaWarning ? buildQuotaAwareContinuation(result.quotaWarning, sentence) : sentence,
|
|
13
13
|
];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Shared text rendering of sibling failed jobs under a failing check bullet. */
|
|
2
|
+
export function renderRelatedJobLines(jobs, indent = " ") {
|
|
3
|
+
if (!jobs || jobs.length === 0)
|
|
4
|
+
return [];
|
|
5
|
+
const lines = [`${indent}Other failed jobs in this run:`];
|
|
6
|
+
for (const job of jobs) {
|
|
7
|
+
lines.push(`${indent}- \`${job.name}\` [conclusion: ${job.conclusion}]`);
|
|
8
|
+
if (job.failedStep)
|
|
9
|
+
lines.push(`${indent} > failed step: ${job.failedStep}`);
|
|
10
|
+
if (job.logExcerpt) {
|
|
11
|
+
for (const line of job.logExcerpt.split("\n"))
|
|
12
|
+
lines.push(`${indent} > ${line}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return lines;
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RepoInfo } from "../github/client.mts";
|
|
2
|
+
import { readStackTopology } from "../github/stack-read.mts";
|
|
3
|
+
import type { RawSummaryPr } from "../github/poll-summary-raw.mts";
|
|
4
|
+
/** Fresh for each iterate tick; never persisted or exposed in CLI output. */
|
|
5
|
+
export declare function createCheckExecutionContext(readyDelaySeconds?: number): {
|
|
6
|
+
readStackTopology(pr: number, repo: RepoInfo): ReturnType<typeof readStackTopology>;
|
|
7
|
+
wantsReceiptSummary(pr: number, repo: RepoInfo): Promise<boolean>;
|
|
8
|
+
setReceiptSummary(raw: RawSummaryPr | null): void;
|
|
9
|
+
getReceiptSummary(): RawSummaryPr | null;
|
|
10
|
+
invalidateReceiptSummary(): void;
|
|
11
|
+
};
|
|
12
|
+
export type CheckExecutionContext = ReturnType<typeof createCheckExecutionContext>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readStackTopology } from "../github/stack-read.mjs";
|
|
2
|
+
import { readReadyReceipt } from "../state/ready-receipts.mjs";
|
|
3
|
+
import { readyDelayElapsed } from "./ready-delay.mjs";
|
|
4
|
+
/** Fresh for each iterate tick; never persisted or exposed in CLI output. */
|
|
5
|
+
export function createCheckExecutionContext(readyDelaySeconds) {
|
|
6
|
+
const topologies = new Map();
|
|
7
|
+
let receiptSummary = null;
|
|
8
|
+
let receiptSummaryInvalidated = false;
|
|
9
|
+
return {
|
|
10
|
+
readStackTopology(pr, repo) {
|
|
11
|
+
const key = `${repo.owner.toLowerCase()}/${repo.name.toLowerCase()}#${pr}`;
|
|
12
|
+
let pending = topologies.get(key);
|
|
13
|
+
if (!pending) {
|
|
14
|
+
pending = readStackTopology(pr, repo);
|
|
15
|
+
topologies.set(key, pending);
|
|
16
|
+
}
|
|
17
|
+
return pending;
|
|
18
|
+
},
|
|
19
|
+
async wantsReceiptSummary(pr, repo) {
|
|
20
|
+
if (readyDelaySeconds === undefined)
|
|
21
|
+
return false;
|
|
22
|
+
const key = { owner: repo.owner, repo: repo.name, pr };
|
|
23
|
+
if (await readReadyReceipt(key))
|
|
24
|
+
return true;
|
|
25
|
+
return readyDelayElapsed(pr, repo.owner, repo.name, readyDelaySeconds);
|
|
26
|
+
},
|
|
27
|
+
setReceiptSummary(raw) {
|
|
28
|
+
receiptSummary = raw;
|
|
29
|
+
},
|
|
30
|
+
getReceiptSummary() {
|
|
31
|
+
return receiptSummaryInvalidated ? null : receiptSummary;
|
|
32
|
+
},
|
|
33
|
+
invalidateReceiptSummary() {
|
|
34
|
+
receiptSummaryInvalidated = true;
|
|
35
|
+
receiptSummary = null;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -31,20 +31,26 @@ export async function tryReuseFingerprintReport(prNumber, repo, stateKey, config
|
|
|
31
31
|
}
|
|
32
32
|
if (!reportAllowsFingerprintSkip(cached.report))
|
|
33
33
|
return null;
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
if (cached.fingerprint.isInMergeQueue)
|
|
35
|
+
return null;
|
|
36
|
+
if (!cached.fingerprint.checkSuitesComplete)
|
|
37
|
+
return null;
|
|
38
|
+
if (cached.fingerprint.commentCount > 100)
|
|
36
39
|
return null;
|
|
37
|
-
if (
|
|
40
|
+
if (cached.fingerprint.reviewCount > 100)
|
|
38
41
|
return null;
|
|
39
|
-
if (
|
|
42
|
+
if (cached.fingerprint.threadCount > 20)
|
|
40
43
|
return null;
|
|
41
|
-
if (
|
|
44
|
+
if (cached.fingerprint.hasMultiCommentThreads)
|
|
42
45
|
return null;
|
|
43
|
-
if (
|
|
46
|
+
if (!cached.fingerprint.rulesComplete)
|
|
47
|
+
return null;
|
|
48
|
+
const live = await fetchPrFingerprint(prNumber, repo);
|
|
49
|
+
if (live.isInMergeQueue || !live.checkSuitesComplete)
|
|
44
50
|
return null;
|
|
45
|
-
if (live.
|
|
51
|
+
if (live.commentCount > 100 || live.reviewCount > 100 || live.threadCount > 20)
|
|
46
52
|
return null;
|
|
47
|
-
if (
|
|
53
|
+
if (live.hasMultiCommentThreads || !live.rulesComplete)
|
|
48
54
|
return null;
|
|
49
55
|
if (!fingerprintsEqual(cached.fingerprint, live))
|
|
50
56
|
return null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type WorkflowSuiteSnapshot } from "../checks/unreported-required.mts";
|
|
2
2
|
import type { BatchPrData, CheckRun, ShepherdReport } from "../types.mts";
|
|
3
3
|
import type { RepoInfo } from "../github/client.mts";
|
|
4
|
+
import type { CheckExecutionContext } from "./check-execution-context.mts";
|
|
4
5
|
export interface UnreportedRequiredFields {
|
|
5
6
|
unreportedRequiredChecks?: string[];
|
|
6
7
|
trunkBehindBy?: number;
|
|
@@ -20,9 +21,9 @@ export declare function collectUnreportedRequired(input: {
|
|
|
20
21
|
name: string;
|
|
21
22
|
pr: number;
|
|
22
23
|
relevantEvents: readonly string[];
|
|
23
|
-
}): Promise<UnreportedRequiredFields>;
|
|
24
|
+
}, context?: CheckExecutionContext): Promise<UnreportedRequiredFields>;
|
|
24
25
|
/**
|
|
25
26
|
* A fingerprint hit can keep a stale trunk `behindBy`. Refresh the trunk compare
|
|
26
27
|
* and the required-context diff without refetching the whole batch.
|
|
27
28
|
*/
|
|
28
|
-
export declare function refreshCachedUnreported(report: ShepherdReport, repo: RepoInfo): Promise<ShepherdReport>;
|
|
29
|
+
export declare function refreshCachedUnreported(report: ShepherdReport, repo: RepoInfo, context?: CheckExecutionContext): Promise<ShepherdReport>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { actionsWorkflowInProgress, reportedCheckNames, unreportedRequiredContexts, } from "../checks/unreported-required.mjs";
|
|
2
2
|
import { loadBaseBehindBy, loadMergeTargetStatus } from "../github/merge-target-rules.mjs";
|
|
3
3
|
/** Load trunk rules for a stack and diff them against the head's check names. */
|
|
4
|
-
export async function collectUnreportedRequired(input) {
|
|
4
|
+
export async function collectUnreportedRequired(input, context) {
|
|
5
5
|
const localContexts = input.batchData.branchRules?.requiredStatusCheckContexts ??
|
|
6
6
|
input.batchData.branchProtection?.requiredStatusCheckContexts ??
|
|
7
7
|
[];
|
|
@@ -13,7 +13,7 @@ export async function collectUnreportedRequired(input) {
|
|
|
13
13
|
headRefName: input.batchData.headRefName,
|
|
14
14
|
localContexts,
|
|
15
15
|
stack: input.batchData.stack,
|
|
16
|
-
});
|
|
16
|
+
}, context);
|
|
17
17
|
const unreported = unreportedRequiredContexts(target.contexts, reportedCheckNames(input.checks));
|
|
18
18
|
const actionsRunning = actionsWorkflowInProgress(input.suites, new Set(input.relevantEvents));
|
|
19
19
|
const baseBehindBy = unreported.length > 0 && !input.batchData.stack
|
|
@@ -34,7 +34,7 @@ export async function collectUnreportedRequired(input) {
|
|
|
34
34
|
* A fingerprint hit can keep a stale trunk `behindBy`. Refresh the trunk compare
|
|
35
35
|
* and the required-context diff without refetching the whole batch.
|
|
36
36
|
*/
|
|
37
|
-
export async function refreshCachedUnreported(report, repo) {
|
|
37
|
+
export async function refreshCachedUnreported(report, repo, context) {
|
|
38
38
|
const stack = report.mergeStatus?.mergeRequirements?.stack;
|
|
39
39
|
if (!report.headRefName)
|
|
40
40
|
return report;
|
|
@@ -48,7 +48,7 @@ export async function refreshCachedUnreported(report, repo) {
|
|
|
48
48
|
headRefName: report.headRefName,
|
|
49
49
|
localContexts: report.mergeStatus.mergeRequirements?.requiredStatusChecks?.contexts ?? [],
|
|
50
50
|
stack,
|
|
51
|
-
});
|
|
51
|
+
}, context);
|
|
52
52
|
const unreported = unreportedRequiredContexts(target.contexts, reportedCheckNames(reportChecks(report)));
|
|
53
53
|
const next = { ...report };
|
|
54
54
|
if (unreported.length > 0)
|
package/bin/commands/check.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CheckExecutionContext } from "./check-execution-context.mts";
|
|
1
2
|
import type { GlobalOptions, ShepherdReport } from "../types.mts";
|
|
2
3
|
export declare function runCheck(opts: GlobalOptions & {
|
|
3
4
|
autoResolve?: boolean;
|
|
@@ -6,4 +7,4 @@ export declare function runCheck(opts: GlobalOptions & {
|
|
|
6
7
|
persistSeen?: boolean;
|
|
7
8
|
fingerprintCache?: boolean;
|
|
8
9
|
merge?: boolean;
|
|
9
|
-
}): Promise<ShepherdReport>;
|
|
10
|
+
}, context?: CheckExecutionContext): Promise<ShepherdReport>;
|
package/bin/commands/check.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { getRepoInfo, getCurrentPrNumber } from "../github/client.mjs";
|
|
|
7
7
|
import { classifyChecks, getCiVerdict } from "../checks/classify.mjs";
|
|
8
8
|
import { mergeStartupFailureChecks } from "../checks/startup-failures.mjs";
|
|
9
9
|
import { fetchStartupFailureChecks, triageFailingChecks } from "../checks/triage.mjs";
|
|
10
|
+
import { TriageBudget } from "../checks/triage-budget.mjs";
|
|
10
11
|
import { deriveMergeStatus } from "../merge-status/derive.mjs";
|
|
11
12
|
import { loadConfig } from "../config/load.mjs";
|
|
12
13
|
import { classifyVisibleComments } from "../comments/visible-comments.mjs";
|
|
@@ -26,7 +27,7 @@ import { discoverRuleFiles, loadRules } from "../classify/loader.mjs";
|
|
|
26
27
|
import { buildClassifyIndex, partitionBatch } from "../classify/apply.mjs";
|
|
27
28
|
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
28
29
|
import { getEffectiveCwd } from "../execution-context.mjs";
|
|
29
|
-
export async function runCheck(opts) {
|
|
30
|
+
export async function runCheck(opts, context) {
|
|
30
31
|
const repo = opts.targetRepository ?? (await getRepoInfo());
|
|
31
32
|
const prNumber = opts.prNumber ?? (await getCurrentPrNumber());
|
|
32
33
|
if (prNumber === null) {
|
|
@@ -38,10 +39,15 @@ export async function runCheck(opts) {
|
|
|
38
39
|
if (reuseFingerprint) {
|
|
39
40
|
const cached = await tryReuseFingerprintReport(prNumber, repo, stateKey, config);
|
|
40
41
|
if (cached)
|
|
41
|
-
return refreshCachedUnreported(cached, repo);
|
|
42
|
+
return refreshCachedUnreported(cached, repo, context);
|
|
42
43
|
}
|
|
43
44
|
const paginateApprovedReviews = config.iterate.minimizeApprovals;
|
|
44
|
-
const
|
|
45
|
+
const includeReceiptSummary = (await context?.wantsReceiptSummary(prNumber, repo)) ?? false;
|
|
46
|
+
const result = await fetchPrBatch(prNumber, repo, {
|
|
47
|
+
paginateApprovedReviews,
|
|
48
|
+
...(includeReceiptSummary && { includeReceiptSummary: true }),
|
|
49
|
+
});
|
|
50
|
+
context?.setReceiptSummary(result.receiptSummary ?? null);
|
|
45
51
|
let batchData = result.data;
|
|
46
52
|
const unknownRefresh = await refreshUnknownMergeability(prNumber, repo, batchData);
|
|
47
53
|
batchData = unknownRefresh.batchData;
|
|
@@ -55,9 +61,10 @@ export async function runCheck(opts) {
|
|
|
55
61
|
return terminal;
|
|
56
62
|
}
|
|
57
63
|
const startupFailuresNeedAttempt = batchData.checks.some((check) => check.source === "startup_failure" && check.runAttempt === undefined);
|
|
64
|
+
const triageBudget = new TriageBudget();
|
|
58
65
|
const startupFailureChecks = result.checkSuitesComplete && !startupFailuresNeedAttempt
|
|
59
66
|
? []
|
|
60
|
-
: await fetchStartupFailureChecks(repo, batchData.headRefOid, prNumber, stateKey);
|
|
67
|
+
: await fetchStartupFailureChecks(repo, batchData.headRefOid, prNumber, stateKey, triageBudget);
|
|
61
68
|
const allChecks = mergeStartupFailureChecks(batchData.checks, startupFailureChecks);
|
|
62
69
|
const classifiedPrChecks = classifyChecks(allChecks);
|
|
63
70
|
const latestRemoval = batchData.latestMergeQueueRemoval;
|
|
@@ -103,8 +110,10 @@ export async function runCheck(opts) {
|
|
|
103
110
|
const filtered = classifiedChecks.filter((c) => c.category === "filtered");
|
|
104
111
|
const ignored = classifiedChecks.filter((c) => c.category === "ignored");
|
|
105
112
|
const triagedBase = failing.length > 0 && !opts.skipTriage
|
|
106
|
-
? await triageFailingChecks(failing, repo, stateKey)
|
|
113
|
+
? await triageFailingChecks(failing, repo, stateKey, triageBudget, classifiedChecks.filter((c) => c.category !== "failing"))
|
|
107
114
|
: failing;
|
|
115
|
+
triageBudget.throwIfSecondary();
|
|
116
|
+
triageBudget.reportOmissionIfNeeded();
|
|
108
117
|
const seenMap = await loadSeenMap(stateKey);
|
|
109
118
|
const botUsernames = normalizeBotUsernames(config.botUsernames);
|
|
110
119
|
const ruleSet = await loadRules(discoverRuleFiles(getEffectiveCwd()));
|
|
@@ -168,7 +177,7 @@ export async function runCheck(opts) {
|
|
|
168
177
|
name: repo.name,
|
|
169
178
|
pr: prNumber,
|
|
170
179
|
relevantEvents: config.checks.ciTriggerEvents,
|
|
171
|
-
});
|
|
180
|
+
}, context);
|
|
172
181
|
let status = computeStatus(verdict, threadVisibility.activeThreads.length + threadVisibility.resolutionOnlyThreads.length, visibleCommentClassification.actionable.length, mergeStatus, changesRequestedReviewCount, unreported.hasUnreportedRequired);
|
|
173
182
|
// Resolve any pending mergeability refresh (and the resulting MERGED/CLOSED short-circuit)
|
|
174
183
|
// before deciding what to persist below — deferWhileQueued must see the same final,
|
|
@@ -251,6 +260,12 @@ export async function runCheck(opts) {
|
|
|
251
260
|
ruleAutoResolveCommentIds: partition.ruleAutoResolveCommentIds.filter((id) => batchData.comments.find((comment) => comment.id === id)?.viewerCanMinimize === true),
|
|
252
261
|
ruleAutoResolveReviewSummaryIds: partition.ruleAutoResolveReviewSummaryIds.filter((id) => batchData.reviewSummaries.find((review) => review.id === id)?.viewerCanMinimize === true),
|
|
253
262
|
};
|
|
263
|
+
if (opts.autoMinimizeSuppressed === true &&
|
|
264
|
+
(authorizedPartition.ruleAutoResolveThreadIds.length > 0 ||
|
|
265
|
+
authorizedPartition.ruleAutoResolveCommentIds.length > 0 ||
|
|
266
|
+
authorizedPartition.ruleAutoResolveReviewSummaryIds.length > 0)) {
|
|
267
|
+
context?.invalidateReceiptSummary();
|
|
268
|
+
}
|
|
254
269
|
const { threadIds: authorizedRuleAutoResolveThreadIds, commentIds: ruleAutoResolveCommentIds, reviewSummaryIds: ruleAutoResolveReviewSummaryIds, autoResolved, autoMinimized, autoResolveErrors, errorReasons, } = await applySuppressedRuleAutoResolve({
|
|
255
270
|
enabled: opts.autoMinimizeSuppressed === true,
|
|
256
271
|
partition: authorizedPartition,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildPrShepherdCommand } from "../cli/runner.mjs";
|
|
2
|
+
import { playbookPointer } from "./playbook-pointer.mjs";
|
|
2
3
|
/**
|
|
3
4
|
* Build the `build-suggestion-patches` instruction step for agent consumers.
|
|
4
5
|
* Currently emitted by iterate `fix_code` for suggestion review threads. The CLI keeps
|
|
@@ -19,5 +20,5 @@ export function buildCommitSuggestionInstruction(prReference, sectionName) {
|
|
|
19
20
|
"<one-sentence headline>",
|
|
20
21
|
"--format=json",
|
|
21
22
|
]).text;
|
|
22
|
-
return `For
|
|
23
|
+
return `For every \`[suggestion]\` thread under \`${sectionName}\`, run one \`${command}\`, repeating \`--thread-id\` and \`--message\` in displayed order. ${playbookPointer("Suggestion patches")}`;
|
|
23
24
|
}
|
|
@@ -2,15 +2,18 @@ import type { AgentCheck, ResolveCommand, Review } from "../../types.mts";
|
|
|
2
2
|
/** Build the stale-CR clause appended to the `## Changes-requested reviews` instruction. */
|
|
3
3
|
export declare function buildCrStaleClause(reviews: Review[]): string;
|
|
4
4
|
/**
|
|
5
|
-
* Build the optional
|
|
6
|
-
* and the user configured a non-blank `iterate.behindBaseHint` — the CLI never prescribes
|
|
5
|
+
* Build the optional branch-update hint. Empty unless the branch is behind or conflicts with its
|
|
6
|
+
* base and the user configured a non-blank `iterate.behindBaseHint` — the CLI never prescribes
|
|
7
7
|
* rebase/merge mechanics itself (see "Keep skills and loop prompts minimal" in AGENTS.md); this
|
|
8
8
|
* only echoes back the caller's own configured pointer. `hint` is trimmed and type-checked at the
|
|
9
9
|
* point of use (rather than at config load) so a malformed rc file value (non-string, or
|
|
10
10
|
* whitespace-only) degrades to "no hint" instead of rendering garbage into agent-facing text or
|
|
11
11
|
* discarding the rest of the user's config.
|
|
12
12
|
*/
|
|
13
|
-
export declare function buildBehindBaseHintInstruction(baseBranch: string, hint: string,
|
|
13
|
+
export declare function buildBehindBaseHintInstruction(baseBranch: string, hint: string, branch: {
|
|
14
|
+
isBehind: boolean;
|
|
15
|
+
hasConflicts: boolean;
|
|
16
|
+
}): string[];
|
|
14
17
|
/**
|
|
15
18
|
* Give one branch-refresh recovery path after Shepherd's single workflow rerun has failed.
|
|
16
19
|
* The fetched PR base branch is raw context; the caller still owns repository-specific git
|
|
@@ -31,8 +34,9 @@ export declare function buildRepeatedWorkflowBranchRecoveryInstructions(baseBran
|
|
|
31
34
|
* tells the agent that playbook exists.
|
|
32
35
|
*/
|
|
33
36
|
export declare function buildResolveCommandInstruction(resolveCommand: ResolveCommand): string[];
|
|
34
|
-
/**
|
|
37
|
+
/** One pointer. Conclusion, rerun, and bare-check rules live in the CI playbook. */
|
|
35
38
|
export declare function buildFailingCheckInstructions(checks: AgentCheck[]): string[];
|
|
36
39
|
/** Update the PR branch after an external blocker merges or closes. Never rerun that job. */
|
|
37
40
|
export declare function buildReleasedBlockerInstruction(prNumber: number): string;
|
|
38
|
-
|
|
41
|
+
/** Recurrence only. Commit, push, rerun, and SHA steps are earlier instructions. */
|
|
42
|
+
export declare function buildFixCompletionInstruction(): string;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { playbookPointer } from "../playbook-pointer.mjs";
|
|
2
|
+
const FIX_CODE_CONTINUATION = "`[FIX_CODE]` is non-terminal. Iterate immediately with the same options.";
|
|
1
3
|
/** Build the stale-CR clause appended to the `## Changes-requested reviews` instruction. */
|
|
2
4
|
export function buildCrStaleClause(reviews) {
|
|
3
5
|
const human = reviews.some((r) => r.staleReview && !r.staleBotCr)
|
|
@@ -6,19 +8,20 @@ export function buildCrStaleClause(reviews) {
|
|
|
6
8
|
return human;
|
|
7
9
|
}
|
|
8
10
|
/**
|
|
9
|
-
* Build the optional
|
|
10
|
-
* and the user configured a non-blank `iterate.behindBaseHint` — the CLI never prescribes
|
|
11
|
+
* Build the optional branch-update hint. Empty unless the branch is behind or conflicts with its
|
|
12
|
+
* base and the user configured a non-blank `iterate.behindBaseHint` — the CLI never prescribes
|
|
11
13
|
* rebase/merge mechanics itself (see "Keep skills and loop prompts minimal" in AGENTS.md); this
|
|
12
14
|
* only echoes back the caller's own configured pointer. `hint` is trimmed and type-checked at the
|
|
13
15
|
* point of use (rather than at config load) so a malformed rc file value (non-string, or
|
|
14
16
|
* whitespace-only) degrades to "no hint" instead of rendering garbage into agent-facing text or
|
|
15
17
|
* discarding the rest of the user's config.
|
|
16
18
|
*/
|
|
17
|
-
export function buildBehindBaseHintInstruction(baseBranch, hint,
|
|
19
|
+
export function buildBehindBaseHintInstruction(baseBranch, hint, branch) {
|
|
18
20
|
const trimmedHint = typeof hint === "string" ? hint.trim() : "";
|
|
19
|
-
if (!isBehind || trimmedHint === "")
|
|
21
|
+
if ((!branch.isBehind && !branch.hasConflicts) || trimmedHint === "")
|
|
20
22
|
return [];
|
|
21
|
-
|
|
23
|
+
const state = branch.hasConflicts ? "conflicts with" : "is behind";
|
|
24
|
+
return [`The branch ${state} PR base branch \`${baseBranch}\`. ${trimmedHint} before pushing.`];
|
|
22
25
|
}
|
|
23
26
|
/**
|
|
24
27
|
* Give one branch-refresh recovery path after Shepherd's single workflow rerun has failed.
|
|
@@ -53,48 +56,25 @@ export function buildResolveCommandInstruction(resolveCommand) {
|
|
|
53
56
|
return [];
|
|
54
57
|
const instructions = [];
|
|
55
58
|
if (resolveCommand.requiresHeadSha) {
|
|
56
|
-
instructions.push("If you did not change code, replace `$HEAD_SHA` with `$(git rev-parse HEAD)
|
|
59
|
+
instructions.push("If you did not change code, replace `$HEAD_SHA` with `$(git rev-parse HEAD)` (it must equal the remote PR head). If you did, use the pushed SHA.");
|
|
57
60
|
}
|
|
58
61
|
if (resolveCommand.requiresDismissMessage) {
|
|
59
62
|
instructions.push("Replace `$DISMISS_MESSAGE` with one sentence describing what changed.");
|
|
60
63
|
}
|
|
61
|
-
instructions.push(
|
|
64
|
+
instructions.push(`Run the \`apply review:\` command above. ${playbookPointer("Review-mutation mechanics")}`);
|
|
62
65
|
return instructions;
|
|
63
66
|
}
|
|
64
|
-
/**
|
|
67
|
+
/** One pointer. Conclusion, rerun, and bare-check rules live in the CI playbook. */
|
|
65
68
|
export function buildFailingCheckInstructions(checks) {
|
|
66
69
|
if (checks.length === 0)
|
|
67
70
|
return [];
|
|
68
|
-
|
|
69
|
-
const hasTriageable = checks.some((c) => c.runId || c.detailsUrl);
|
|
70
|
-
const hasRerunAuthorized = checks.some((c) => c.rerunCommand);
|
|
71
|
-
const instructions = [];
|
|
72
|
-
if (hasTriageable) {
|
|
73
|
-
instructions.push('Triage every failure under `## Failing checks`. See "CI failure triage" in the pr-shepherd skill for read-only inspection rules.');
|
|
74
|
-
}
|
|
75
|
-
if (hasRerunAuthorized) {
|
|
76
|
-
instructions.push('A `[rerun authorized]` check includes a `rerun:` command. See "CI failure triage" in the pr-shepherd skill for which conclusions warrant a rerun versus a code fix.');
|
|
77
|
-
}
|
|
78
|
-
if (hasBare) {
|
|
79
|
-
instructions.push("For each `(no runId)` failure, preserve the displayed metadata; Shepherd will escalate when no other autonomous work remains.");
|
|
80
|
-
}
|
|
81
|
-
return instructions;
|
|
71
|
+
return [`Triage \`## Failing checks\`. ${playbookPointer("CI failure triage")}`];
|
|
82
72
|
}
|
|
83
73
|
/** Update the PR branch after an external blocker merges or closes. Never rerun that job. */
|
|
84
74
|
export function buildReleasedBlockerInstruction(prNumber) {
|
|
85
75
|
return `Update this PR branch from its base with \`gh pr update-branch ${prNumber} --rebase\`. Do not rerun the job; a rerun retests the old merge ref.`;
|
|
86
76
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
: "push to the PR head branch";
|
|
91
|
-
if (hasConflicts)
|
|
92
|
-
return `\`[FIX_CODE]\` is non-terminal: resolve the conflicts, commit, ${push}, then iterate immediately with the same options.`;
|
|
93
|
-
if (hasShaGatedReviewMutations) {
|
|
94
|
-
return `\`[FIX_CODE]\` is non-terminal: if you changed code, commit and ${push}, then run the review mutations using the pushed commit SHA and iterate immediately with the same options; if you did not change code, complete the authorized review mutations and iterate immediately with the same options.`;
|
|
95
|
-
}
|
|
96
|
-
if (checks.some((check) => check.rerunCommand)) {
|
|
97
|
-
return "`[FIX_CODE]` is non-terminal. Run any warranted reruns for `[rerun authorized]` checks (or apply code fixes for real failures), then iterate immediately with the same options to continue.";
|
|
98
|
-
}
|
|
99
|
-
return "`[FIX_CODE]` is non-terminal. After completing these steps, iterate immediately with the same options to continue.";
|
|
77
|
+
/** Recurrence only. Commit, push, rerun, and SHA steps are earlier instructions. */
|
|
78
|
+
export function buildFixCompletionInstruction() {
|
|
79
|
+
return FIX_CODE_CONTINUATION;
|
|
100
80
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
2
|
+
import { renderRelatedJobLines } from "../../cli/related-jobs-format.mjs";
|
|
1
3
|
import { loadConfig } from "../../config/load.mjs";
|
|
2
4
|
import { renderResolveCommand } from "./render.mjs";
|
|
3
5
|
function renderEscalateAuthor(item) {
|
|
@@ -52,6 +54,7 @@ function renderEscalateCheck(check) {
|
|
|
52
54
|
lines.push(` > ${check.summary}`);
|
|
53
55
|
if (check.logExcerpt)
|
|
54
56
|
lines.push(...renderBlockquoteLines(check.logExcerpt, " "));
|
|
57
|
+
lines.push(...renderRelatedJobLines(check.relatedJobs));
|
|
55
58
|
if (check.rerunCommand)
|
|
56
59
|
lines.push(` rerun: \`${check.rerunCommand}\``);
|
|
57
60
|
for (const annotation of check.annotations ?? []) {
|
|
@@ -19,6 +19,10 @@ import { canRerunWorkflows } from "../../checks/conclusions.mjs";
|
|
|
19
19
|
import { loadConfig } from "../../config/load.mjs";
|
|
20
20
|
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
21
21
|
const EMPTY_RELEASED = new Set();
|
|
22
|
+
function hasLogEvidence(check) {
|
|
23
|
+
return (Boolean(check.logExcerpt?.trim()) ||
|
|
24
|
+
(check.relatedJobs ?? []).some((job) => Boolean(job.logExcerpt?.trim())));
|
|
25
|
+
}
|
|
22
26
|
function checkRequiresHumanFollowUp(check) {
|
|
23
27
|
if (check.rerunCommand)
|
|
24
28
|
return false;
|
|
@@ -30,13 +34,13 @@ function checkRequiresHumanFollowUp(check) {
|
|
|
30
34
|
// identify a code or configuration fix for the agent. Only a later failure with
|
|
31
35
|
// no actionable evidence needs a human handoff.
|
|
32
36
|
if (check.runAttempt !== undefined && check.runAttempt > 1)
|
|
33
|
-
return !check
|
|
37
|
+
return !hasLogEvidence(check);
|
|
34
38
|
// An external check's direct URL is actionable evidence: the agent can inspect the
|
|
35
39
|
// provider and/or reproduce the reported failure locally. Only a truly bare check
|
|
36
40
|
// has no autonomous investigation path.
|
|
37
41
|
if (check.runId === null)
|
|
38
42
|
return !check.detailsUrl?.trim();
|
|
39
|
-
return !check
|
|
43
|
+
return !hasLogEvidence(check);
|
|
40
44
|
}
|
|
41
45
|
function nextFixAttempts(stored, threads, countAttempt) {
|
|
42
46
|
const threadAttempts = stored ? { ...stored.threadAttempts } : {};
|
|
@@ -80,6 +80,7 @@ export function buildRelevantChecks(report) {
|
|
|
80
80
|
...(c.failedStep !== undefined && { failedStep: c.failedStep }),
|
|
81
81
|
...(c.summary !== undefined && { summary: c.summary }),
|
|
82
82
|
...(c.logExcerpt !== undefined && { logExcerpt: c.logExcerpt }),
|
|
83
|
+
...(c.relatedJobs !== undefined && { relatedJobs: c.relatedJobs }),
|
|
83
84
|
...(c.annotations !== undefined && { annotations: c.annotations }),
|
|
84
85
|
...(c.scope !== undefined && { scope: c.scope }),
|
|
85
86
|
...(c.commitOid !== undefined && { commitOid: c.commitOid }),
|
|
@@ -27,6 +27,7 @@ import { stackDraftHold } from "./parent-first.mjs";
|
|
|
27
27
|
import { findStaleNativeStackAncestry } from "./stale-ancestry.mjs";
|
|
28
28
|
import { annotateBlockedWait, resolveCheckBlockerGate } from "./check-blocker-gate.mjs";
|
|
29
29
|
import { buildUnreportedFixResult, planUnreportedRequired } from "./unreported-required.mjs";
|
|
30
|
+
import { createCheckExecutionContext } from "../check-execution-context.mjs";
|
|
30
31
|
export function runIterate(opts) {
|
|
31
32
|
return withIterateApiUsage(opts, () => runIterateCore(opts));
|
|
32
33
|
}
|
|
@@ -40,6 +41,7 @@ async function runIterateCore(opts) {
|
|
|
40
41
|
throw new ShepherdError("No open PR found for current branch. Pass a PR number explicitly.", EXIT.UNAVAILABLE);
|
|
41
42
|
}
|
|
42
43
|
const neverCancelRuns = opts.neverCancelRuns ?? config.actions.neverCancelRuns;
|
|
44
|
+
const checkContext = createCheckExecutionContext(readyDelaySeconds);
|
|
43
45
|
const report = await runCheck({
|
|
44
46
|
...opts,
|
|
45
47
|
prNumber,
|
|
@@ -48,7 +50,7 @@ async function runIterateCore(opts) {
|
|
|
48
50
|
// compatibility key in the config loader.
|
|
49
51
|
autoResolve: true,
|
|
50
52
|
autoMinimizeSuppressed: config.actions.autoMinimizeSuppressed,
|
|
51
|
-
});
|
|
53
|
+
}, checkContext);
|
|
52
54
|
const [repoOwner, repoName] = report.repo.split("/");
|
|
53
55
|
if (!repoOwner || !repoName) {
|
|
54
56
|
throw new ShepherdError(`Unexpected repo format: "${report.repo}" (expected "owner/name")`, EXIT.DATAERR);
|
|
@@ -84,6 +86,7 @@ async function runIterateCore(opts) {
|
|
|
84
86
|
// the apply command remains a working fallback instead of silently dropping it.
|
|
85
87
|
let reviewSummaryIds = minimizeIds;
|
|
86
88
|
if (selfMinimizeIds.length > 0) {
|
|
89
|
+
checkContext.invalidateReceiptSummary();
|
|
87
90
|
const { minimized } = await autoMinimizeComments(selfMinimizeIds);
|
|
88
91
|
const minimizedIds = new Set(minimized);
|
|
89
92
|
const unminimized = selfMinimizeIds.filter((id) => !minimizedIds.has(id));
|
|
@@ -110,13 +113,13 @@ async function runIterateCore(opts) {
|
|
|
110
113
|
const staleAncestry = await findStaleNativeStackAncestry(report, {
|
|
111
114
|
owner: repoOwner,
|
|
112
115
|
name: repoName,
|
|
113
|
-
});
|
|
116
|
+
}, checkContext);
|
|
114
117
|
const isCleanReadyState = report.status === "READY" &&
|
|
115
118
|
!report.mergeStatus.isDraft &&
|
|
116
119
|
!hasReadinessWork &&
|
|
117
120
|
!activeMerge &&
|
|
118
121
|
staleAncestry === null;
|
|
119
|
-
const receiptCurrent = await revalidateReadyReceipt(receiptKey, report, hasReadinessWork);
|
|
122
|
+
const receiptCurrent = await revalidateReadyReceipt(receiptKey, report, hasReadinessWork, checkContext);
|
|
120
123
|
const headSha = report.headSha ?? "unknown";
|
|
121
124
|
// The elapsed marker survives a hidden-comment acknowledgement tick and is
|
|
122
125
|
// consumed only when this tick cancels or merges. A receipt that is still
|
|
@@ -233,7 +236,7 @@ async function runIterateCore(opts) {
|
|
|
233
236
|
if (markReadyResult)
|
|
234
237
|
return markReadyResult;
|
|
235
238
|
if (readyState.shouldCancel && !report.mergeStatus.isDraft) {
|
|
236
|
-
const receiptWritten = receiptCurrent || (await recordReadyReceipt(receiptKey, report));
|
|
239
|
+
const receiptWritten = receiptCurrent || (await recordReadyReceipt(receiptKey, report, checkContext));
|
|
237
240
|
// Aggregate --stack routing trusts only a layer's receipt, so an unwritten
|
|
238
241
|
// one keeps the elapsed marker and retries next tick. A one-PR receipt
|
|
239
242
|
// only lets a rerun skip the wait, so its failure never changes the action.
|
|
@@ -279,7 +282,7 @@ async function runIterateCore(opts) {
|
|
|
279
282
|
};
|
|
280
283
|
return annotateBlockedWait(await applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, wait, reportForWork, reviewSummaryIds), blockerGate);
|
|
281
284
|
}
|
|
282
|
-
async function recordReadyReceipt(key, report) {
|
|
285
|
+
async function recordReadyReceipt(key, report, context) {
|
|
283
286
|
if (report.status !== "READY" ||
|
|
284
287
|
report.mergeStatus.state !== "OPEN" ||
|
|
285
288
|
report.mergeStatus.isDraft ||
|
|
@@ -287,7 +290,7 @@ async function recordReadyReceipt(key, report) {
|
|
|
287
290
|
!report.baseRefOid)
|
|
288
291
|
return false;
|
|
289
292
|
try {
|
|
290
|
-
const raw = await
|
|
293
|
+
const raw = await receiptSummaryForReport(key, report, context);
|
|
291
294
|
if (fingerprintRawSummaryPr(raw) === null ||
|
|
292
295
|
raw.state !== "OPEN" ||
|
|
293
296
|
raw.isDraft ||
|
|
@@ -327,7 +330,7 @@ async function recordReadyReceipt(key, report) {
|
|
|
327
330
|
}
|
|
328
331
|
}
|
|
329
332
|
/** Clear a stale READY receipt; return whether a current receipt remains. */
|
|
330
|
-
async function revalidateReadyReceipt(key, report, hasReadinessWork) {
|
|
333
|
+
async function revalidateReadyReceipt(key, report, hasReadinessWork, context) {
|
|
331
334
|
const receipt = await readReadyReceipt(key);
|
|
332
335
|
if (!receipt)
|
|
333
336
|
return false;
|
|
@@ -344,7 +347,7 @@ async function revalidateReadyReceipt(key, report, hasReadinessWork) {
|
|
|
344
347
|
return false;
|
|
345
348
|
}
|
|
346
349
|
try {
|
|
347
|
-
const raw = await
|
|
350
|
+
const raw = await receiptSummaryForReport(key, report, context);
|
|
348
351
|
const summary = await summarizePollSummaryPr(raw, { owner: key.owner, name: key.repo }, { stackPrNumber: report.pr });
|
|
349
352
|
if (annotationProbeUnavailable(raw)) {
|
|
350
353
|
const sameHead = raw.headRefOid === report.headSha;
|
|
@@ -385,3 +388,12 @@ async function revalidateReadyReceipt(key, report, hasReadinessWork) {
|
|
|
385
388
|
return false;
|
|
386
389
|
}
|
|
387
390
|
}
|
|
391
|
+
/** A mismatched or invalidated same-request hint falls back to the original fresh read. */
|
|
392
|
+
async function receiptSummaryForReport(key, report, context) {
|
|
393
|
+
const candidate = context?.getReceiptSummary();
|
|
394
|
+
if (candidate?.number === report.pr &&
|
|
395
|
+
candidate.headRefOid === report.headSha &&
|
|
396
|
+
candidate.baseRefOid === report.baseRefOid)
|
|
397
|
+
return candidate;
|
|
398
|
+
return fetchRawSummaryPr(report.pr, { owner: key.owner, name: key.repo });
|
|
399
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { playbookPointer } from "../playbook-pointer.mjs";
|
|
1
2
|
/**
|
|
2
3
|
* One gh-stack rebase step. A native stack layer must not be rebased or merged from its base
|
|
3
4
|
* branch alone: that rewrites one branch and strands every layer above it.
|
|
@@ -15,8 +16,8 @@ export function buildNativeStackRebaseInstruction(repo, stackNumber, start) {
|
|
|
15
16
|
: "bottomPr" in start
|
|
16
17
|
? [`check out the head branch of PR #${start.bottomPr}`, "gh stack rebase"]
|
|
17
18
|
: [`check out the bottom open layer whose base is \`${start.trunk}\``, "gh stack rebase"];
|
|
18
|
-
const prepare = `if \`gh stack\` does not track stack #${stackNumber} locally, import it with \`gh stack checkout ${stackNumber}
|
|
19
|
-
return `From a clean checkout of \`${repo}\`, ${prepare}. Then ${checkout} and run \`${command}
|
|
19
|
+
const prepare = `if \`gh stack\` does not track stack #${stackNumber} locally, import it with \`gh stack checkout ${stackNumber}\``;
|
|
20
|
+
return `From a clean checkout of \`${repo}\`, ${prepare}. Then ${checkout} and run \`${command}\`. ${playbookPointer("Branch update")}`;
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
23
|
* The stack-aware branch update for a native stack layer (a conflict, or a behind branch whose
|