pr-shepherd 0.54.1 → 0.55.1
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 +3 -3
- package/bin/checks/unreported-required.d.mts +29 -0
- package/bin/checks/unreported-required.mjs +52 -0
- package/bin/classify/apply.d.mts +2 -0
- package/bin/classify/apply.mjs +31 -31
- package/bin/classify/rule-action.d.mts +8 -0
- package/bin/classify/rule-action.mjs +29 -0
- package/bin/classify/types.d.mts +1 -1
- package/bin/cli/help-command-pages.d.mts +2 -2
- package/bin/cli/help-iterate-poll-pages.d.mts +2 -2
- package/bin/cli/help-iterate-poll-pages.mjs +6 -5
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +2 -2
- package/bin/cli/help.d.mts +3 -3
- package/bin/cli/iterate-formatter.mjs +32 -9
- package/bin/cli/iterate-instructions.mjs +6 -0
- package/bin/cli/iterate-lean.mjs +17 -0
- package/bin/cli/poll-summary-emitter.mjs +4 -1
- package/bin/cli/poll-summary-formatter.mjs +10 -3
- package/bin/cli/stack-overview.d.mts +48 -0
- package/bin/cli/stack-overview.mjs +138 -0
- package/bin/commands/check-fingerprint.mjs +2 -1
- package/bin/commands/check-status.d.mts +1 -1
- package/bin/commands/check-status.mjs +8 -3
- package/bin/commands/check-unreported.d.mts +28 -0
- package/bin/commands/check-unreported.mjs +101 -0
- package/bin/commands/check.mjs +45 -38
- package/bin/commands/commit-suggestion-instruction.d.mts +1 -1
- package/bin/commands/commit-suggestion-instruction.mjs +1 -1
- package/bin/commands/iterate/base.mjs +11 -0
- package/bin/commands/iterate/check-instructions.d.mts +1 -1
- package/bin/commands/iterate/check-instructions.mjs +1 -1
- package/bin/commands/iterate/escalate.mjs +4 -0
- package/bin/commands/iterate/index.mjs +69 -3
- package/bin/commands/iterate/merge-state.mjs +12 -12
- package/bin/commands/iterate/merge.d.mts +12 -2
- package/bin/commands/iterate/merge.mjs +32 -2
- package/bin/commands/iterate/unreported-required.d.mts +18 -0
- package/bin/commands/iterate/unreported-required.mjs +136 -0
- package/bin/commands/poll-summary.mjs +1 -0
- package/bin/commands/poll.mjs +9 -2
- package/bin/commands/ready-mergeability.d.mts +1 -1
- package/bin/commands/ready-mergeability.mjs +2 -2
- package/bin/commands/rule-auto-resolve-format.d.mts +22 -0
- package/bin/commands/rule-auto-resolve-format.mjs +151 -0
- package/bin/commands/rule-auto-resolve.d.mts +21 -0
- package/bin/commands/rule-auto-resolve.mjs +113 -0
- package/bin/commands/shepherd-journal.d.mts +1 -1
- package/bin/commands/shepherd-journal.mjs +1 -1
- package/bin/commands/stack-drain.d.mts +0 -5
- package/bin/commands/stack-drain.mjs +26 -16
- package/bin/commands/stack-merge-flag.d.mts +7 -0
- package/bin/commands/stack-merge-flag.mjs +6 -0
- package/bin/config/load.d.mts +3 -0
- package/bin/config/load.mjs +9 -2
- package/bin/config/merge-method.d.mts +27 -0
- package/bin/config/merge-method.mjs +56 -0
- package/bin/exit-codes.d.mts +1 -1
- package/bin/exit-codes.mjs +2 -1
- package/bin/github/batch-parse-suites.d.mts +3 -0
- package/bin/github/batch-parse-suites.mjs +15 -0
- package/bin/github/batch-parsers.d.mts +1 -1
- package/bin/github/batch-parsers.mjs +7 -0
- package/bin/github/batch-raw-types.d.mts +5 -0
- package/bin/github/batch.d.mts +3 -0
- package/bin/github/batch.mjs +9 -1
- package/bin/github/errors.d.mts +6 -0
- package/bin/github/errors.mjs +23 -5
- package/bin/github/fingerprint-fields.d.mts +1 -0
- package/bin/github/fingerprint-fields.mjs +1 -1
- package/bin/github/gql/base-behind.gql +20 -0
- package/bin/github/gql/batch-pr-page.gql +2 -0
- package/bin/github/gql/batch-pr.gql +5 -0
- package/bin/github/gql/commit-check-suites.gql +1 -0
- package/bin/github/gql/poll-stack-summary.gql +6 -0
- package/bin/github/gql/poll-summary-annotation-probe.gql +32 -0
- package/bin/github/gql/poll-summary-check-contexts.gql +2 -3
- package/bin/github/gql/poll-summary-fragment.gql +16 -0
- package/bin/github/gql/pr-merge-policy.gql +1 -44
- package/bin/github/gql/ref-rules-query.gql +18 -0
- package/bin/github/gql/ref-rules.gql +46 -0
- package/bin/github/graphql-internal-retry.d.mts +1 -1
- package/bin/github/graphql-internal-retry.mjs +4 -3
- package/bin/github/merge-queue-checks.mjs +7 -3
- package/bin/github/merge-target-rules.d.mts +28 -0
- package/bin/github/merge-target-rules.mjs +77 -0
- package/bin/github/poll-summary-annotation-probe.d.mts +19 -0
- package/bin/github/poll-summary-annotation-probe.mjs +88 -0
- package/bin/github/poll-summary-fingerprint.mjs +1 -1
- package/bin/github/poll-summary-projector.d.mts +1 -1
- package/bin/github/poll-summary-projector.mjs +19 -12
- package/bin/github/poll-summary-queue-removal.mjs +7 -3
- package/bin/github/poll-summary-raw.d.mts +16 -0
- package/bin/github/poll-summary-readiness.mjs +1 -0
- package/bin/github/poll-summary-route.mjs +5 -0
- package/bin/github/poll-summary-unreported.d.mts +6 -0
- package/bin/github/poll-summary-unreported.mjs +51 -0
- package/bin/github/poll-summary.d.mts +1 -0
- package/bin/github/poll-summary.mjs +28 -3
- package/bin/github/queries.d.mts +9 -0
- package/bin/github/queries.mjs +11 -2
- package/bin/github/queue-removal-freshness.d.mts +20 -5
- package/bin/github/queue-removal-freshness.mjs +37 -10
- package/bin/github/stack-read.d.mts +4 -0
- package/bin/github/stack-read.mjs +13 -1
- package/bin/mcp/server.mjs +6 -1
- package/bin/state/ci-retrigger.d.mts +25 -0
- package/bin/state/ci-retrigger.mjs +48 -0
- package/bin/types/escalate.d.mts +1 -1
- package/bin/types/github.d.mts +11 -0
- package/bin/types/iterate.d.mts +16 -3
- package/bin/types/poll-summary.d.mts +13 -0
- package/bin/types/report.d.mts +34 -1
- package/package.json +1 -1
- 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 -3
- package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +1 -1
- package/src/classify/types.mts +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { ruleAutoResolveFromReport } from "../rule-auto-resolve-format.mjs";
|
|
1
2
|
import { buildActiveChecks, buildRelevantChecks, buildSummary, buildSuppressedCheckFields, } from "./helpers.mjs";
|
|
2
3
|
export function buildIterateBase(report, readyState) {
|
|
4
|
+
const ruleAutoResolve = ruleAutoResolveFromReport(report);
|
|
3
5
|
return {
|
|
4
6
|
pr: report.pr,
|
|
5
7
|
repo: report.repo,
|
|
@@ -21,6 +23,15 @@ export function buildIterateBase(report, readyState) {
|
|
|
21
23
|
...buildSuppressedCheckFields(report),
|
|
22
24
|
activity: report.activity,
|
|
23
25
|
mergeQueue: report.mergeQueue,
|
|
26
|
+
...(report.unreportedRequiredChecks &&
|
|
27
|
+
report.unreportedRequiredChecks.length > 0 && {
|
|
28
|
+
unreportedRequiredChecks: report.unreportedRequiredChecks,
|
|
29
|
+
}),
|
|
30
|
+
...(report.trunkBehindBy !== undefined &&
|
|
31
|
+
report.trunkBehindBy > 0 && { trunkBehindBy: report.trunkBehindBy }),
|
|
32
|
+
...(report.baseBehindBy !== undefined &&
|
|
33
|
+
report.baseBehindBy > 0 && { baseBehindBy: report.baseBehindBy }),
|
|
24
34
|
...(report.fingerprintReused === true && { fingerprintReused: true }),
|
|
35
|
+
...(ruleAutoResolve && { ruleAutoResolve }),
|
|
25
36
|
};
|
|
26
37
|
}
|
|
@@ -4,7 +4,7 @@ export declare function buildCrStaleClause(reviews: Review[]): string;
|
|
|
4
4
|
/**
|
|
5
5
|
* Build the optional behind-base push hint. Empty unless the branch is actually behind its base
|
|
6
6
|
* and the user configured a non-blank `iterate.behindBaseHint` — the CLI never prescribes
|
|
7
|
-
* rebase/merge mechanics itself (see "Keep skills and loop prompts minimal" in
|
|
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
|
|
@@ -8,7 +8,7 @@ export function buildCrStaleClause(reviews) {
|
|
|
8
8
|
/**
|
|
9
9
|
* Build the optional behind-base push hint. Empty unless the branch is actually behind its base
|
|
10
10
|
* and the user configured a non-blank `iterate.behindBaseHint` — the CLI never prescribes
|
|
11
|
-
* rebase/merge mechanics itself (see "Keep skills and loop prompts minimal" in
|
|
11
|
+
* rebase/merge mechanics itself (see "Keep skills and loop prompts minimal" in AGENTS.md); this
|
|
12
12
|
* only echoes back the caller's own configured pointer. `hint` is trimmed and type-checked at the
|
|
13
13
|
* point of use (rather than at config load) so a malformed rc file value (non-string, or
|
|
14
14
|
* whitespace-only) degrades to "no hint" instead of rendering garbage into agent-facing text or
|
|
@@ -240,6 +240,10 @@ export function buildEscalateSuggestion(triggers, detail) {
|
|
|
240
240
|
const duration = detail ?? "60 minutes";
|
|
241
241
|
return `No progress detected for ${duration} — state has not changed. This is a manual checkpoint: inspect the PR and apply a manual fix before resuming.`;
|
|
242
242
|
}
|
|
243
|
+
if (triggers.includes("required-checks-unreported")) {
|
|
244
|
+
const names = detail ? ` (${detail})` : "";
|
|
245
|
+
return `Required checks are still unreported${names}, and no CI is running. Rebase and push is the fix when the branch is behind. This head is current, and another reopen will not create a job the workflow does not emit. Path filters are the usual cause. Investigate that, then handle the missing checks manually.`;
|
|
246
|
+
}
|
|
243
247
|
if (triggers.includes("stall-state-unavailable")) {
|
|
244
248
|
const reason = detail ?? "unknown error";
|
|
245
249
|
return `The stall timer could not be saved (${reason}). Fix PR_SHEPHERD_STATE_DIR or the directory permissions, then resume. Automated polling is paused because a stuck loop would not be detected.`;
|
|
@@ -18,6 +18,7 @@ import { markReadyIfAuthorized } from "./mark-ready.mjs";
|
|
|
18
18
|
import { withIterateApiUsage } from "./run.mjs";
|
|
19
19
|
import { fetchRawSummaryPr } from "../../github/poll-summary.mjs";
|
|
20
20
|
import { summarizePollSummaryPr } from "../../github/poll-summary-projector.mjs";
|
|
21
|
+
import { annotationProbeUnavailable } from "../../github/poll-summary-annotation-probe.mjs";
|
|
21
22
|
import { fingerprintRawSummaryPr } from "../../github/poll-summary-fingerprint.mjs";
|
|
22
23
|
import { currentQueueRemovalEvent } from "../../github/poll-summary-queue-removal.mjs";
|
|
23
24
|
import { isCurrentSummaryReady } from "../../github/poll-summary-readiness.mjs";
|
|
@@ -25,6 +26,7 @@ import { clearReadyReceipt, isReadyReceiptCurrent, readReadyReceipt, writeReadyR
|
|
|
25
26
|
import { stackDraftHold } from "./parent-first.mjs";
|
|
26
27
|
import { findStaleNativeStackAncestry } from "./stale-ancestry.mjs";
|
|
27
28
|
import { annotateBlockedWait, resolveCheckBlockerGate } from "./check-blocker-gate.mjs";
|
|
29
|
+
import { buildUnreportedFixResult, planUnreportedRequired } from "./unreported-required.mjs";
|
|
28
30
|
export function runIterate(opts) {
|
|
29
31
|
return withIterateApiUsage(opts, () => runIterateCore(opts));
|
|
30
32
|
}
|
|
@@ -122,6 +124,16 @@ async function runIterateCore(opts) {
|
|
|
122
124
|
// readiness evidence, so a rerun (e.g. with --merge) does not wait again.
|
|
123
125
|
const readyState = await updateReadyDelay(report.pr, isCleanReadyState, readyDelaySeconds, repoOwner, repoName, { headSha, alreadyElapsed: receiptCurrent });
|
|
124
126
|
const base = buildIterateBase(reportForWork, readyState);
|
|
127
|
+
const unreportedPlan = await planUnreportedRequired({
|
|
128
|
+
report,
|
|
129
|
+
base,
|
|
130
|
+
stateKey: stallKey,
|
|
131
|
+
headSha,
|
|
132
|
+
otherAutonomousWork: hasActionableWork || staleAncestry !== null,
|
|
133
|
+
});
|
|
134
|
+
if (unreportedPlan.escalate)
|
|
135
|
+
return unreportedPlan.escalate;
|
|
136
|
+
const unreportedRepair = unreportedPlan.repairInstructions ?? [];
|
|
125
137
|
// Checks (including merge-queue synthetic-commit checks) and hard conflicts are signals
|
|
126
138
|
// GitHub itself is already acting on — the queue will eject the PR for these regardless of
|
|
127
139
|
// what Shepherd does, so they always surface immediately. Only review threads/comments/
|
|
@@ -148,9 +160,36 @@ async function runIterateCore(opts) {
|
|
|
148
160
|
surfacedApprovals,
|
|
149
161
|
botUsernames,
|
|
150
162
|
releasedCheckNames: blockerGate?.releasedNames,
|
|
163
|
+
...((unreportedRepair.length > 0 || staleAncestry !== null) && {
|
|
164
|
+
repairInstructions: [...unreportedRepair, ...(staleAncestry?.instructions ?? [])],
|
|
165
|
+
}),
|
|
166
|
+
ruleAutoResolveThreadIds: report.threads.ruleAutoResolveIds,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (unreportedRepair.length > 0 && staleAncestry) {
|
|
170
|
+
return handleFixCode({
|
|
171
|
+
base,
|
|
172
|
+
report: reportForWork,
|
|
173
|
+
opts: { ...opts, prNumber, neverCancelRuns },
|
|
174
|
+
headSha,
|
|
175
|
+
stallKey,
|
|
176
|
+
prNumber,
|
|
177
|
+
stallTimeoutSeconds,
|
|
178
|
+
repoOwner,
|
|
179
|
+
repoName,
|
|
180
|
+
reviewSummaryIds,
|
|
181
|
+
firstLookSummaries,
|
|
182
|
+
editedSummaries,
|
|
183
|
+
surfacedApprovals,
|
|
184
|
+
botUsernames,
|
|
185
|
+
releasedCheckNames: blockerGate?.releasedNames,
|
|
186
|
+
repairInstructions: [...unreportedRepair, ...staleAncestry.instructions],
|
|
151
187
|
ruleAutoResolveThreadIds: report.threads.ruleAutoResolveIds,
|
|
152
188
|
});
|
|
153
189
|
}
|
|
190
|
+
if (unreportedRepair.length > 0) {
|
|
191
|
+
return annotateBlockedWait(await applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, buildUnreportedFixResult(base, report, unreportedRepair), reportForWork, reviewSummaryIds), blockerGate);
|
|
192
|
+
}
|
|
154
193
|
const mergeStateResult = await handleActiveMergeState({
|
|
155
194
|
enabled: opts.merge,
|
|
156
195
|
active: activeMerge,
|
|
@@ -182,7 +221,7 @@ async function runIterateCore(opts) {
|
|
|
182
221
|
surfacedApprovals,
|
|
183
222
|
botUsernames,
|
|
184
223
|
releasedCheckNames: blockerGate?.releasedNames,
|
|
185
|
-
repairInstructions: staleAncestry.instructions,
|
|
224
|
+
repairInstructions: [...unreportedRepair, ...staleAncestry.instructions],
|
|
186
225
|
ruleAutoResolveThreadIds: report.threads.ruleAutoResolveIds,
|
|
187
226
|
});
|
|
188
227
|
}
|
|
@@ -220,6 +259,17 @@ async function runIterateCore(opts) {
|
|
|
220
259
|
log: `CANCEL: PR #${base.pr} ${cancelNote} — ready-delay elapsed, stopping`,
|
|
221
260
|
};
|
|
222
261
|
}
|
|
262
|
+
if (isCleanReadyState &&
|
|
263
|
+
readyState.isReady &&
|
|
264
|
+
!readyState.shouldCancel &&
|
|
265
|
+
readyState.remainingSeconds > 0) {
|
|
266
|
+
await clearStallState(stallKey);
|
|
267
|
+
return {
|
|
268
|
+
...base,
|
|
269
|
+
action: "ready",
|
|
270
|
+
log: `READY: PR #${base.pr} is ready — ${readyState.remainingSeconds}s of ready-delay remaining — ${buildWaitLog(base).slice("WAIT: ".length)}`,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
223
273
|
const hold = stackDraftHold(report, autoMarkReady);
|
|
224
274
|
const wait = {
|
|
225
275
|
...base,
|
|
@@ -238,14 +288,19 @@ async function recordReadyReceipt(key, report) {
|
|
|
238
288
|
return false;
|
|
239
289
|
try {
|
|
240
290
|
const raw = await fetchRawSummaryPr(report.pr, { owner: key.owner, name: key.repo });
|
|
241
|
-
|
|
242
|
-
if (fingerprint === null ||
|
|
291
|
+
if (fingerprintRawSummaryPr(raw) === null ||
|
|
243
292
|
raw.state !== "OPEN" ||
|
|
244
293
|
raw.isDraft ||
|
|
245
294
|
raw.headRefOid !== report.headSha ||
|
|
246
295
|
raw.baseRefOid !== report.baseRefOid)
|
|
247
296
|
return false;
|
|
297
|
+
// Summarize hydrates annotation totals onto the snapshot before the stored hash.
|
|
248
298
|
const summary = await summarizePollSummaryPr(raw, { owner: key.owner, name: key.repo }, { stackPrNumber: report.pr });
|
|
299
|
+
if (annotationProbeUnavailable(raw))
|
|
300
|
+
return false;
|
|
301
|
+
const fingerprint = fingerprintRawSummaryPr(raw);
|
|
302
|
+
if (fingerprint === null)
|
|
303
|
+
return false;
|
|
249
304
|
if (!isCurrentSummaryReady(raw, summary.checks ?? {}, summary.review ?? {}))
|
|
250
305
|
return false;
|
|
251
306
|
const removalEvent = currentQueueRemovalEvent(raw);
|
|
@@ -290,6 +345,17 @@ async function revalidateReadyReceipt(key, report, hasReadinessWork) {
|
|
|
290
345
|
}
|
|
291
346
|
try {
|
|
292
347
|
const raw = await fetchRawSummaryPr(report.pr, { owner: key.owner, name: key.repo });
|
|
348
|
+
const summary = await summarizePollSummaryPr(raw, { owner: key.owner, name: key.repo }, { stackPrNumber: report.pr });
|
|
349
|
+
if (annotationProbeUnavailable(raw)) {
|
|
350
|
+
const sameHead = raw.headRefOid === report.headSha;
|
|
351
|
+
const sameBase = retainQueuedReceipt || raw.baseRefOid === report.baseRefOid;
|
|
352
|
+
const stillReady = isCurrentSummaryReady(raw, summary.checks ?? {}, summary.review ?? {});
|
|
353
|
+
if (!sameHead || !sameBase || !stillReady) {
|
|
354
|
+
await clearReadyReceipt(key);
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
293
359
|
// Queue predecessors can advance the target branch without changing this
|
|
294
360
|
// PR's source. Compare against the receipt's base only while both fresh
|
|
295
361
|
// views still place the PR in the queue.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { clearStallState } from "../../state/iterate-stall.mjs";
|
|
2
2
|
import { buildEscalateHumanMessage, buildEscalateSuggestion } from "./escalate.mjs";
|
|
3
|
-
import { buildMergeCommandPlan } from "./merge.mjs";
|
|
3
|
+
import { buildMergeCommandPlan, unavailableMergeResult } from "./merge.mjs";
|
|
4
4
|
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
5
5
|
import { buildPrShepherdCommand } from "../../cli/runner.mjs";
|
|
6
6
|
import { inlineCode } from "../../util/markdown.mjs";
|
|
@@ -49,17 +49,17 @@ export function buildReadyMergeOutcome(enabled, readyElapsed, base, report) {
|
|
|
49
49
|
return buildStackedRouteResult(base, report, stack);
|
|
50
50
|
const queue = Boolean(report.mergeStatus.mergeRequirements?.mergeQueue?.required ||
|
|
51
51
|
report.mergeStatus.mergeRequirements?.mergeQueue?.enabled);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
};
|
|
52
|
+
const plan = buildMergeCommandPlan({
|
|
53
|
+
pr: report.pr,
|
|
54
|
+
repo: report.repo,
|
|
55
|
+
nodeId: report.nodeId,
|
|
56
|
+
headSha: report.headSha ?? "unknown",
|
|
57
|
+
queue,
|
|
58
|
+
...(report.allowedMergeMethods && { allowedMergeMethods: report.allowedMergeMethods }),
|
|
59
|
+
});
|
|
60
|
+
if ("unavailable" in plan)
|
|
61
|
+
return unavailableMergeResult(base, report, plan.unavailable);
|
|
62
|
+
return { ...base, action: "merge", merge: plan };
|
|
63
63
|
}
|
|
64
64
|
/** Raw counts of non-CI actionable work held back for one queued-PR wait tick. Omitted (all zero) when empty. */
|
|
65
65
|
function buildDeferredWork(input) {
|
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type MergeMethod } from "../../config/merge-method.mts";
|
|
2
|
+
import type { IterateResult, IterateResultBase, MergeCommandPlan } from "../../types.mts";
|
|
2
3
|
interface MergePlanInput {
|
|
3
4
|
pr: number;
|
|
4
5
|
repo: string;
|
|
5
6
|
nodeId: string;
|
|
6
7
|
headSha: string;
|
|
7
8
|
queue: boolean;
|
|
9
|
+
/** Omitted when the batch did not select repository merge settings. */
|
|
10
|
+
allowedMergeMethods?: readonly MergeMethod[];
|
|
8
11
|
}
|
|
9
|
-
export
|
|
12
|
+
export interface MergeMethodUnavailable {
|
|
13
|
+
unavailable: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function buildMergeCommandPlan(input: MergePlanInput): MergeCommandPlan | MergeMethodUnavailable;
|
|
10
16
|
export declare function renderMergeCommand(command: {
|
|
11
17
|
argv: string[];
|
|
12
18
|
}): string;
|
|
19
|
+
export declare function unavailableMergeResult(base: IterateResultBase, report: {
|
|
20
|
+
pr: number;
|
|
21
|
+
repo: string;
|
|
22
|
+
}, unavailable: string): IterateResult;
|
|
13
23
|
export {};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { loadConfig } from "../../config/load.mjs";
|
|
2
2
|
import { findMergeStrategies } from "../../config/merge-command-args.mjs";
|
|
3
|
+
import { chooseMergeMethod, configuredMergeMethod, } from "../../config/merge-method.mjs";
|
|
4
|
+
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
3
5
|
import { renderShellCommand } from "../../cli/runner.mjs";
|
|
6
|
+
import { buildEscalateHumanMessage } from "./escalate.mjs";
|
|
4
7
|
const ENQUEUE_MUTATION = "mutation EnqueuePullRequest($pullRequestId: ID!, $expectedHeadOid: GitObjectID!) { enqueuePullRequest(input: { pullRequestId: $pullRequestId, expectedHeadOid: $expectedHeadOid }) { mergeQueueEntry { id } } }";
|
|
5
8
|
export function buildMergeCommandPlan(input) {
|
|
6
9
|
const base = [
|
|
@@ -32,9 +35,17 @@ export function buildMergeCommandPlan(input) {
|
|
|
32
35
|
},
|
|
33
36
|
};
|
|
34
37
|
}
|
|
35
|
-
const
|
|
38
|
+
const mergeConfig = loadConfig().merge;
|
|
39
|
+
const configuredArgs = mergeConfig?.commandArgs ?? [];
|
|
40
|
+
const decision = chooseMergeMethod({
|
|
41
|
+
allowed: input.allowedMergeMethods,
|
|
42
|
+
configured: configuredMergeMethod(mergeConfig ?? {}),
|
|
43
|
+
fallback: "merge",
|
|
44
|
+
});
|
|
45
|
+
if ("unavailable" in decision)
|
|
46
|
+
return decision;
|
|
36
47
|
const hasStrategy = findMergeStrategies(configuredArgs).length > 0;
|
|
37
|
-
const commandArgs = hasStrategy ? configuredArgs : [...configuredArgs,
|
|
48
|
+
const commandArgs = hasStrategy ? configuredArgs : [...configuredArgs, `--${decision.method}`];
|
|
38
49
|
return {
|
|
39
50
|
mode: "auto",
|
|
40
51
|
command: { argv: [...base, "--auto", ...commandArgs] },
|
|
@@ -44,3 +55,22 @@ export function buildMergeCommandPlan(input) {
|
|
|
44
55
|
export function renderMergeCommand(command) {
|
|
45
56
|
return renderShellCommand(command.argv);
|
|
46
57
|
}
|
|
58
|
+
export function unavailableMergeResult(base, report, unavailable) {
|
|
59
|
+
const escalateBase = {
|
|
60
|
+
triggers: ["merge-method-unavailable"],
|
|
61
|
+
unresolvedThreads: [],
|
|
62
|
+
ambiguousComments: [],
|
|
63
|
+
changesRequestedReviews: [],
|
|
64
|
+
suggestion: unavailable,
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
...base,
|
|
68
|
+
action: "escalate",
|
|
69
|
+
escalate: {
|
|
70
|
+
...escalateBase,
|
|
71
|
+
humanMessage: buildEscalateHumanMessage(escalateBase, formatPrUrl(report.repo, report.pr), {
|
|
72
|
+
merge: true,
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IterateResult, IterateResultBase, ShepherdReport } from "../../types.mts";
|
|
2
|
+
interface UnreportedPlan {
|
|
3
|
+
escalate?: IterateResult;
|
|
4
|
+
repairInstructions?: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function planUnreportedRequired(input: {
|
|
7
|
+
report: ShepherdReport;
|
|
8
|
+
base: IterateResultBase;
|
|
9
|
+
stateKey: {
|
|
10
|
+
owner: string;
|
|
11
|
+
repo: string;
|
|
12
|
+
pr: number;
|
|
13
|
+
};
|
|
14
|
+
headSha: string;
|
|
15
|
+
otherAutonomousWork: boolean;
|
|
16
|
+
}): Promise<UnreportedPlan>;
|
|
17
|
+
export declare function buildUnreportedFixResult(base: IterateResultBase, report: ShepherdReport, instructions: string[]): IterateResult;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { buildPrShepherdCommand } from "../../cli/runner.mjs";
|
|
2
|
+
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
3
|
+
import { readCiRetrigger, sameCiRetrigger, writeCiRetrigger } from "../../state/ci-retrigger.mjs";
|
|
4
|
+
import { buildFixCompletionInstruction } from "./check-instructions.mjs";
|
|
5
|
+
import { buildEscalateHumanMessage, buildEscalateSuggestion } from "./escalate.mjs";
|
|
6
|
+
import { buildNativeStackRebaseInstruction } from "./native-stack-rebase.mjs";
|
|
7
|
+
/** Rebase when the trunk or the PR base is behind; otherwise close and reopen once. */
|
|
8
|
+
function decideUnreportedRequired(input) {
|
|
9
|
+
if (input.behind)
|
|
10
|
+
return "rebase";
|
|
11
|
+
if (input.alreadyRetriggered)
|
|
12
|
+
return input.otherAutonomousWork ? "none" : "escalate";
|
|
13
|
+
return "reopen";
|
|
14
|
+
}
|
|
15
|
+
function listedChecks(names) {
|
|
16
|
+
return names.map((name) => `\`${name}\``).join(", ");
|
|
17
|
+
}
|
|
18
|
+
/** Facts, then rebase-and-push, then investigate or escalate if that push does not start CI. */
|
|
19
|
+
function buildUnreportedRequiredInstructions(input) {
|
|
20
|
+
const names = listedChecks(input.names);
|
|
21
|
+
const gap = input.behindBy > 0 && input.trunk
|
|
22
|
+
? `The stack trunk is behind by ${input.behindBy} commits.`
|
|
23
|
+
: input.behindBy > 0
|
|
24
|
+
? `This branch is behind \`${input.baseBranch}\` by ${input.behindBy} commits.`
|
|
25
|
+
: input.behind
|
|
26
|
+
? "This PR's derived merge status is BEHIND."
|
|
27
|
+
: `This branch is not behind \`${input.baseBranch}\`.`;
|
|
28
|
+
const facts = `${gap} No CI checks are running, and required checks have not passed: ${names}.`;
|
|
29
|
+
if (!input.behind) {
|
|
30
|
+
return [
|
|
31
|
+
facts,
|
|
32
|
+
`Retrigger workflows once for this head with \`gh pr close ${input.pr} -R ${input.repo}\` then \`gh pr reopen ${input.pr} -R ${input.repo}\`.`,
|
|
33
|
+
"If those checks are still missing on the next poll, investigate why the workflows did not start. Do not close the PR again. Shepherd escalates with `required-checks-unreported` when this remains the only blocker.",
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
const rebase = input.stackRebase
|
|
37
|
+
? `${input.stackRebase} Then push the rewritten stack with \`gh stack push\`.`
|
|
38
|
+
: `Rebase onto \`${input.baseBranch}\` and push.`;
|
|
39
|
+
return [
|
|
40
|
+
facts,
|
|
41
|
+
`${rebase} Rebase and push is how these checks start.`,
|
|
42
|
+
"If that push does not start the checks, investigate why the workflows did not run. Shepherd escalates with `required-checks-unreported` when the branch is current and this remains the only blocker.",
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
export async function planUnreportedRequired(input) {
|
|
46
|
+
const names = input.report.unreportedRequiredChecks ?? [];
|
|
47
|
+
const actionsRunning = input.report.actionsWorkflowInProgress === true || input.report.checks.inProgress.length > 0;
|
|
48
|
+
const failing = input.report.checks.failing.length > 0;
|
|
49
|
+
const conflicts = input.report.mergeStatus.status === "CONFLICTS";
|
|
50
|
+
const inQueue = input.report.mergeQueue?.inQueue === true;
|
|
51
|
+
if (names.length === 0 || actionsRunning || failing || conflicts || inQueue)
|
|
52
|
+
return {};
|
|
53
|
+
const behindBy = input.report.trunkBehindBy ?? input.report.baseBehindBy ?? 0;
|
|
54
|
+
const behind = behindBy > 0 || input.report.mergeStatus.status === "BEHIND";
|
|
55
|
+
const decision = decideUnreportedRequired({
|
|
56
|
+
behind,
|
|
57
|
+
alreadyRetriggered: sameCiRetrigger(await readCiRetrigger(input.stateKey), input.headSha, names),
|
|
58
|
+
otherAutonomousWork: input.otherAutonomousWork,
|
|
59
|
+
});
|
|
60
|
+
if (decision === "none")
|
|
61
|
+
return {};
|
|
62
|
+
if (decision === "escalate")
|
|
63
|
+
return { escalate: escalateUnreported(input.base, input.report, names) };
|
|
64
|
+
const rebase = stackRebase(input.report);
|
|
65
|
+
const instructions = buildUnreportedRequiredInstructions({
|
|
66
|
+
names,
|
|
67
|
+
repo: input.report.repo,
|
|
68
|
+
pr: input.report.pr,
|
|
69
|
+
baseBranch: input.report.baseBranch,
|
|
70
|
+
behind,
|
|
71
|
+
behindBy,
|
|
72
|
+
trunk: (input.report.trunkBehindBy ?? 0) > 0,
|
|
73
|
+
...(rebase && { stackRebase: rebase }),
|
|
74
|
+
});
|
|
75
|
+
if (decision === "reopen") {
|
|
76
|
+
await writeCiRetrigger(input.stateKey, { headSha: input.headSha, contexts: names });
|
|
77
|
+
}
|
|
78
|
+
return { repairInstructions: instructions };
|
|
79
|
+
}
|
|
80
|
+
export function buildUnreportedFixResult(base, report, instructions) {
|
|
81
|
+
const prUrl = formatPrUrl(report.repo, report.pr);
|
|
82
|
+
return {
|
|
83
|
+
...base,
|
|
84
|
+
action: "fix_code",
|
|
85
|
+
fix: {
|
|
86
|
+
threads: [],
|
|
87
|
+
resolutionOnlyThreads: [],
|
|
88
|
+
actionableComments: [],
|
|
89
|
+
reviewSummaryIds: [],
|
|
90
|
+
firstLookSummaries: [],
|
|
91
|
+
editedSummaries: [],
|
|
92
|
+
surfacedApprovals: [],
|
|
93
|
+
checks: [],
|
|
94
|
+
changesRequestedReviews: [],
|
|
95
|
+
resolveCommand: {
|
|
96
|
+
argv: buildPrShepherdCommand(["apply", "review", prUrl]).argv,
|
|
97
|
+
requiresHeadSha: false,
|
|
98
|
+
requiresDismissMessage: false,
|
|
99
|
+
hasMutations: false,
|
|
100
|
+
},
|
|
101
|
+
instructions: [...instructions, buildFixCompletionInstruction([])],
|
|
102
|
+
inProgressRunIds: [],
|
|
103
|
+
protectedRuns: [],
|
|
104
|
+
firstLookThreads: [],
|
|
105
|
+
firstLookComments: [],
|
|
106
|
+
},
|
|
107
|
+
cancelled: [],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function stackRebase(report) {
|
|
111
|
+
const stack = report.mergeStatus.mergeRequirements?.stack;
|
|
112
|
+
if (!stack)
|
|
113
|
+
return undefined;
|
|
114
|
+
const start = report.stackBottomPr !== undefined
|
|
115
|
+
? { bottomPr: report.stackBottomPr }
|
|
116
|
+
: { trunk: stack.baseRefName };
|
|
117
|
+
return buildNativeStackRebaseInstruction(report.repo, stack.number, start);
|
|
118
|
+
}
|
|
119
|
+
function escalateUnreported(base, report, names) {
|
|
120
|
+
const detail = names.map((name) => `\`${name}\``).join(", ");
|
|
121
|
+
const escalateBase = {
|
|
122
|
+
triggers: ["required-checks-unreported"],
|
|
123
|
+
unresolvedThreads: [],
|
|
124
|
+
ambiguousComments: [],
|
|
125
|
+
changesRequestedReviews: [],
|
|
126
|
+
suggestion: buildEscalateSuggestion(["required-checks-unreported"], detail),
|
|
127
|
+
};
|
|
128
|
+
return {
|
|
129
|
+
...base,
|
|
130
|
+
action: "escalate",
|
|
131
|
+
escalate: {
|
|
132
|
+
...escalateBase,
|
|
133
|
+
humanMessage: buildEscalateHumanMessage(escalateBase, report.pr),
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -31,6 +31,7 @@ async function runPollSummaryCore(opts) {
|
|
|
31
31
|
reason: allTerminal ? "all_terminal" : actionable ? "actionable" : "waiting",
|
|
32
32
|
prs: fetched.prs,
|
|
33
33
|
...(fetched.stackAncestry?.length && { stackAncestry: fetched.stackAncestry }),
|
|
34
|
+
...(fetched.allowedMergeMethods && { allowedMergeMethods: fetched.allowedMergeMethods }),
|
|
34
35
|
}, opts.merge === true);
|
|
35
36
|
return applyStackStallGuard(planned, repo, opts.stallTimeoutSeconds ?? loadConfig().iterate.stallTimeoutMinutes * 60);
|
|
36
37
|
}
|
package/bin/commands/poll.mjs
CHANGED
|
@@ -6,6 +6,10 @@ import { quotaPollIntervalMs } from "./poll-quota.mjs";
|
|
|
6
6
|
import { onePrCancelFromPulls, onePrRateLimitTargets } from "./poll-rate-limit-cancel.mjs";
|
|
7
7
|
import { createUntilTerminalRateLimitRetry } from "./poll-rate-limit-wait.mjs";
|
|
8
8
|
import { writeDebounceProgress, writeWaitProgress } from "./poll-progress.mjs";
|
|
9
|
+
function noteRuleAutoResolve(result) {
|
|
10
|
+
if (result.ruleAutoResolve?.summary)
|
|
11
|
+
process.stderr.write(`${result.ruleAutoResolve.summary}\n`);
|
|
12
|
+
}
|
|
9
13
|
const DEFAULT_POLL_DEBOUNCE_SECONDS = 60;
|
|
10
14
|
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
11
15
|
const TIMER_DRIFT_TOLERANCE_MS = 500;
|
|
@@ -38,8 +42,7 @@ async function runPollCore(opts) {
|
|
|
38
42
|
const remainingBefore = untilTerminal
|
|
39
43
|
? Number.POSITIVE_INFINITY
|
|
40
44
|
: timeoutMs - (Date.now() - start);
|
|
41
|
-
// Cache only internal continuation ticks
|
|
42
|
-
// and any tick we return to the caller must fetch BatchPr.
|
|
45
|
+
// Cache only internal continuation ticks; returned ticks must fetch BatchPr.
|
|
43
46
|
const allowCache = debounceUntil === null && remainingBefore + TIMER_DRIFT_TOLERANCE_MS >= intervalMs;
|
|
44
47
|
const iterateTick = (fingerprintCache) => runIterate({
|
|
45
48
|
...iterateOpts,
|
|
@@ -89,6 +92,7 @@ async function runPollCore(opts) {
|
|
|
89
92
|
if (lastResult.action === "fix_code" && debounceSeconds > 0 && !pastDebounce) {
|
|
90
93
|
debounceUntil ??= Date.now() + debounceMs;
|
|
91
94
|
const remainingMs = Math.max(debounceUntil - Date.now(), 0);
|
|
95
|
+
noteRuleAutoResolve(lastResult);
|
|
92
96
|
writeDebounceProgress(tick, Date.now() - start, remainingMs);
|
|
93
97
|
await sleep(Math.min(intervalMs, remainingMs));
|
|
94
98
|
continue;
|
|
@@ -123,6 +127,7 @@ async function runPollCore(opts) {
|
|
|
123
127
|
verbose,
|
|
124
128
|
lastWaitSignature,
|
|
125
129
|
});
|
|
130
|
+
noteRuleAutoResolve(lastResult);
|
|
126
131
|
await sleep(sleepMs);
|
|
127
132
|
continue;
|
|
128
133
|
}
|
|
@@ -139,10 +144,12 @@ async function runPollCore(opts) {
|
|
|
139
144
|
break;
|
|
140
145
|
}
|
|
141
146
|
}
|
|
147
|
+
noteRuleAutoResolve(lastResult);
|
|
142
148
|
await sleep(sleepMs);
|
|
143
149
|
continue;
|
|
144
150
|
}
|
|
145
151
|
if (lastResult.action === "fix_code" && debounceSeconds > 0 && !pastDebounce) {
|
|
152
|
+
noteRuleAutoResolve(lastResult);
|
|
146
153
|
debounceUntil ??= Date.now() + debounceMs;
|
|
147
154
|
const remainingMs = debounceUntil - Date.now();
|
|
148
155
|
if (remainingMs > 0) {
|
|
@@ -10,6 +10,6 @@ export declare function refreshUnknownMergeability(prNumber: number, repo: RepoI
|
|
|
10
10
|
batchData: BatchPrData;
|
|
11
11
|
didRefresh: boolean;
|
|
12
12
|
}>;
|
|
13
|
-
export declare function refreshReadyMergeability(prNumber: number, repo: RepoInfo, batchData: BatchPrData, verdict: CiVerdict, unresolvedThreads: number, unresolvedComments: number, changesRequestedCount: number): Promise<ReadyMergeabilityRefresh>;
|
|
13
|
+
export declare function refreshReadyMergeability(prNumber: number, repo: RepoInfo, batchData: BatchPrData, verdict: CiVerdict, unresolvedThreads: number, unresolvedComments: number, changesRequestedCount: number, hasUnreportedRequired?: boolean): Promise<ReadyMergeabilityRefresh>;
|
|
14
14
|
export declare function isBlockedByFilteredCheck(mergeStatus: MergeStatusResult, verdict: CiVerdict): boolean;
|
|
15
15
|
export {};
|
|
@@ -8,10 +8,10 @@ export async function refreshUnknownMergeability(prNumber, repo, batchData) {
|
|
|
8
8
|
}
|
|
9
9
|
return { batchData: await readMergeability(prNumber, repo, batchData), didRefresh: true };
|
|
10
10
|
}
|
|
11
|
-
export async function refreshReadyMergeability(prNumber, repo, batchData, verdict, unresolvedThreads, unresolvedComments, changesRequestedCount) {
|
|
11
|
+
export async function refreshReadyMergeability(prNumber, repo, batchData, verdict, unresolvedThreads, unresolvedComments, changesRequestedCount, hasUnreportedRequired = false) {
|
|
12
12
|
const refreshedBatchData = await readMergeability(prNumber, repo, batchData);
|
|
13
13
|
const mergeStatus = deriveMergeStatus(refreshedBatchData);
|
|
14
|
-
const status = computeStatus(verdict, unresolvedThreads, unresolvedComments, mergeStatus, changesRequestedCount);
|
|
14
|
+
const status = computeStatus(verdict, unresolvedThreads, unresolvedComments, mergeStatus, changesRequestedCount, hasUnreportedRequired);
|
|
15
15
|
return { batchData: refreshedBatchData, mergeStatus, status };
|
|
16
16
|
}
|
|
17
17
|
export function isBlockedByFilteredCheck(mergeStatus, verdict) {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AutoMinimizedItem, AutoResolvedThread, RuleAutoResolveReport, ShepherdReport } from "../types.mts";
|
|
2
|
+
export declare function uniqueReasons(reasons: readonly string[]): string[];
|
|
3
|
+
export declare function reasonClause(reasons: readonly string[]): string | undefined;
|
|
4
|
+
export declare function ruleAutoResolveBody(counts: {
|
|
5
|
+
threads: number;
|
|
6
|
+
comments: number;
|
|
7
|
+
reviewSummaries: number;
|
|
8
|
+
}): string | undefined;
|
|
9
|
+
export declare function formatRuleAutoResolveJournalItem(input: {
|
|
10
|
+
body: string;
|
|
11
|
+
reasons: readonly string[];
|
|
12
|
+
viewerLogin?: string;
|
|
13
|
+
urls: readonly string[];
|
|
14
|
+
}): string;
|
|
15
|
+
export declare function decorateAutoResolveError(error: string, reasons: readonly string[]): string;
|
|
16
|
+
export declare function reasonsForError(error: string, ids: readonly string[], ruleReasons: ReadonlyMap<string, readonly string[]>): string[];
|
|
17
|
+
export declare function ruleAutoResolveFromReport(report: ShepherdReport): RuleAutoResolveReport | undefined;
|
|
18
|
+
export declare function projectRuleAutoResolve(event: RuleAutoResolveReport): Record<string, unknown>;
|
|
19
|
+
export declare function insertRuleAutoResolveSection(text: string, event: RuleAutoResolveReport | undefined): string;
|
|
20
|
+
export declare function stripReplayedRuleAutoResolve(report: ShepherdReport): ShepherdReport;
|
|
21
|
+
export declare function minimizedItem(kind: AutoMinimizedItem["kind"], id: string, url: string | undefined, reasons: readonly string[]): AutoMinimizedItem;
|
|
22
|
+
export declare function resolvedThread(thread: AutoResolvedThread, reasons: readonly string[]): AutoResolvedThread;
|