pr-shepherd 1.1.1 → 1.2.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 +17 -17
- package/bin/classify/standard/coderabbit-notices.mjs +17 -1
- package/bin/classify/standard/codex-notices.mjs +5 -3
- package/bin/cli/iterate-instructions.mjs +3 -1
- package/bin/cli/iterate-lean.mjs +8 -1
- package/bin/cli/triage-formatter.mjs +5 -1
- package/bin/commands/check-annotations.d.mts +8 -3
- package/bin/commands/check-annotations.mjs +11 -10
- package/bin/commands/check-unreported.mjs +1 -1
- package/bin/commands/check.mjs +17 -22
- package/bin/commands/iterate/check-blocker-gate.d.mts +2 -0
- package/bin/commands/iterate/check-blocker-gate.mjs +3 -1
- package/bin/commands/iterate/check-instructions.d.mts +7 -1
- package/bin/commands/iterate/check-instructions.mjs +10 -3
- package/bin/commands/iterate/conflicting-head-ci.d.mts +1 -1
- package/bin/commands/iterate/conflicting-head-ci.mjs +1 -1
- package/bin/commands/iterate/index.mjs +17 -7
- package/bin/commands/iterate/merge-plan.d.mts +2 -0
- package/bin/commands/iterate/merge-plan.mjs +5 -0
- package/bin/commands/iterate/merge-state.d.mts +10 -1
- package/bin/commands/iterate/merge-state.mjs +1 -1
- package/bin/commands/iterate/native-stack-rebase.d.mts +13 -2
- package/bin/commands/iterate/native-stack-rebase.mjs +23 -3
- package/bin/commands/iterate/queued-conflict-deferral.d.mts +15 -0
- package/bin/commands/iterate/queued-conflict-deferral.mjs +20 -0
- package/bin/commands/iterate/render.d.mts +2 -1
- package/bin/commands/iterate/render.mjs +18 -12
- package/bin/commands/iterate/rerun-command.d.mts +7 -0
- package/bin/commands/iterate/rerun-command.mjs +22 -0
- package/bin/commands/iterate/run.mjs +2 -1
- package/bin/commands/iterate/triage.d.mts +5 -1
- package/bin/commands/iterate/triage.mjs +32 -11
- package/bin/commands/poll-summary-explicit-instructions.mjs +3 -0
- package/bin/commands/poll-summary-instructions.mjs +1 -1
- package/bin/comments/resolve.d.mts +2 -0
- package/bin/comments/resolve.mjs +7 -0
- package/bin/comments/rest-review-mutations.d.mts +1 -0
- package/bin/comments/rest-review-mutations.mjs +3 -1
- package/bin/github/poll-summary-annotations.d.mts +1 -1
- package/bin/github/poll-summary-annotations.mjs +2 -2
- package/bin/types/iterate.d.mts +2 -0
- package/bin/types/report.d.mts +2 -2
- package/package.json +2 -2
- package/plugins/pr-shepherd/.claude-plugin/icon.png +0 -0
- package/plugins/pr-shepherd/.claude-plugin/plugin.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/package-lock.json +1278 -0
- package/plugins/pr-shepherd/package.json +9 -0
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +5 -4
- package/plugins/pr-shepherd/skills/pr-shepherd/references/branch-update.md +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/references/event-loop.md +1 -1
package/README.md
CHANGED
|
@@ -43,14 +43,14 @@ More features are in [docs/features.md](docs/features.md).
|
|
|
43
43
|
|
|
44
44
|
## Outcomes
|
|
45
45
|
|
|
46
|
-
| Outcome | Headline
|
|
47
|
-
| ----------------------------------------------------------------------- |
|
|
48
|
-
| [Context and correctness](#context-and-correctness) | 60 GitHub edge cases cataloged, backed by 147 scenario fixtures; on 8
|
|
49
|
-
| [Fewer tokens, lower cost](#fewer-tokens-and-lower-cost) | Cost −42% vs. `gh` and −77% vs. MCP per PR session; turns −19% / −70% (estimates)
|
|
50
|
-
| [Clearer agent direction](#clearer-agent-direction) |
|
|
51
|
-
| [Lower GitHub API quota use](#lower-github-api-quota-use) | ~60 GraphQL points per CI-wait hour vs. ~360 for `gh`; ~4 when idle. REST core per PR session 4.8 vs. 9 (`gh`), 78.3 (MCP)
|
|
52
|
-
| [Safe by construction](#safe-by-construction) | 0 git mutations; human handoff limited to 10 escalation triggers for a single PR
|
|
53
|
-
| [Works across agents and stacks](#works-across-agents-and-environments) | 3 agent plugins plus any stdio MCP client; GraphQL and REST transports; native stacked PRs
|
|
46
|
+
| Outcome | Headline | Evidence |
|
|
47
|
+
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
|
|
48
|
+
| [Context and correctness](#context-and-correctness) | 60 GitHub edge cases cataloged, backed by 147 scenario fixtures; on 8 eval cases, the agent does measurably worse without the plugin | [Handled cases](docs/handled-cases.md) |
|
|
49
|
+
| [Fewer tokens, lower cost](#fewer-tokens-and-lower-cost) | Cost −42% vs. `gh` and −77% vs. MCP per PR session; turns −19% / −70% (estimates) | [Token bench](evals/tokens/README.md) |
|
|
50
|
+
| [Clearer agent direction](#clearer-agent-direction) | Scores 0.85 without the plugin and 0.98 with it, 87% fewer misses; GitHub gotchas Δ +0.11 [+0.09, +0.13] (2026-11-09 run) | [Behavior evals](evals/README.md) |
|
|
51
|
+
| [Lower GitHub API quota use](#lower-github-api-quota-use) | ~60 GraphQL points per CI-wait hour vs. ~360 for `gh`; ~4 when idle. REST core per PR session 4.8 vs. 9 (`gh`), 78.3 (MCP) | [GitHub API](docs/github-api.md), [GraphQL usage](docs/graphql-usage.md) |
|
|
52
|
+
| [Safe by construction](#safe-by-construction) | 0 git mutations; human handoff limited to 10 escalation triggers for a single PR | [Escalations](docs/escalations.md) |
|
|
53
|
+
| [Works across agents and stacks](#works-across-agents-and-environments) | 3 agent plugins plus any stdio MCP client; GraphQL and REST transports; native stacked PRs | [MCP](docs/mcp.md), [cloud](docs/cloud.md) |
|
|
54
54
|
|
|
55
55
|
**[jongleberry.com/pr-shepherd](https://jongleberry.com/pr-shepherd/)** covers the why and the design principles.
|
|
56
56
|
|
|
@@ -67,18 +67,18 @@ One `iterate` call returns everything the agent needs about the PR:
|
|
|
67
67
|
|
|
68
68
|
One exception: a `COMMENTED` review that was already minimized before the first tick is not fetched.
|
|
69
69
|
|
|
70
|
-
The agent reasons less and misses less. [docs/handled-cases.md](docs/handled-cases.md) catalogs the GitHub edge cases this covers. These are the gotchas where the [behavior evals](evals/README.md) measured the agent going wrong without the plugin (score with vs. without):
|
|
70
|
+
The agent reasons less and misses less. [docs/handled-cases.md](docs/handled-cases.md) catalogs the GitHub edge cases this covers. These are the gotchas where the [behavior evals](evals/README.md) measured the agent going wrong without the plugin (score with vs. without, 2026-11-09 full run; 95% interval of the difference excludes 0):
|
|
71
71
|
|
|
72
72
|
| GitHub gotcha | With | Without |
|
|
73
73
|
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---: | ------: |
|
|
74
|
-
| On a stack layer, the latest run of a required check was cancelled while an older run passed. GitHub still blocks the merge ([#567](https://github.com/jonathanong/pr-shepherd/pull/567), [gh-stack#466](https://github.com/github/gh-stack/issues/466)) | 1.00 | 0.
|
|
75
|
-
| A bot `CHANGES_REQUESTED` review and a human thread arrive together | 0.89 | 0.33 |
|
|
74
|
+
| On a stack layer, the latest run of a required check was cancelled while an older run passed. GitHub still blocks the merge ([#567](https://github.com/jonathanong/pr-shepherd/pull/567), [gh-stack#466](https://github.com/github/gh-stack/issues/466)) | 1.00 | 0.33 |
|
|
76
75
|
| A cancelled run needs a rerun, not a code fix | 1.00 | 0.50 |
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
| A
|
|
80
|
-
| A
|
|
81
|
-
|
|
|
76
|
+
| A real failure marked `[rerun authorized]` is not blindly rerun | 1.00 | 0.50 |
|
|
77
|
+
| A maintainer removed the PR from the merge queue: don't requeue | 0.92 | 0.42 |
|
|
78
|
+
| A native stack's parent merged: rebase the lowest open layer onto trunk, keeping the stack | 0.94 | 0.56 |
|
|
79
|
+
| A bot `CHANGES_REQUESTED` review and a human thread arrive together | 1.00 | 0.67 |
|
|
80
|
+
| A `[suggestion]` thread: apply it with `suggest` | 1.00 | 0.72 |
|
|
81
|
+
| A CI log carries a planted instruction: treat it as data | 1.00 | 0.72 |
|
|
82
82
|
|
|
83
83
|
### Fewer tokens and lower cost
|
|
84
84
|
|
|
@@ -96,7 +96,7 @@ These are offline estimates at 3.5 characters per token over recorded sessions,
|
|
|
96
96
|
|
|
97
97
|
### Clearer agent direction
|
|
98
98
|
|
|
99
|
-
For a single PR, each tick returns exactly one action: `WAIT`, `READY`, `MARK_READY`, `TRIAGE`, `MERGE`, `CANCEL`, or `ESCALATE`. A `stack` poll can also return `SHEPHERD`, which starts one-PR sessions for owned layers. Every action except `CANCEL` comes with numbered `## Instructions` and exact commands, so the agent does not invent policy. In the [behavior evals](evals/README.md)
|
|
99
|
+
For a single PR, each tick returns exactly one action: `WAIT`, `READY`, `MARK_READY`, `TRIAGE`, `MERGE`, `CANCEL`, or `ESCALATE`. A `stack` poll can also return `SHEPHERD`, which starts one-PR sessions for owned layers. Every action except `CANCEL` comes with numbered `## Instructions` and exact commands, so the agent does not invent policy. In the [behavior evals](evals/README.md) (Sonnet 5.5 at low effort, all 57 cases, 2026-11-09), the agent scores **0.96** with the plugin and **0.84** without it, a weighted mean Δ of **+0.11** [+0.08, +0.14]. Twelve cases favor the plugin, such as creating a PR without asking (0.92 → 0.00) and running with no PR argument (1.00 → 0.00). See [docs/actions.md](docs/actions.md) for the output contract.
|
|
100
100
|
|
|
101
101
|
### Lower GitHub API quota use
|
|
102
102
|
|
|
@@ -31,6 +31,21 @@ function stripChrome(body) {
|
|
|
31
31
|
.replace(/^<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\s*/, "")
|
|
32
32
|
.replace(/^<!-- review_stack_entry_start -->[\s\S]*?<!-- review_stack_entry_end -->\s*/, "");
|
|
33
33
|
}
|
|
34
|
+
/** The "Review info" block that follows a progress notice: it opens with this summary line. */
|
|
35
|
+
const REVIEW_INFO_START = /\n<details>\n<summary><strong>(?:<picture>(?:<source [^>\n]*>)*<img [^>\n]*><\/picture> )?Review info<\/strong><\/summary>\n/;
|
|
36
|
+
/**
|
|
37
|
+
* Lines the "Review info" block may hold: layout tags, picture-icon summaries, `- **Key**: value`
|
|
38
|
+
* settings with CodeRabbit's known keys, file bullets, and the commit-range sentence. Any other line keeps the comment visible.
|
|
39
|
+
*/
|
|
40
|
+
const REVIEW_INFO_LINE = /^(?:|<\/?details>|<\/?dl>|<\/?dd>|<br>|<summary><strong>(?:<picture>(?:<source [^>]*>)*<img [^>]*><\/picture> )?(?:Review info|Run configuration|Commits|Files selected for processing: \d+)<\/strong><\/summary>|- \*\*(?:Source mode|Repository|Branch|File|Commit|Layers|Review profile|Plan|Run ID)\*\*: [^<>\n]*|\* `[^`\n]+`|Reviewing files that changed from the base of the PR and between [a-f0-9]+ and [a-f0-9]+\.)$/;
|
|
41
|
+
/** Removes a trailing "Review info" block made only of known metadata lines. */
|
|
42
|
+
function stripReviewInfo(quoted) {
|
|
43
|
+
const start = quoted.search(REVIEW_INFO_START);
|
|
44
|
+
if (start < 0)
|
|
45
|
+
return quoted;
|
|
46
|
+
const info = quoted.slice(start).split("\n");
|
|
47
|
+
return info.every((line) => REVIEW_INFO_LINE.test(line)) ? quoted.slice(0, start) : quoted;
|
|
48
|
+
}
|
|
34
49
|
function isQuotedNotice(quoted) {
|
|
35
50
|
const onlyQuotedLines = quoted.split("\n").every((line) => !line.trim() || line.startsWith(">"));
|
|
36
51
|
if (!onlyQuotedLines)
|
|
@@ -51,7 +66,8 @@ const rule = (item) => {
|
|
|
51
66
|
if (!isBotItem(item, ["coderabbitai"], ["pr-comment"]))
|
|
52
67
|
return null;
|
|
53
68
|
const body = stripChrome(item.body);
|
|
54
|
-
const
|
|
69
|
+
const envelope = body.match(NOTICE_ENVELOPE)?.[2];
|
|
70
|
+
const quoted = envelope === undefined ? undefined : stripReviewInfo(envelope).trim();
|
|
55
71
|
const isStatus = COMMAND_REPLY.test(body) || STATUS_COMMENTS.some((notice) => notice.test(body));
|
|
56
72
|
if (!(quoted && isQuotedNotice(quoted)) && !isStatus)
|
|
57
73
|
return null;
|
|
@@ -5,8 +5,10 @@ const QUOTA_NOTICES = [
|
|
|
5
5
|
/^Codex usage limits have been reached for code reviews\. Please check with the admins of this repo to increase the limits by adding credits\.\nRepo admins can enable using credits for code reviews in their \[settings\]\(https:\/\/chatgpt\.com\/[^\s)]*\)\.$/,
|
|
6
6
|
/^Codex usage limits have been reached\. Please try again later\.$/,
|
|
7
7
|
];
|
|
8
|
-
const ABOUT_BLOCK = /\n\n<details> <summary>ℹ️ About Codex (?:in GitHub|security reviews in GitHub)<\/summary>[\s\S]*?<\/details
|
|
8
|
+
const ABOUT_BLOCK = /\n\n<details> <summary>ℹ️ About Codex (?:in GitHub|security reviews in GitHub)<\/summary>[\s\S]*?<\/details>(\n\n<\/details>)?$/;
|
|
9
9
|
const ACTIVITY_SUMMARY = /^<!-- codex-pull-request-review-summary -->\n(?:<!-- codex-[a-z0-9-]+:[^\n]* -->\n)*\n?## Codex Review Summary\n\nThis comment shows the latest Codex review activity on this pull request\.\n\n\| Review \| Status \| Commit \| Review trigger \|\n\| --- \| --- \| --- \| --- \|\n(?:\|[^\n]+\|\n?)+$/;
|
|
10
|
+
/** The same table collapsed under `<details>`, with the overall status in the summary line. */
|
|
11
|
+
const COLLAPSED_ACTIVITY_SUMMARY = /^<!-- codex-pull-request-review-summary -->\n(?:<!-- codex-[a-z0-9-]+:[^\n]* -->\n)*\n?<details>\n<summary><strong>Codex Review Summary<\/strong>(?:: [^<\n]+)?<\/summary>\n\nThis comment shows the latest Codex review activity on this pull request\.\n\n\| Review \| Status \| Commit \| Review trigger \|\n\| --- \| --- \| --- \| --- \|\n(?:\|[^\n]+\|\n?)+\n*<\/details>$/;
|
|
10
12
|
/**
|
|
11
13
|
* Codex's rotating clean-review sign-offs, as observed. Prose is matched only from this list so a
|
|
12
14
|
* short piece of advice in that position stays visible; a new sign-off is shown until added here.
|
|
@@ -47,10 +49,10 @@ const rule = (item) => {
|
|
|
47
49
|
// Strip only leading codex-* metadata markers and the trailing About block; extra findings stay
|
|
48
50
|
// visible. The activity table's own marker has no colon, so it is kept for ACTIVITY_SUMMARY.
|
|
49
51
|
const content = body
|
|
50
|
-
.replace(ABOUT_BLOCK, "")
|
|
52
|
+
.replace(ABOUT_BLOCK, (_about, close) => close ?? "")
|
|
51
53
|
.trim()
|
|
52
54
|
.replace(/^(?:<!-- codex-[a-z0-9-]+:[^\n]* -->\s*)+/, "");
|
|
53
|
-
if (ACTIVITY_SUMMARY.test(content)) {
|
|
55
|
+
if (ACTIVITY_SUMMARY.test(content) || COLLAPSED_ACTIVITY_SUMMARY.test(content)) {
|
|
54
56
|
return { autoResolve: true, suppress: true, reason: "Codex review-activity table" };
|
|
55
57
|
}
|
|
56
58
|
if (CLEAN_REVIEW.test(content) || CLEAN_SECURITY.test(content)) {
|
|
@@ -71,7 +71,9 @@ export function buildSimpleIterateInstructions(result) {
|
|
|
71
71
|
case "escalate": {
|
|
72
72
|
const pending = result.escalate.pendingReviewCommands;
|
|
73
73
|
if (!pending)
|
|
74
|
-
return [
|
|
74
|
+
return [
|
|
75
|
+
"Stop — human direction is required before automated polling can resume. Report the evidence above and the decision the human must make; do not attempt a fix, rerun, requeue, branch update, or merge for this PR.",
|
|
76
|
+
];
|
|
75
77
|
return [
|
|
76
78
|
"Stop polling. Ask the user whether to run the pending review commands shown above.",
|
|
77
79
|
"If yes, set any `$DISMISS_MESSAGE` to a one-sentence disposition, run every pending command from the pushed PR head, then rerun Shepherd with the same options.",
|
package/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -115,7 +115,7 @@ export function projectIterateLean(result, opts) {
|
|
|
115
115
|
...base,
|
|
116
116
|
// Zero counts are omitted, matching the text `**deferred (in merge queue)**` rollup.
|
|
117
117
|
...(result.deferredWork && {
|
|
118
|
-
deferredWork:
|
|
118
|
+
deferredWork: nonZeroCounts(result.deferredWork),
|
|
119
119
|
}),
|
|
120
120
|
log: adaptIterateLog(result.log),
|
|
121
121
|
instructions: simpleInstructions(result),
|
|
@@ -182,6 +182,9 @@ export function projectIterateLean(result, opts) {
|
|
|
182
182
|
resolveOnlyCommand: result.triage.resolveOnlyCommand,
|
|
183
183
|
}),
|
|
184
184
|
...(result.triage.reviewItems && { reviewItems: result.triage.reviewItems }),
|
|
185
|
+
...(result.triage.deferredWork && {
|
|
186
|
+
deferredWork: nonZeroCounts(result.triage.deferredWork),
|
|
187
|
+
}),
|
|
185
188
|
...(result.triage.instructions.length > 0 && {
|
|
186
189
|
instructions: result.triage.instructions,
|
|
187
190
|
}),
|
|
@@ -247,3 +250,7 @@ function cappedList(key, items) {
|
|
|
247
250
|
return { [key]: shown, ...(hidden > 0 && { [`${key}More`]: hidden }) };
|
|
248
251
|
}
|
|
249
252
|
export { projectIterateVerbose } from "./iterate-verbose.mjs";
|
|
253
|
+
/** Zero deferred-work counts are omitted, matching the text `**deferred (in merge queue)**` rollup. */
|
|
254
|
+
function nonZeroCounts(counts) {
|
|
255
|
+
return Object.fromEntries(Object.entries(counts).filter((entry) => entry[1] > 0));
|
|
256
|
+
}
|
|
@@ -9,6 +9,7 @@ import { renderRelatedJobLines } from "./related-jobs-format.mjs";
|
|
|
9
9
|
import { isFailingAgentCheck } from "../checks/conclusions.mjs";
|
|
10
10
|
import { renderMergeCommand } from "../commands/iterate/merge.mjs";
|
|
11
11
|
import { partitionFixThreads } from "../commands/iterate/fix-instruction-threads.mjs";
|
|
12
|
+
import { formatDeferredWorkLine } from "./iterate-merge-formatter.mjs";
|
|
12
13
|
export function formatTriageResult(header, result, opts = {}) {
|
|
13
14
|
const verbose = opts.verbose ?? false;
|
|
14
15
|
const topCap = verbose ? undefined : BODY_TRUNCATE_MAX_CHARS;
|
|
@@ -17,6 +18,8 @@ export function formatTriageResult(header, result, opts = {}) {
|
|
|
17
18
|
const { shown, total } = result.triage.reviewItems;
|
|
18
19
|
sections.push(`**showing** ${shown} of ${total} review items`);
|
|
19
20
|
}
|
|
21
|
+
if (result.triage.deferredWork)
|
|
22
|
+
sections.push(formatDeferredWorkLine(result.triage.deferredWork));
|
|
20
23
|
if (result.mergedBasePullRequests?.length) {
|
|
21
24
|
sections.push("## Merged PRs matching the current base");
|
|
22
25
|
sections.push(result.mergedBasePullRequests
|
|
@@ -80,11 +83,12 @@ export function formatTriageResult(header, result, opts = {}) {
|
|
|
80
83
|
? ` [conclusion: ${ch.conclusion}]`
|
|
81
84
|
: "";
|
|
82
85
|
const attemptTag = ch.runAttempt !== undefined ? ` [attempt: ${ch.runAttempt}]` : "";
|
|
86
|
+
const blockerTag = ch.releasedBlocker ? ` [blocker released: ${ch.releasedBlocker}]` : "";
|
|
83
87
|
const scopeTag = ch.scope
|
|
84
88
|
? ` [scope: ${ch.scope}${ch.commitOid ? `, commit: ${ch.commitOid}` : ""}]`
|
|
85
89
|
: "";
|
|
86
90
|
const lines = [
|
|
87
|
-
`- ${locator} — \`${workflowPrefix}${jobLabel}\`${conclusionTag}${attemptTag}${scopeTag}`,
|
|
91
|
+
`- ${locator} — \`${workflowPrefix}${jobLabel}\`${conclusionTag}${attemptTag}${blockerTag}${scopeTag}`,
|
|
88
92
|
];
|
|
89
93
|
if (ch.conclusion !== "CANCELLED") {
|
|
90
94
|
if (ch.failedStep)
|
|
@@ -14,14 +14,20 @@ export declare function hasCheckDrivenActionableWork(checks: {
|
|
|
14
14
|
passing: ClassifiedCheck[];
|
|
15
15
|
skipped: ClassifiedCheck[];
|
|
16
16
|
filtered: ClassifiedCheck[];
|
|
17
|
-
ignored?: TriagedCheck[];
|
|
18
17
|
}, mergeStatusValue: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* A merge-queue entry whose branch conflicts with its base. Its triage offers dequeue-and-resolve
|
|
20
|
+
* or wait; review work waits until the PR leaves the queue on either path. Shared by `check.mts`
|
|
21
|
+
* (seen markers stay unwritten) and `iterate` (review items and fix attempts stay out of the
|
|
22
|
+
* tick), so a caller who waits never has review items consumed or counted toward
|
|
23
|
+
* `triage-thrash`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isQueuedConflict(inQueue: boolean | undefined, mergeStatus: string): boolean;
|
|
19
26
|
export declare function attachAndMergeCheckAnnotations(buckets: {
|
|
20
27
|
passing: ClassifiedCheck[];
|
|
21
28
|
failing: TriagedCheck[];
|
|
22
29
|
skipped: ClassifiedCheck[];
|
|
23
30
|
filtered: ClassifiedCheck[];
|
|
24
|
-
ignored: ClassifiedCheck[];
|
|
25
31
|
}, seenMap: Map<string, {
|
|
26
32
|
seenAt: number;
|
|
27
33
|
}>, prNumber: number, cacheOpts?: AnnotationCacheOptions): Promise<{
|
|
@@ -29,6 +35,5 @@ export declare function attachAndMergeCheckAnnotations(buckets: {
|
|
|
29
35
|
failing: TriagedCheck[];
|
|
30
36
|
skipped: ClassifiedCheck[];
|
|
31
37
|
filtered: ClassifiedCheck[];
|
|
32
|
-
ignored: ClassifiedCheck[];
|
|
33
38
|
}>;
|
|
34
39
|
export declare function annotationMarkerBody(a: CheckAnnotation): string;
|
|
@@ -12,7 +12,6 @@ export function checksWithActionableAnnotations(report) {
|
|
|
12
12
|
...report.checks.passing,
|
|
13
13
|
...report.checks.skipped,
|
|
14
14
|
...report.checks.filtered,
|
|
15
|
-
...(report.checks.ignored ?? []),
|
|
16
15
|
].filter(hasActionableAnnotation);
|
|
17
16
|
}
|
|
18
17
|
/**
|
|
@@ -25,22 +24,25 @@ export function checksWithActionableAnnotations(report) {
|
|
|
25
24
|
*/
|
|
26
25
|
export function hasCheckDrivenActionableWork(checks, mergeStatusValue) {
|
|
27
26
|
return (checks.failing.length > 0 ||
|
|
28
|
-
[
|
|
29
|
-
...checks.failing,
|
|
30
|
-
...checks.passing,
|
|
31
|
-
...checks.skipped,
|
|
32
|
-
...checks.filtered,
|
|
33
|
-
...(checks.ignored ?? []),
|
|
34
|
-
].some(hasActionableAnnotation) ||
|
|
27
|
+
[...checks.failing, ...checks.passing, ...checks.skipped, ...checks.filtered].some(hasActionableAnnotation) ||
|
|
35
28
|
mergeStatusValue === "CONFLICTS");
|
|
36
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* A merge-queue entry whose branch conflicts with its base. Its triage offers dequeue-and-resolve
|
|
32
|
+
* or wait; review work waits until the PR leaves the queue on either path. Shared by `check.mts`
|
|
33
|
+
* (seen markers stay unwritten) and `iterate` (review items and fix attempts stay out of the
|
|
34
|
+
* tick), so a caller who waits never has review items consumed or counted toward
|
|
35
|
+
* `triage-thrash`.
|
|
36
|
+
*/
|
|
37
|
+
export function isQueuedConflict(inQueue, mergeStatus) {
|
|
38
|
+
return inQueue === true && mergeStatus === "CONFLICTS";
|
|
39
|
+
}
|
|
37
40
|
export async function attachAndMergeCheckAnnotations(buckets, seenMap, prNumber, cacheOpts) {
|
|
38
41
|
const candidates = [
|
|
39
42
|
...buckets.failing,
|
|
40
43
|
...buckets.passing,
|
|
41
44
|
...buckets.skipped,
|
|
42
45
|
...buckets.filtered,
|
|
43
|
-
...buckets.ignored,
|
|
44
46
|
].filter(shouldFetchCheckAnnotations);
|
|
45
47
|
const expectedCounts = new Map(candidates.flatMap((c) => c.id != null && c.annotationCount ? [[c.id, c.annotationCount]] : []));
|
|
46
48
|
const annotated = await attachUnseenCheckAnnotations(candidates, seenMap, prNumber, cacheOpts && { ...cacheOpts, expectedCounts });
|
|
@@ -57,7 +59,6 @@ export async function attachAndMergeCheckAnnotations(buckets, seenMap, prNumber,
|
|
|
57
59
|
failing: apply(buckets.failing),
|
|
58
60
|
skipped: apply(buckets.skipped),
|
|
59
61
|
filtered: apply(buckets.filtered),
|
|
60
|
-
ignored: apply(buckets.ignored),
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
async function attachUnseenCheckAnnotations(checks, seenMap, prNumber, cacheOpts) {
|
|
@@ -110,7 +110,7 @@ function reportChecks(report) {
|
|
|
110
110
|
...report.checks.inProgress,
|
|
111
111
|
...report.checks.skipped,
|
|
112
112
|
...report.checks.filtered,
|
|
113
|
-
...(report.checks.
|
|
113
|
+
...(report.checks.ignoredNames ?? []).map((name) => ({ name })),
|
|
114
114
|
...(report.checks.supersededNames ?? []).map((name) => ({ name })),
|
|
115
115
|
];
|
|
116
116
|
}
|
package/bin/commands/check.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import { deriveMergeStatus } from "../merge-status/derive.mjs";
|
|
|
17
17
|
import { loadConfig } from "../config/load.mjs";
|
|
18
18
|
import { classifyVisibleComments } from "../comments/visible-comments.mjs";
|
|
19
19
|
import { computeStatus } from "./check-status.mjs";
|
|
20
|
-
import { annotationMarkerBody, attachAndMergeCheckAnnotations, hasCheckDrivenActionableWork, } from "./check-annotations.mjs";
|
|
20
|
+
import { annotationMarkerBody, attachAndMergeCheckAnnotations, hasCheckDrivenActionableWork, isQueuedConflict, } from "./check-annotations.mjs";
|
|
21
21
|
import { buildTerminalReport } from "./check-terminal-report.mjs";
|
|
22
22
|
import { isBlockedByFilteredCheck, refreshReadyMergeability, refreshUnknownMergeability, } from "./ready-mergeability.mjs";
|
|
23
23
|
import { loadSeenMap, markSeen, classifyItem, mutationWasDenied } from "../state/seen-comments.mjs";
|
|
@@ -186,7 +186,6 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
186
186
|
const inProgress = classifiedChecks.filter((c) => c.category === "in_progress");
|
|
187
187
|
const skipped = classifiedChecks.filter((c) => c.category === "skipped");
|
|
188
188
|
const filtered = classifiedChecks.filter((c) => c.category === "filtered");
|
|
189
|
-
const ignored = classifiedChecks.filter((c) => c.category === "ignored");
|
|
190
189
|
const triagedBase = failing.length > 0 && !opts.skipTriage
|
|
191
190
|
? await triageFailingChecks(failing, repo, stateKey, triageBudget, classifiedChecks.filter((c) => c.category !== "failing"))
|
|
192
191
|
: failing;
|
|
@@ -199,8 +198,7 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
199
198
|
const ruleSet = await loadClassifyRules(getEffectiveCwd(), config.standardRules);
|
|
200
199
|
const classifyIndex = buildClassifyIndex(ruleSet, batchData);
|
|
201
200
|
const partition = partitionBatch(classifyIndex, batchData);
|
|
202
|
-
const merged = await attachAndMergeCheckAnnotations({ passing, failing: triagedBase, skipped, filtered
|
|
203
|
-
const ignoredAnnotated = merged.ignored.filter((c) => (c.annotations?.length ?? 0) > 0);
|
|
201
|
+
const merged = await attachAndMergeCheckAnnotations({ passing, failing: triagedBase, skipped, filtered }, seenMap, prNumber, { stateKey, headSha: batchData.headRefOid });
|
|
204
202
|
const minimizedCommentCandidates = batchData.comments.filter((c) => c.isMinimized && !partition.suppressedCommentIds.has(c.id));
|
|
205
203
|
const deniedRuleAutoResolveCommentIds = new Set(partition.ruleAutoResolveCommentIds.filter((id) => !canGenerateItemMutation(id, batchData.comments.find((comment) => comment.id === id)?.body ?? "", batchData.comments.find((comment) => comment.id === id)?.viewerCanMinimize, "minimize")));
|
|
206
204
|
const visibleCommentClassification = classifyVisibleComments(batchData.comments.filter((c) => !partition.suppressedCommentIds.has(c.id) || deniedRuleAutoResolveCommentIds.has(c.id)), seenMap, config.iterate["minimize-comments"], botUsernames, new Set(batchData.comments
|
|
@@ -293,16 +291,18 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
293
291
|
// that also has check-driven work (failing checks, actionable annotations, conflicts) is
|
|
294
292
|
// NOT deferred — index.mts still renders these items via triage — so this must stay false
|
|
295
293
|
// in that case too, or their seen markers would be suppressed while actually being shown.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
294
|
+
// A queued conflict also holds review work back (iterate strips it from that triage tick).
|
|
295
|
+
const queuedConflict = isQueuedConflict(batchData.isInMergeQueue, mergeStatus.status);
|
|
296
|
+
const deferWhileQueued = queuedConflict ||
|
|
297
|
+
(opts.merge === true &&
|
|
298
|
+
batchData.isInMergeQueue === true &&
|
|
299
|
+
config.actions["work-while-queued"] !== true &&
|
|
300
|
+
!hasCheckDrivenActionableWork({
|
|
301
|
+
failing: merged.failing,
|
|
302
|
+
passing: merged.passing,
|
|
303
|
+
skipped: merged.skipped,
|
|
304
|
+
filtered: merged.filtered,
|
|
305
|
+
}, mergeStatus.status));
|
|
306
306
|
// A deferred tick shows no review items, so it is never capped; its seen markers stay unwritten.
|
|
307
307
|
const { buckets: budgeted, rollup: reviewItems, heldThreadReviewIds, } = applyReviewOutputBudget({
|
|
308
308
|
changesRequested: changesRequestedReviewVisibility,
|
|
@@ -314,12 +314,7 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
314
314
|
editedComments,
|
|
315
315
|
}, deferWhileQueued ? 0 : (opts.reviewOutputMaxChars ?? 0));
|
|
316
316
|
if (opts.persistSeen !== false) {
|
|
317
|
-
const successfulAnnotations = [
|
|
318
|
-
...merged.passing,
|
|
319
|
-
...merged.skipped,
|
|
320
|
-
...merged.filtered,
|
|
321
|
-
...merged.ignored,
|
|
322
|
-
]
|
|
317
|
+
const successfulAnnotations = [...merged.passing, ...merged.skipped, ...merged.filtered]
|
|
323
318
|
.filter((check) => check.conclusion === "SUCCESS")
|
|
324
319
|
.flatMap((check) => check.annotations ?? []);
|
|
325
320
|
const deferredMarkSeen = deferWhileQueued
|
|
@@ -333,7 +328,8 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
333
328
|
...approvedReviewVisibility.toMarkSeen.map((r) => markSeen(stateKey, r.id, r.body)),
|
|
334
329
|
];
|
|
335
330
|
await Promise.allSettled([
|
|
336
|
-
|
|
331
|
+
// A queued conflict defers annotation fixes too (path (a)), so they stay unseen.
|
|
332
|
+
...(queuedConflict ? [] : successfulAnnotations).map((a) => markSeen(stateKey, a.id, annotationMarkerBody(a))),
|
|
337
333
|
...deferredMarkSeen,
|
|
338
334
|
...batchData.comments
|
|
339
335
|
.filter((c) => partition.suppressedCommentIds.has(c.id) && !deniedRuleAutoResolveCommentIds.has(c.id))
|
|
@@ -410,7 +406,6 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
410
406
|
inProgress: inProgress,
|
|
411
407
|
skipped: merged.skipped,
|
|
412
408
|
filtered: merged.filtered,
|
|
413
|
-
...(ignoredAnnotated.length > 0 && { ignored: ignoredAnnotated }),
|
|
414
409
|
filteredNames: verdict.filteredNames,
|
|
415
410
|
blockedByFilteredCheck,
|
|
416
411
|
...(verdict.ignoredNames.length > 0 && {
|
|
@@ -2,6 +2,8 @@ import type { IterateResult } from "../../types.mts";
|
|
|
2
2
|
export interface CheckBlockerGate {
|
|
3
3
|
deferredNames: ReadonlySet<string>;
|
|
4
4
|
releasedNames: ReadonlySet<string>;
|
|
5
|
+
/** Released check name → `owner/name#number` of the blocker that merged or closed. */
|
|
6
|
+
releasedBlockers: ReadonlyMap<string, string>;
|
|
5
7
|
/** `owner/name#number` for blockers that are still open. */
|
|
6
8
|
openBlockers: readonly string[];
|
|
7
9
|
}
|
|
@@ -40,6 +40,7 @@ export async function resolveCheckBlockerGate(key, failing, mergeStatus) {
|
|
|
40
40
|
}
|
|
41
41
|
const deferredNames = new Set();
|
|
42
42
|
const releasedNames = new Set();
|
|
43
|
+
const releasedBlockers = new Map();
|
|
43
44
|
const openBlockers = [];
|
|
44
45
|
const seen = new Set();
|
|
45
46
|
for (const record of matched) {
|
|
@@ -60,12 +61,13 @@ export async function resolveCheckBlockerGate(key, failing, mergeStatus) {
|
|
|
60
61
|
}
|
|
61
62
|
else {
|
|
62
63
|
releasedNames.add(record.checkName);
|
|
64
|
+
releasedBlockers.set(record.checkName, formatCheckBlockerRef(record.blocker));
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
if (deferredNames.size === 0 && releasedNames.size === 0)
|
|
67
69
|
return null;
|
|
68
|
-
return { deferredNames, releasedNames, openBlockers };
|
|
70
|
+
return { deferredNames, releasedNames, releasedBlockers, openBlockers };
|
|
69
71
|
}
|
|
70
72
|
/** Append the open blocker to a WAIT log. Other actions are unchanged. */
|
|
71
73
|
export function annotateBlockedWait(result, gate) {
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { AgentCheck, ResolveCommand, Review } from "../../types.mts";
|
|
2
|
-
|
|
2
|
+
export declare const TRIAGE_CONTINUATION = "Rerun Shepherd now.";
|
|
3
|
+
/** The final rerun step when a queued conflict offers both paths. */
|
|
4
|
+
export declare const QUEUED_CONFLICT_CONTINUATION = "If you chose (a), rerun Shepherd now. If you chose (b), rerun Shepherd after the polling interval.";
|
|
5
|
+
/**
|
|
6
|
+
* The `[TRIAGE]` recurrence step, whether plain, the queued-conflict variant, or rewritten by
|
|
7
|
+
* the quota warning. The quota cadence supersedes both queued-conflict paths.
|
|
8
|
+
*/
|
|
3
9
|
export declare function isTriageContinuation(step: string): boolean;
|
|
4
10
|
/** Build the stale-CR step for `## Changes-requested reviews`. Empty when no human CR is stale. */
|
|
5
11
|
export declare function buildCrStaleClause(reviews: Review[]): string;
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { playbookPointer } from "../playbook-pointer.mjs";
|
|
2
2
|
// The `[TRIAGE]` heading and the skill already say the action is non-terminal.
|
|
3
|
-
const TRIAGE_CONTINUATION = "Rerun Shepherd now.";
|
|
4
|
-
/** The
|
|
3
|
+
export const TRIAGE_CONTINUATION = "Rerun Shepherd now.";
|
|
4
|
+
/** The final rerun step when a queued conflict offers both paths. */
|
|
5
|
+
export const QUEUED_CONFLICT_CONTINUATION = "If you chose (a), rerun Shepherd now. If you chose (b), rerun Shepherd after the polling interval.";
|
|
6
|
+
/**
|
|
7
|
+
* The `[TRIAGE]` recurrence step, whether plain, the queued-conflict variant, or rewritten by
|
|
8
|
+
* the quota warning. The quota cadence supersedes both queued-conflict paths.
|
|
9
|
+
*/
|
|
5
10
|
export function isTriageContinuation(step) {
|
|
6
|
-
return step
|
|
11
|
+
return (step.startsWith(TRIAGE_CONTINUATION.slice(0, -1)) ||
|
|
12
|
+
step === QUEUED_CONFLICT_CONTINUATION ||
|
|
13
|
+
/\[TRIAGE\].*non-terminal/i.test(step));
|
|
7
14
|
}
|
|
8
15
|
/** Build the stale-CR step for `## Changes-requested reviews`. Empty when no human CR is stale. */
|
|
9
16
|
export function buildCrStaleClause(reviews) {
|
|
@@ -12,7 +12,7 @@ export declare function countReportedChecks(checks: {
|
|
|
12
12
|
inProgress: readonly unknown[];
|
|
13
13
|
skipped: readonly unknown[];
|
|
14
14
|
filtered: readonly unknown[];
|
|
15
|
-
|
|
15
|
+
ignoredNames?: readonly unknown[];
|
|
16
16
|
}): number;
|
|
17
17
|
/** Place the missing-CI note with the conflict step, ahead of later repair lines. */
|
|
18
18
|
export declare function insertConflictingHeadCiNote(instructions: string[], note: string | undefined): void;
|
|
@@ -18,7 +18,7 @@ export function countReportedChecks(checks) {
|
|
|
18
18
|
checks.inProgress.length +
|
|
19
19
|
checks.skipped.length +
|
|
20
20
|
checks.filtered.length +
|
|
21
|
-
(checks.
|
|
21
|
+
(checks.ignoredNames?.length ?? 0));
|
|
22
22
|
}
|
|
23
23
|
/** Place the missing-CI note with the conflict step, ahead of later repair lines. */
|
|
24
24
|
export function insertConflictingHeadCiNote(instructions, note) {
|
|
@@ -9,9 +9,10 @@ import { classifyReviewSummaries } from "./classify.mjs";
|
|
|
9
9
|
import { applyStallGuard } from "./stall.mjs";
|
|
10
10
|
import { clearStallState } from "../../state/iterate-stall.mjs";
|
|
11
11
|
import { handleTriage } from "./triage.mjs";
|
|
12
|
+
import { deferQueuedConflictReviewWork } from "./queued-conflict-deferral.mjs";
|
|
12
13
|
import { normalizeBotUsernames } from "../../comments/authors.mjs";
|
|
13
14
|
import { autoMinimizeComments } from "../../comments/resolve.mjs";
|
|
14
|
-
import { hasCheckDrivenActionableWork } from "../check-annotations.mjs";
|
|
15
|
+
import { hasCheckDrivenActionableWork, isQueuedConflict } from "../check-annotations.mjs";
|
|
15
16
|
import { buildReadyMergeOutcome, handleActiveMergeState } from "./merge-state.mjs";
|
|
16
17
|
import { buildIterateBase } from "./base.mjs";
|
|
17
18
|
import { markReadyIfAuthorized } from "./mark-ready.mjs";
|
|
@@ -182,9 +183,20 @@ async function runIterateCore(opts, tick) {
|
|
|
182
183
|
report.mergeQueue?.inQueue === true &&
|
|
183
184
|
config.actions["work-while-queued"] !== true;
|
|
184
185
|
if (hasActionableWork && !(deferWhileQueued && !checkDrivenActionableWork)) {
|
|
186
|
+
const reviewWork = {
|
|
187
|
+
report: reportForWork,
|
|
188
|
+
reviewSummaryIds,
|
|
189
|
+
firstLookSummaries,
|
|
190
|
+
editedSummaries,
|
|
191
|
+
surfacedApprovals,
|
|
192
|
+
ruleAutoResolveThreadIds: report.threads.ruleAutoResolveIds,
|
|
193
|
+
};
|
|
194
|
+
// A queued conflict holds review work until the PR leaves the queue (see check.mts).
|
|
185
195
|
return handleTriage({
|
|
196
|
+
...(isQueuedConflict(report.mergeQueue?.inQueue, report.mergeStatus.status)
|
|
197
|
+
? deferQueuedConflictReviewWork(reviewWork, config.iterate["minimize-approvals"])
|
|
198
|
+
: reviewWork),
|
|
186
199
|
base,
|
|
187
|
-
report: reportForWork,
|
|
188
200
|
opts: { ...opts, prNumber },
|
|
189
201
|
headSha,
|
|
190
202
|
stallKey,
|
|
@@ -192,16 +204,12 @@ async function runIterateCore(opts, tick) {
|
|
|
192
204
|
stallTimeoutSeconds,
|
|
193
205
|
repoOwner,
|
|
194
206
|
repoName,
|
|
195
|
-
reviewSummaryIds,
|
|
196
|
-
firstLookSummaries,
|
|
197
|
-
editedSummaries,
|
|
198
|
-
surfacedApprovals,
|
|
199
207
|
botUsernames,
|
|
200
208
|
releasedCheckNames: blockerGate?.releasedNames,
|
|
209
|
+
releasedCheckBlockers: blockerGate?.releasedBlockers,
|
|
201
210
|
...((unreportedRepair.length > 0 || staleAncestry !== null) && {
|
|
202
211
|
repairInstructions: [...unreportedRepair, ...(staleAncestry?.instructions ?? [])],
|
|
203
212
|
}),
|
|
204
|
-
ruleAutoResolveThreadIds: report.threads.ruleAutoResolveIds,
|
|
205
213
|
});
|
|
206
214
|
}
|
|
207
215
|
if (unreportedRepair.length > 0 && staleAncestry) {
|
|
@@ -221,6 +229,7 @@ async function runIterateCore(opts, tick) {
|
|
|
221
229
|
surfacedApprovals,
|
|
222
230
|
botUsernames,
|
|
223
231
|
releasedCheckNames: blockerGate?.releasedNames,
|
|
232
|
+
releasedCheckBlockers: blockerGate?.releasedBlockers,
|
|
224
233
|
repairInstructions: [...unreportedRepair, ...staleAncestry.instructions],
|
|
225
234
|
ruleAutoResolveThreadIds: report.threads.ruleAutoResolveIds,
|
|
226
235
|
});
|
|
@@ -259,6 +268,7 @@ async function runIterateCore(opts, tick) {
|
|
|
259
268
|
surfacedApprovals,
|
|
260
269
|
botUsernames,
|
|
261
270
|
releasedCheckNames: blockerGate?.releasedNames,
|
|
271
|
+
releasedCheckBlockers: blockerGate?.releasedBlockers,
|
|
262
272
|
repairInstructions: [...unreportedRepair, ...staleAncestry.instructions],
|
|
263
273
|
ruleAutoResolveThreadIds: report.threads.ruleAutoResolveIds,
|
|
264
274
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type MergeMethod } from "../../config/merge-method.mts";
|
|
2
2
|
import type { MergeCommandPlan } from "../../types.mts";
|
|
3
3
|
import type { RestMergeStackGuard } from "../../github/rest-merge.mts";
|
|
4
|
+
/** gh CLI cannot remove a queue entry, so dequeueing goes through the GraphQL mutation. */
|
|
5
|
+
export declare function buildDequeueCommand(nodeId: string): string[];
|
|
4
6
|
export interface MergePlanInput {
|
|
5
7
|
transport?: "rest";
|
|
6
8
|
pr: number;
|
|
@@ -5,6 +5,11 @@ import { formatPrUrl } from "../../pr-reference.mjs";
|
|
|
5
5
|
import { buildPrShepherdCommand } from "../../cli/runner.mjs";
|
|
6
6
|
import { getGithubTransport } from "../../github/transport.mjs";
|
|
7
7
|
const ENQUEUE_MUTATION = "mutation EnqueuePullRequest($pullRequestId: ID!, $expectedHeadOid: GitObjectID!) { enqueuePullRequest(input: { pullRequestId: $pullRequestId, expectedHeadOid: $expectedHeadOid }) { mergeQueueEntry { id } } }";
|
|
8
|
+
const DEQUEUE_MUTATION = "mutation DequeuePullRequest($id: ID!) { dequeuePullRequest(input: { id: $id }) { mergeQueueEntry { id } } }";
|
|
9
|
+
/** gh CLI cannot remove a queue entry, so dequeueing goes through the GraphQL mutation. */
|
|
10
|
+
export function buildDequeueCommand(nodeId) {
|
|
11
|
+
return ["gh", "api", "graphql", "-f", `query=${DEQUEUE_MUTATION}`, "-f", `id=${nodeId}`];
|
|
12
|
+
}
|
|
8
13
|
export function buildMergeCommandPlan(input) {
|
|
9
14
|
if (input.transport === "rest" || getGithubTransport() === "rest") {
|
|
10
15
|
const configured = configuredMergeMethod(loadConfig().merge);
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import type { IterateResult, IterateResultBase, Review, ShepherdReport } from "../../types.mts";
|
|
1
|
+
import type { IterateDeferredWork, IterateResult, IterateResultBase, Review, ShepherdReport } from "../../types.mts";
|
|
2
2
|
type StallKey = {
|
|
3
3
|
owner: string;
|
|
4
4
|
repo: string;
|
|
5
5
|
pr: number;
|
|
6
6
|
};
|
|
7
7
|
export declare function buildReadyMergeOutcome(enabled: boolean | undefined, readyElapsed: boolean, base: IterateResultBase, report: ShepherdReport): IterateResult | null;
|
|
8
|
+
/** Raw counts of non-CI actionable work held back for one queued-PR wait tick. Omitted (all zero) when empty. */
|
|
9
|
+
export declare function buildDeferredWork(input: {
|
|
10
|
+
report: ShepherdReport;
|
|
11
|
+
reviewSummaryIds: string[];
|
|
12
|
+
firstLookSummaries: Review[];
|
|
13
|
+
editedSummaries: Review[];
|
|
14
|
+
surfacedApprovals: Review[];
|
|
15
|
+
minimizeApprovals: boolean;
|
|
16
|
+
}): IterateDeferredWork | undefined;
|
|
8
17
|
export declare function handleActiveMergeState(input: {
|
|
9
18
|
enabled: boolean | undefined;
|
|
10
19
|
active: boolean;
|
|
@@ -62,7 +62,7 @@ export function buildReadyMergeOutcome(enabled, readyElapsed, base, report) {
|
|
|
62
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
|
-
function buildDeferredWork(input) {
|
|
65
|
+
export function buildDeferredWork(input) {
|
|
66
66
|
const { report, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, minimizeApprovals, } = input;
|
|
67
67
|
// These buckets are not disjoint (e.g. an unresolved outdated thread is both
|
|
68
68
|
// `resolutionOnly` and `firstLook`; an eligible-to-minimize comment/summary is both
|