pr-shepherd 1.1.2 → 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 -0
- package/bin/commands/check-annotations.mjs +10 -0
- package/bin/commands/check.mjs +15 -11
- 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/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/types/iterate.d.mts +2 -0
- package/bin/types/report.d.mts +2 -0
- 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)
|
|
@@ -15,6 +15,14 @@ export declare function hasCheckDrivenActionableWork(checks: {
|
|
|
15
15
|
skipped: ClassifiedCheck[];
|
|
16
16
|
filtered: ClassifiedCheck[];
|
|
17
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;
|
|
18
26
|
export declare function attachAndMergeCheckAnnotations(buckets: {
|
|
19
27
|
passing: ClassifiedCheck[];
|
|
20
28
|
failing: TriagedCheck[];
|
|
@@ -27,6 +27,16 @@ export function hasCheckDrivenActionableWork(checks, mergeStatusValue) {
|
|
|
27
27
|
[...checks.failing, ...checks.passing, ...checks.skipped, ...checks.filtered].some(hasActionableAnnotation) ||
|
|
28
28
|
mergeStatusValue === "CONFLICTS");
|
|
29
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
|
+
}
|
|
30
40
|
export async function attachAndMergeCheckAnnotations(buckets, seenMap, prNumber, cacheOpts) {
|
|
31
41
|
const candidates = [
|
|
32
42
|
...buckets.failing,
|
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";
|
|
@@ -291,15 +291,18 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
291
291
|
// that also has check-driven work (failing checks, actionable annotations, conflicts) is
|
|
292
292
|
// NOT deferred — index.mts still renders these items via triage — so this must stay false
|
|
293
293
|
// in that case too, or their seen markers would be suppressed while actually being shown.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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));
|
|
303
306
|
// A deferred tick shows no review items, so it is never capped; its seen markers stay unwritten.
|
|
304
307
|
const { buckets: budgeted, rollup: reviewItems, heldThreadReviewIds, } = applyReviewOutputBudget({
|
|
305
308
|
changesRequested: changesRequestedReviewVisibility,
|
|
@@ -325,7 +328,8 @@ async function runScopedCheck(opts, context, repo, prNumber, stateKey) {
|
|
|
325
328
|
...approvedReviewVisibility.toMarkSeen.map((r) => markSeen(stateKey, r.id, r.body)),
|
|
326
329
|
];
|
|
327
330
|
await Promise.allSettled([
|
|
328
|
-
|
|
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))),
|
|
329
333
|
...deferredMarkSeen,
|
|
330
334
|
...batchData.comments
|
|
331
335
|
.filter((c) => partition.suppressedCommentIds.has(c.id) && !deniedRuleAutoResolveCommentIds.has(c.id))
|
|
@@ -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) {
|
|
@@ -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
|
|
@@ -35,8 +35,19 @@ export declare function buildNativeStackLayerRebase(repo: string, pr: {
|
|
|
35
35
|
trunk: string;
|
|
36
36
|
bottomPr?: number;
|
|
37
37
|
}, stackBottomPr?: number): string | undefined;
|
|
38
|
-
/**
|
|
39
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Point at the conflicts, and at the stack rebase when the PR is a native stack layer. A queued
|
|
40
|
+
* conflicting entry cannot form a merge group, so GitHub removes it once the queue reaches it.
|
|
41
|
+
* Pulling it out early (dequeue, then resolve) or waiting for that removal is the caller's
|
|
42
|
+
* call, so a queued PR gets both paths without a recommendation. `dequeueCommand` is the
|
|
43
|
+
* rendered dequeue mutation; it is present only while the PR is a queue entry.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildConflictInstruction(stackRebase: string | undefined, dequeueCommand?: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* The two paths for a conflicting PR that is still a queue entry. `resolve` is the branch
|
|
48
|
+
* update route; on a native stack, dequeueing a layer can also take queued layers above it out.
|
|
49
|
+
*/
|
|
50
|
+
export declare function buildQueuedConflictChoice(dequeueCommand: string, resolve: string, stack: boolean): string;
|
|
40
51
|
/**
|
|
41
52
|
* Push a conflict resolution or branch refresh: one branch, or the whole rewritten native stack.
|
|
42
53
|
* After a merge-queue ejection update, push only when the update or a fix changed the head.
|
|
@@ -54,10 +54,30 @@ export function buildNativeStackLayerRebase(repo, pr, stack, trunkConflict, stac
|
|
|
54
54
|
: { parentBranch: pr.baseBranch };
|
|
55
55
|
return buildNativeStackRebaseInstruction(repo, stack.number, start);
|
|
56
56
|
}
|
|
57
|
-
/**
|
|
58
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Point at the conflicts, and at the stack rebase when the PR is a native stack layer. A queued
|
|
59
|
+
* conflicting entry cannot form a merge group, so GitHub removes it once the queue reaches it.
|
|
60
|
+
* Pulling it out early (dequeue, then resolve) or waiting for that removal is the caller's
|
|
61
|
+
* call, so a queued PR gets both paths without a recommendation. `dequeueCommand` is the
|
|
62
|
+
* rendered dequeue mutation; it is present only while the PR is a queue entry.
|
|
63
|
+
*/
|
|
64
|
+
export function buildConflictInstruction(stackRebase, dequeueCommand) {
|
|
59
65
|
const pointer = "The branch has merge conflicts (see `**branch**` above).";
|
|
60
|
-
|
|
66
|
+
const resolve = stackRebase ?? "Resolve them before committing.";
|
|
67
|
+
if (!dequeueCommand)
|
|
68
|
+
return `${pointer} ${resolve}`;
|
|
69
|
+
return `${pointer} ${buildQueuedConflictChoice(dequeueCommand, resolve, stackRebase !== undefined)}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The two paths for a conflicting PR that is still a queue entry. `resolve` is the branch
|
|
73
|
+
* update route; on a native stack, dequeueing a layer can also take queued layers above it out.
|
|
74
|
+
*/
|
|
75
|
+
export function buildQueuedConflictChoice(dequeueCommand, resolve, stack) {
|
|
76
|
+
const leaves = stack
|
|
77
|
+
? "this PR leaves the queue, and so may any queued layers above it in this stack; other entries stay queued"
|
|
78
|
+
: "only this PR leaves the queue and the other entries stay queued";
|
|
79
|
+
const requeue = stack ? "enqueue it and any removed layers again" : "enqueue it again";
|
|
80
|
+
return `The PR is still in the merge queue, but GitHub cannot merge it until the conflicts are resolved. Choose one path: (a) pull it from the queue and resolve now — first run \`${dequeueCommand}\` (\`gh pr\` cannot remove a queue entry); ${leaves}; then: ${resolve} Once the PR is ready again, ${requeue} (with \`--merge\`, Shepherd prints the enqueue command). Or (b) wait — skip the dequeue and every step marked \`If you chose (a):\`, and rerun Shepherd after the polling interval instead of immediately, until GitHub removes the entry itself; this step repeats until then, and an unchanged conflict escalates through \`stall-timeout\`. On either path, review items stay deferred until the PR leaves the queue.`;
|
|
61
81
|
}
|
|
62
82
|
/**
|
|
63
83
|
* Push a conflict resolution or branch refresh: one branch, or the whole rewritten native stack.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { IterateDeferredWork, Review, ShepherdReport } from "../../types.mts";
|
|
2
|
+
/** Review work for one triage tick; see `isQueuedConflict` in check-annotations.mts. */
|
|
3
|
+
interface ReviewWork {
|
|
4
|
+
report: ShepherdReport;
|
|
5
|
+
reviewSummaryIds: string[];
|
|
6
|
+
firstLookSummaries: Review[];
|
|
7
|
+
editedSummaries: Review[];
|
|
8
|
+
surfacedApprovals: Review[];
|
|
9
|
+
ruleAutoResolveThreadIds?: string[];
|
|
10
|
+
}
|
|
11
|
+
/** Strip review work from a queued-conflict tick and count what was held back. */
|
|
12
|
+
export declare function deferQueuedConflictReviewWork(work: ReviewWork, minimizeApprovals: boolean): Omit<ReviewWork, "ruleAutoResolveThreadIds"> & {
|
|
13
|
+
deferredWork?: IterateDeferredWork;
|
|
14
|
+
};
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { buildDeferredWork } from "./merge-state.mjs";
|
|
2
|
+
/** Strip review work from a queued-conflict tick and count what was held back. */
|
|
3
|
+
export function deferQueuedConflictReviewWork(work, minimizeApprovals) {
|
|
4
|
+
const deferredWork = buildDeferredWork({ ...work, minimizeApprovals });
|
|
5
|
+
const { reviewItems: _held, ...report } = work.report;
|
|
6
|
+
const { ruleAutoResolveIds: _rule, ...threads } = report.threads;
|
|
7
|
+
return {
|
|
8
|
+
report: {
|
|
9
|
+
...report,
|
|
10
|
+
threads: { ...threads, actionable: [], resolutionOnly: [], firstLook: [] },
|
|
11
|
+
comments: { ...report.comments, actionable: [], minimizeIds: [], firstLook: [] },
|
|
12
|
+
changesRequestedReviews: [],
|
|
13
|
+
},
|
|
14
|
+
reviewSummaryIds: [],
|
|
15
|
+
firstLookSummaries: [],
|
|
16
|
+
editedSummaries: [],
|
|
17
|
+
surfacedApprovals: [],
|
|
18
|
+
...(deferredWork && { deferredWork }),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -10,4 +10,5 @@ export declare function renderResolveCommand(rc: ResolveCommand): string;
|
|
|
10
10
|
export declare function buildFixInstructions(threads: AgentThread[], actionableComments: AgentComment[], checks: AgentCheck[], changesRequestedReviews: Review[], baseBranch: string, resolveCommand: ResolveCommand, hasConflicts: boolean, prReference: string | number, firstLookThreads?: FirstLookThread[], firstLookComments?: FirstLookComment[], firstLookSummaries?: Review[], editedSummaries?: Review[], resolutionOnlyThreads?: ReviewThread[], resolveOnlyCommand?: ResolveCommand, behindBaseHint?: string, // iterate.behind-base-hint — see buildBehindBaseHintInstruction
|
|
11
11
|
isBehind?: boolean, _viewerCanUpdate?: boolean, hasExhaustedWorkflowRerun?: boolean, stackRebase?: string, // native stack layers rebase with gh-stack, not branch by branch
|
|
12
12
|
queueEjection?: QueueEjectionRecovery, // current merge-queue removal with failing queue checks
|
|
13
|
-
instructionStyle?: InstructionStyle
|
|
13
|
+
instructionStyle?: InstructionStyle, // see iterate.instructions
|
|
14
|
+
dequeueCommand?: string): string[];
|