pr-shepherd 0.49.0 → 0.50.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 +2 -2
- package/bin/cli/help-command-pages.d.mts +8 -8
- package/bin/cli/help-command-pages.mjs +8 -8
- package/bin/cli/help.d.mts +8 -8
- package/bin/cli/iterate-instructions.mjs +9 -2
- package/bin/cli/iterate-lean.mjs +11 -0
- package/bin/cli-parser.mjs +3 -0
- package/bin/commands/iterate/check-instructions.d.mts +2 -8
- package/bin/commands/iterate/check-instructions.mjs +2 -11
- package/bin/commands/iterate/escalate.mjs +30 -2
- package/bin/commands/iterate/fix-code.mjs +62 -20
- package/bin/commands/iterate/render.mjs +1 -1
- package/bin/commands/iterate/stall.mjs +30 -0
- package/bin/commands/iterate/thread-mutation-routing.d.mts +0 -2
- package/bin/commands/iterate/thread-mutation-routing.mjs +1 -2
- package/bin/commands/resolve-mutate.mjs +22 -68
- package/bin/comments/resolve.d.mts +5 -0
- package/bin/comments/resolve.mjs +2 -11
- package/bin/state/fix-attempts.d.mts +3 -4
- package/bin/state/fix-attempts.mjs +2 -3
- package/bin/types/escalate.d.mts +10 -0
- 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 -8
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ Conversations Resolved: No [Not Required]
|
|
|
72
72
|
2. Apply every warranted review fix in each file referenced above.
|
|
73
73
|
3. Triage every failure under `## Failing checks`. See "CI failure triage" in the pr-shepherd skill for read-only inspection rules.
|
|
74
74
|
4. If you changed code, commit any remaining changes and push to the PR head branch, then run review mutations using the pushed commit SHA and iterate immediately with the same options. If you did not change code, do not commit and continue.
|
|
75
|
-
5.
|
|
75
|
+
5. Substitute any command placeholders and run the generated review mutations.
|
|
76
76
|
6. If you did not change code, replace `$HEAD_SHA` with `$(git rev-parse HEAD)`, which must equal the current remote PR head. If you changed code, commit and push to the PR head branch first, then replace `$HEAD_SHA` with the pushed commit SHA.
|
|
77
77
|
7. Replace `$DISMISS_MESSAGE` with one sentence describing what changed.
|
|
78
78
|
8. Run the `apply review:` command shown above. See "Review-mutation mechanics" in the pr-shepherd skill for dismiss-ID retention.
|
|
@@ -92,7 +92,7 @@ This system is opinionated and works best with PRs that use required status chec
|
|
|
92
92
|
- Draft PRs can be marked ready automatically when clean; disable with `actions.autoMarkReady: false` or `--no-auto-mark-ready`.
|
|
93
93
|
- With `--merge`, actionable review threads/comments/reviews/summaries are held back (`WAIT`, with raw deferred-work counts) while a PR sits in the merge queue, since a Shepherd-initiated push would eject it; set `actions.workWhileQueued: true` to act on them immediately instead. Failing checks and merge conflicts are never deferred.
|
|
94
94
|
- The CLI never performs git mutations itself — it only emits commit/push instructions for the agent to run. Push access to the PR head is a usage precondition; GitHub viewer fields do not create a separate push-authorization handoff.
|
|
95
|
-
- Generated iterate mutations and automatic actions are capability-aware and omit unauthorized commands. Explicit `apply` operations
|
|
95
|
+
- Generated iterate mutations and automatic actions are capability-aware and omit unauthorized commands. Explicit `apply` operations forward the caller's requested IDs without iterate's author or capability policy and surface GitHub's result.
|
|
96
96
|
- `build_suggestion_patches` turns one or more ordered GitHub suggestion threads into checked patches and commit metadata, but never edits the working tree or git history. Local HEAD may be ahead when the live PR head is its ancestor.
|
|
97
97
|
|
|
98
98
|
## Usage
|
|
@@ -14,7 +14,8 @@ Run 'pr-shepherd apply <review|files|journal> --help' for command-specific detai
|
|
|
14
14
|
--help, -h Print this help and exit before GitHub I/O.`;
|
|
15
15
|
readonly "apply review": `pr-shepherd apply review
|
|
16
16
|
|
|
17
|
-
Apply GitHub review-state mutations after fixes.
|
|
17
|
+
Apply user-directed GitHub review-state mutations after fixes. Supplied IDs are forwarded to GitHub;
|
|
18
|
+
GitHub accepts or rejects each mutation.
|
|
18
19
|
|
|
19
20
|
Usage:
|
|
20
21
|
pr-shepherd apply review [PR] --reply-thread-ids A,B --message MSG
|
|
@@ -24,10 +25,9 @@ Usage:
|
|
|
24
25
|
|
|
25
26
|
Flags:
|
|
26
27
|
--resolve-thread-ids <ids> Comma-separated review thread IDs (PRRT_*) to resolve.
|
|
27
|
-
|
|
28
|
-
--reply-thread-ids <ids> Comma-separated human review thread IDs to reply to.
|
|
28
|
+
--reply-thread-ids <ids> Comma-separated review thread IDs to reply to.
|
|
29
29
|
--minimize-comment-ids <ids> Comma-separated issue/review comment IDs to minimize.
|
|
30
|
-
--dismiss-review-ids <ids> Comma-separated
|
|
30
|
+
--dismiss-review-ids <ids> Comma-separated review IDs to dismiss.
|
|
31
31
|
--message <text> Reply/dismiss message. Required with reply or dismiss IDs.
|
|
32
32
|
--require-sha <sha> Wait for this full 40-character lowercase PR head SHA.
|
|
33
33
|
--format text|json Output format. Default: text.
|
|
@@ -121,7 +121,8 @@ Use --dry-run to preview paths without removing them.
|
|
|
121
121
|
readonly "admin log-file": string;
|
|
122
122
|
readonly resolve: `pr-shepherd resolve
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
Deprecated compatibility alias for user-directed GitHub review-state mutations. Supplied IDs are forwarded to GitHub;
|
|
125
|
+
GitHub accepts or rejects each mutation.
|
|
125
126
|
|
|
126
127
|
Usage:
|
|
127
128
|
pr-shepherd resolve [PR] --reply-thread-ids A,B --message MSG
|
|
@@ -131,11 +132,10 @@ Usage:
|
|
|
131
132
|
|
|
132
133
|
Flags:
|
|
133
134
|
--resolve-thread-ids <ids> Comma-separated review thread IDs (PRRT_*) to resolve.
|
|
134
|
-
Human-authored thread IDs are skipped; use --reply-thread-ids.
|
|
135
135
|
Note: comment IDs (PRRC_*) from gh api are not thread IDs and will fail.
|
|
136
|
-
--reply-thread-ids <ids> Comma-separated
|
|
136
|
+
--reply-thread-ids <ids> Comma-separated review thread IDs to reply to.
|
|
137
137
|
--minimize-comment-ids <ids> Comma-separated issue/review comment IDs to minimize.
|
|
138
|
-
--dismiss-review-ids <ids> Comma-separated
|
|
138
|
+
--dismiss-review-ids <ids> Comma-separated review IDs to dismiss.
|
|
139
139
|
--message <text> Reply/dismiss message. Required with --reply-thread-ids
|
|
140
140
|
or --dismiss-review-ids.
|
|
141
141
|
--require-sha <sha> Wait until GitHub reports this PR head SHA before mutating.
|
|
@@ -17,7 +17,8 @@ Run 'pr-shepherd apply <review|files|journal> --help' for command-specific detai
|
|
|
17
17
|
--help, -h Print this help and exit before GitHub I/O.`,
|
|
18
18
|
"apply review": `pr-shepherd apply review
|
|
19
19
|
|
|
20
|
-
Apply GitHub review-state mutations after fixes.
|
|
20
|
+
Apply user-directed GitHub review-state mutations after fixes. Supplied IDs are forwarded to GitHub;
|
|
21
|
+
GitHub accepts or rejects each mutation.
|
|
21
22
|
|
|
22
23
|
Usage:
|
|
23
24
|
pr-shepherd apply review [PR] --reply-thread-ids A,B --message MSG
|
|
@@ -27,10 +28,9 @@ Usage:
|
|
|
27
28
|
|
|
28
29
|
Flags:
|
|
29
30
|
--resolve-thread-ids <ids> Comma-separated review thread IDs (PRRT_*) to resolve.
|
|
30
|
-
|
|
31
|
-
--reply-thread-ids <ids> Comma-separated human review thread IDs to reply to.
|
|
31
|
+
--reply-thread-ids <ids> Comma-separated review thread IDs to reply to.
|
|
32
32
|
--minimize-comment-ids <ids> Comma-separated issue/review comment IDs to minimize.
|
|
33
|
-
--dismiss-review-ids <ids> Comma-separated
|
|
33
|
+
--dismiss-review-ids <ids> Comma-separated review IDs to dismiss.
|
|
34
34
|
--message <text> Reply/dismiss message. Required with reply or dismiss IDs.
|
|
35
35
|
--require-sha <sha> Wait for this full 40-character lowercase PR head SHA.
|
|
36
36
|
--format text|json Output format. Default: text.
|
|
@@ -124,7 +124,8 @@ Use --dry-run to preview paths without removing them.
|
|
|
124
124
|
"admin log-file": LOG_FILE_USAGE.replaceAll("pr-shepherd log-file", "pr-shepherd admin log-file"),
|
|
125
125
|
resolve: `pr-shepherd resolve
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
Deprecated compatibility alias for user-directed GitHub review-state mutations. Supplied IDs are forwarded to GitHub;
|
|
128
|
+
GitHub accepts or rejects each mutation.
|
|
128
129
|
|
|
129
130
|
Usage:
|
|
130
131
|
pr-shepherd resolve [PR] --reply-thread-ids A,B --message MSG
|
|
@@ -134,11 +135,10 @@ Usage:
|
|
|
134
135
|
|
|
135
136
|
Flags:
|
|
136
137
|
--resolve-thread-ids <ids> Comma-separated review thread IDs (PRRT_*) to resolve.
|
|
137
|
-
Human-authored thread IDs are skipped; use --reply-thread-ids.
|
|
138
138
|
Note: comment IDs (PRRC_*) from gh api are not thread IDs and will fail.
|
|
139
|
-
--reply-thread-ids <ids> Comma-separated
|
|
139
|
+
--reply-thread-ids <ids> Comma-separated review thread IDs to reply to.
|
|
140
140
|
--minimize-comment-ids <ids> Comma-separated issue/review comment IDs to minimize.
|
|
141
|
-
--dismiss-review-ids <ids> Comma-separated
|
|
141
|
+
--dismiss-review-ids <ids> Comma-separated review IDs to dismiss.
|
|
142
142
|
--message <text> Reply/dismiss message. Required with --reply-thread-ids
|
|
143
143
|
or --dismiss-review-ids.
|
|
144
144
|
--require-sha <sha> Wait until GitHub reports this PR head SHA before mutating.
|
package/bin/cli/help.d.mts
CHANGED
|
@@ -14,7 +14,8 @@ Run 'pr-shepherd apply <review|files|journal> --help' for command-specific detai
|
|
|
14
14
|
--help, -h Print this help and exit before GitHub I/O.`;
|
|
15
15
|
readonly "apply review": `pr-shepherd apply review
|
|
16
16
|
|
|
17
|
-
Apply GitHub review-state mutations after fixes.
|
|
17
|
+
Apply user-directed GitHub review-state mutations after fixes. Supplied IDs are forwarded to GitHub;
|
|
18
|
+
GitHub accepts or rejects each mutation.
|
|
18
19
|
|
|
19
20
|
Usage:
|
|
20
21
|
pr-shepherd apply review [PR] --reply-thread-ids A,B --message MSG
|
|
@@ -24,10 +25,9 @@ Usage:
|
|
|
24
25
|
|
|
25
26
|
Flags:
|
|
26
27
|
--resolve-thread-ids <ids> Comma-separated review thread IDs (PRRT_*) to resolve.
|
|
27
|
-
|
|
28
|
-
--reply-thread-ids <ids> Comma-separated human review thread IDs to reply to.
|
|
28
|
+
--reply-thread-ids <ids> Comma-separated review thread IDs to reply to.
|
|
29
29
|
--minimize-comment-ids <ids> Comma-separated issue/review comment IDs to minimize.
|
|
30
|
-
--dismiss-review-ids <ids> Comma-separated
|
|
30
|
+
--dismiss-review-ids <ids> Comma-separated review IDs to dismiss.
|
|
31
31
|
--message <text> Reply/dismiss message. Required with reply or dismiss IDs.
|
|
32
32
|
--require-sha <sha> Wait for this full 40-character lowercase PR head SHA.
|
|
33
33
|
--format text|json Output format. Default: text.
|
|
@@ -121,7 +121,8 @@ Use --dry-run to preview paths without removing them.
|
|
|
121
121
|
readonly "admin log-file": string;
|
|
122
122
|
readonly resolve: `pr-shepherd resolve
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
Deprecated compatibility alias for user-directed GitHub review-state mutations. Supplied IDs are forwarded to GitHub;
|
|
125
|
+
GitHub accepts or rejects each mutation.
|
|
125
126
|
|
|
126
127
|
Usage:
|
|
127
128
|
pr-shepherd resolve [PR] --reply-thread-ids A,B --message MSG
|
|
@@ -131,11 +132,10 @@ Usage:
|
|
|
131
132
|
|
|
132
133
|
Flags:
|
|
133
134
|
--resolve-thread-ids <ids> Comma-separated review thread IDs (PRRT_*) to resolve.
|
|
134
|
-
Human-authored thread IDs are skipped; use --reply-thread-ids.
|
|
135
135
|
Note: comment IDs (PRRC_*) from gh api are not thread IDs and will fail.
|
|
136
|
-
--reply-thread-ids <ids> Comma-separated
|
|
136
|
+
--reply-thread-ids <ids> Comma-separated review thread IDs to reply to.
|
|
137
137
|
--minimize-comment-ids <ids> Comma-separated issue/review comment IDs to minimize.
|
|
138
|
-
--dismiss-review-ids <ids> Comma-separated
|
|
138
|
+
--dismiss-review-ids <ids> Comma-separated review IDs to dismiss.
|
|
139
139
|
--message <text> Reply/dismiss message. Required with --reply-thread-ids
|
|
140
140
|
or --dismiss-review-ids.
|
|
141
141
|
--require-sha <sha> Wait until GitHub reports this PR head SHA before mutating.
|
|
@@ -38,8 +38,15 @@ export function buildSimpleIterateInstructions(result) {
|
|
|
38
38
|
}
|
|
39
39
|
case "cancel":
|
|
40
40
|
return ["Stop — the PR loop is complete. No further polling is needed."];
|
|
41
|
-
case "escalate":
|
|
42
|
-
|
|
41
|
+
case "escalate": {
|
|
42
|
+
const pending = result.escalate.pendingReviewCommands;
|
|
43
|
+
if (!pending)
|
|
44
|
+
return ["Stop — human direction is required before automated polling can resume."];
|
|
45
|
+
return [
|
|
46
|
+
"Stop polling. Ask the user whether to run the pending review commands shown above.",
|
|
47
|
+
"If yes, replace any `$HEAD_SHA` with the full 40-character pushed PR-head SHA and any `$DISMISS_MESSAGE` with a one-sentence disposition, run every pending command, then rerun Shepherd with the same options.",
|
|
48
|
+
];
|
|
49
|
+
}
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
52
|
export function adaptIterateLog(log) {
|
package/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -160,6 +160,14 @@ export function projectIterateLean(result, opts) {
|
|
|
160
160
|
...(result.escalate.changesRequestedReviews.length > 0 && {
|
|
161
161
|
changesRequestedReviews: result.escalate.changesRequestedReviews,
|
|
162
162
|
}),
|
|
163
|
+
...(result.escalate.firstLookSummaries &&
|
|
164
|
+
result.escalate.firstLookSummaries.length > 0 && {
|
|
165
|
+
firstLookSummaries: result.escalate.firstLookSummaries,
|
|
166
|
+
}),
|
|
167
|
+
...(result.escalate.editedSummaries &&
|
|
168
|
+
result.escalate.editedSummaries.length > 0 && {
|
|
169
|
+
editedSummaries: result.escalate.editedSummaries,
|
|
170
|
+
}),
|
|
163
171
|
...(result.escalate.checks &&
|
|
164
172
|
result.escalate.checks.length > 0 && {
|
|
165
173
|
checks: result.escalate.checks,
|
|
@@ -182,6 +190,9 @@ export function projectIterateLean(result, opts) {
|
|
|
182
190
|
result.escalate.authorization.length > 0 && {
|
|
183
191
|
authorization: result.escalate.authorization,
|
|
184
192
|
}),
|
|
193
|
+
...(result.escalate.pendingReviewCommands && {
|
|
194
|
+
pendingReviewCommands: result.escalate.pendingReviewCommands,
|
|
195
|
+
}),
|
|
185
196
|
suggestion: result.escalate.suggestion,
|
|
186
197
|
humanMessage: result.escalate.humanMessage,
|
|
187
198
|
},
|
package/bin/cli-parser.mjs
CHANGED
|
@@ -213,4 +213,7 @@ async function handleResolve(args, command = "apply review") {
|
|
|
213
213
|
process.stdout.write(globalOpts.format === "json"
|
|
214
214
|
? `${JSON.stringify(result, null, 2)}\n`
|
|
215
215
|
: `${formatMutateResult(result)}\n`);
|
|
216
|
+
if (result.errors.length > 0) {
|
|
217
|
+
process.exitCode = result.rateLimit ? EXIT.TEMPFAIL : EXIT.UNAVAILABLE;
|
|
218
|
+
}
|
|
216
219
|
}
|
|
@@ -26,15 +26,9 @@ export declare function buildRepeatedWorkflowBranchRecoveryInstructions(baseBran
|
|
|
26
26
|
*
|
|
27
27
|
* - `$HEAD_SHA`/`$DISMISS_MESSAGE` substitution: without it, the printed command has an
|
|
28
28
|
* empty `--message`/invalid `--require-sha` and `apply review` rejects the mutation.
|
|
29
|
-
* Marker-based self-reply routing is already reflected in the generated IDs. The instruction
|
|
30
|
-
* below makes that behavior explicit so an authenticated viewer's unmarked human feedback is
|
|
31
|
-
* not mistaken for an automated reply merely because the GitHub login matches.
|
|
32
|
-
*
|
|
33
29
|
* Contrast with what *does* stay in the skill's "Review-mutation mechanics" playbook —
|
|
34
|
-
* dismiss-ID retention
|
|
35
|
-
*
|
|
36
|
-
* command run unmodified is already correct for them. The pointer below is load-bearing:
|
|
37
|
-
* without it, nothing in CLI output tells the agent that playbook exists.
|
|
30
|
+
* dismiss-ID retention. The pointer below is load-bearing: without it, nothing in CLI output
|
|
31
|
+
* tells the agent that playbook exists.
|
|
38
32
|
*/
|
|
39
33
|
export declare function buildResolveCommandInstruction(resolveCommand: ResolveCommand): string[];
|
|
40
34
|
/** Build the CI-triage pointer; the skill limits follow-up actions to included evidence. */
|
|
@@ -43,23 +43,14 @@ export function buildRepeatedWorkflowBranchRecoveryInstructions(baseBranch, hasE
|
|
|
43
43
|
*
|
|
44
44
|
* - `$HEAD_SHA`/`$DISMISS_MESSAGE` substitution: without it, the printed command has an
|
|
45
45
|
* empty `--message`/invalid `--require-sha` and `apply review` rejects the mutation.
|
|
46
|
-
* Marker-based self-reply routing is already reflected in the generated IDs. The instruction
|
|
47
|
-
* below makes that behavior explicit so an authenticated viewer's unmarked human feedback is
|
|
48
|
-
* not mistaken for an automated reply merely because the GitHub login matches.
|
|
49
|
-
*
|
|
50
46
|
* Contrast with what *does* stay in the skill's "Review-mutation mechanics" playbook —
|
|
51
|
-
* dismiss-ID retention
|
|
52
|
-
*
|
|
53
|
-
* command run unmodified is already correct for them. The pointer below is load-bearing:
|
|
54
|
-
* without it, nothing in CLI output tells the agent that playbook exists.
|
|
47
|
+
* dismiss-ID retention. The pointer below is load-bearing: without it, nothing in CLI output
|
|
48
|
+
* tells the agent that playbook exists.
|
|
55
49
|
*/
|
|
56
50
|
export function buildResolveCommandInstruction(resolveCommand) {
|
|
57
51
|
if (!resolveCommand.hasMutations)
|
|
58
52
|
return [];
|
|
59
53
|
const instructions = [];
|
|
60
|
-
if ((resolveCommand.replyThreadIds?.length ?? 0) > 0) {
|
|
61
|
-
instructions.push("Run the generated thread IDs unchanged. A latest comment beginning `<!-- pr-shepherd -->` is an established Shepherd reply; a marked thread that is still being resolved is emitted resolve-only, not for another reply.");
|
|
62
|
-
}
|
|
63
54
|
if (resolveCommand.requiresHeadSha) {
|
|
64
55
|
instructions.push("If you did not change code, replace `$HEAD_SHA` with `$(git rev-parse HEAD)`, which must equal the current remote PR head. If you changed code, commit and push to the PR head branch first, then replace `$HEAD_SHA` with the pushed commit SHA.");
|
|
65
56
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadConfig } from "../../config/load.mjs";
|
|
2
2
|
import { inlineCode } from "../../util/markdown.mjs";
|
|
3
|
+
import { renderResolveCommand } from "./render.mjs";
|
|
3
4
|
function renderEscalateAuthor(item) {
|
|
4
5
|
return [`@${item.author}`, item.authorType, item.authorAssociation].filter(Boolean).join(" · ");
|
|
5
6
|
}
|
|
@@ -121,6 +122,8 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
|
|
|
121
122
|
const hasItems = escalate.unresolvedThreads.length > 0 ||
|
|
122
123
|
escalate.changesRequestedReviews.length > 0 ||
|
|
123
124
|
escalate.ambiguousComments.length > 0 ||
|
|
125
|
+
(escalate.firstLookSummaries?.length ?? 0) > 0 ||
|
|
126
|
+
(escalate.editedSummaries?.length ?? 0) > 0 ||
|
|
124
127
|
(escalate.checks?.length ?? 0) > 0 ||
|
|
125
128
|
(escalate.stalledChecks?.length ?? 0) > 0;
|
|
126
129
|
if (hasItems) {
|
|
@@ -157,6 +160,20 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
|
|
|
157
160
|
lines.push(` > ${bodyLine}`);
|
|
158
161
|
lines.push("");
|
|
159
162
|
}
|
|
163
|
+
for (const [heading, summaries] of [
|
|
164
|
+
["Review summaries (first look)", escalate.firstLookSummaries ?? []],
|
|
165
|
+
["Review summaries (edited since first look)", escalate.editedSummaries ?? []],
|
|
166
|
+
]) {
|
|
167
|
+
if (summaries.length === 0)
|
|
168
|
+
continue;
|
|
169
|
+
lines.push(`### ${heading}`, "");
|
|
170
|
+
for (const summary of summaries) {
|
|
171
|
+
lines.push(`- review \`${summary.id}\` (${renderEscalateAuthor(summary)}):`, "");
|
|
172
|
+
for (const bodyLine of summary.body.split("\n"))
|
|
173
|
+
lines.push(` > ${bodyLine}`);
|
|
174
|
+
lines.push("");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
160
177
|
for (const c of escalate.ambiguousComments) {
|
|
161
178
|
lines.push(`- comment \`${c.id}\` (${renderEscalateAuthor(c)}):`);
|
|
162
179
|
lines.push("");
|
|
@@ -194,7 +211,17 @@ export function buildEscalateHumanMessage(escalate, pr, opts) {
|
|
|
194
211
|
lines.push("## Fix attempts");
|
|
195
212
|
lines.push("");
|
|
196
213
|
for (const a of escalate.thrashHistory) {
|
|
197
|
-
lines.push(`- thread \`${a.threadId}\`
|
|
214
|
+
lines.push(`- thread \`${a.threadId}\` pending commands returned ${a.attempts} times`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const pending = escalate.pendingReviewCommands;
|
|
218
|
+
if (pending?.resolveOnlyCommand?.hasMutations || pending?.resolveCommand?.hasMutations) {
|
|
219
|
+
lines.push("", "## Pending review commands", "");
|
|
220
|
+
if (pending.resolveOnlyCommand?.hasMutations) {
|
|
221
|
+
lines.push(`- resolve-only: \`${renderResolveCommand(pending.resolveOnlyCommand)}\``);
|
|
222
|
+
}
|
|
223
|
+
if (pending.resolveCommand?.hasMutations) {
|
|
224
|
+
lines.push(`- apply review: \`${renderResolveCommand(pending.resolveCommand)}\``);
|
|
198
225
|
}
|
|
199
226
|
}
|
|
200
227
|
lines.push("");
|
|
@@ -227,7 +254,8 @@ export function buildEscalateSuggestion(triggers, detail) {
|
|
|
227
254
|
return `Could not determine the PR's base branch${reason} — automated rebases are paused because branch safety is unclear. Run the rebase manually against the PR's real target branch.`;
|
|
228
255
|
}
|
|
229
256
|
if (triggers.includes("fix-thrash")) {
|
|
230
|
-
|
|
257
|
+
const attempts = loadConfig().iterate.fixAttemptsPerThread;
|
|
258
|
+
return `The same thread(s) remain unresolved after their pending review commands were returned for ${attempts} FIX_CODE ticks. Automated iteration is paused for a manual decision.`;
|
|
231
259
|
}
|
|
232
260
|
if (triggers.includes("bot-cr-not-dismissed")) {
|
|
233
261
|
const ids = detail ? ` (review IDs: ${detail})` : "";
|
|
@@ -33,7 +33,7 @@ function checkRequiresHumanFollowUp(check) {
|
|
|
33
33
|
return !check.detailsUrl?.trim();
|
|
34
34
|
return !check.logExcerpt?.trim();
|
|
35
35
|
}
|
|
36
|
-
function nextFixAttempts(stored,
|
|
36
|
+
function nextFixAttempts(stored, threads, countAttempt) {
|
|
37
37
|
const threadAttempts = stored ? { ...stored.threadAttempts } : {};
|
|
38
38
|
const threadBodyHashes = stored?.threadBodyHashes
|
|
39
39
|
? { ...stored.threadBodyHashes }
|
|
@@ -41,13 +41,32 @@ function nextFixAttempts(stored, headSha, threads) {
|
|
|
41
41
|
for (const t of threads) {
|
|
42
42
|
const bodyHash = hashBody(threadTranscriptBody(t));
|
|
43
43
|
const previousHash = threadBodyHashes[t.id];
|
|
44
|
-
if (
|
|
44
|
+
if (!countAttempt)
|
|
45
45
|
continue;
|
|
46
46
|
threadAttempts[t.id] = previousHash === bodyHash ? (threadAttempts[t.id] ?? 0) + 1 : 1;
|
|
47
47
|
threadBodyHashes[t.id] = bodyHash;
|
|
48
48
|
}
|
|
49
49
|
return { threadAttempts, threadBodyHashes };
|
|
50
50
|
}
|
|
51
|
+
function previousFixAttempts(stored, threads) {
|
|
52
|
+
if (!stored?.threadBodyHashes)
|
|
53
|
+
return {};
|
|
54
|
+
const attempts = {};
|
|
55
|
+
for (const thread of threads) {
|
|
56
|
+
const bodyHash = hashBody(threadTranscriptBody(thread));
|
|
57
|
+
if (stored.threadBodyHashes[thread.id] === bodyHash) {
|
|
58
|
+
attempts[thread.id] = stored.threadAttempts[thread.id] ?? 0;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return attempts;
|
|
62
|
+
}
|
|
63
|
+
function pendingReviewCommands(resolveCommand, resolveOnlyCommand) {
|
|
64
|
+
const pending = {
|
|
65
|
+
...(resolveOnlyCommand?.hasMutations && { resolveOnlyCommand }),
|
|
66
|
+
...(resolveCommand.hasMutations && { resolveCommand }),
|
|
67
|
+
};
|
|
68
|
+
return Object.keys(pending).length > 0 ? pending : undefined;
|
|
69
|
+
}
|
|
51
70
|
export async function handleFixCode(ctx) {
|
|
52
71
|
const { base, report, opts, headSha, stallKey, prNumber, stallTimeoutSeconds, repoOwner, repoName, reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, botUsernames, ruleAutoResolveThreadIds, } = ctx;
|
|
53
72
|
const prReference = formatPrUrl(report.repo, prNumber);
|
|
@@ -70,7 +89,22 @@ export async function handleFixCode(ctx) {
|
|
|
70
89
|
const retryableActionableThreads = mutationActionableThreads.filter((thread) => thread.path !== null && thread.line !== null);
|
|
71
90
|
const protectedRuns = [];
|
|
72
91
|
const stored = await readFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber });
|
|
73
|
-
const
|
|
92
|
+
const countFixCodeAttempt = opts.persistSeen !== false;
|
|
93
|
+
const priorThreadAttempts = previousFixAttempts(stored, retryableActionableThreads);
|
|
94
|
+
const { threadAttempts, threadBodyHashes } = nextFixAttempts(stored, retryableActionableThreads, countFixCodeAttempt);
|
|
95
|
+
const resolutionOnlyThreadsForWork = report.threads.resolutionOnly.filter((thread) => !skippedThreadIds.has(thread.id) &&
|
|
96
|
+
((thread.path !== null && thread.line !== null) ||
|
|
97
|
+
threadHasAuthorizedMutation(thread, replyIdSet, resolveIdSet)));
|
|
98
|
+
const actionableChangesRequestedReviews = report.changesRequestedReviews.filter((review) => review.staleReview !== true ||
|
|
99
|
+
!isHumanAuthor(review) ||
|
|
100
|
+
isConfiguredBotAuthor(review, botUsernames));
|
|
101
|
+
const changesRequestedReviewsForWork = actionableChangesRequestedReviews.filter((review) => !unauthorizedDismissals.some((candidate) => candidate.id === review.id));
|
|
102
|
+
const buildReviewCommands = (checks) => buildResolveCommand(report.threads.actionable
|
|
103
|
+
.filter((thread) => mutationActionableThreads.some((candidate) => candidate.id === thread.id))
|
|
104
|
+
.map(toAgentThread), resolutionOnlyThreadsForWork, [
|
|
105
|
+
...(report.comments.minimizeIds ?? report.comments.actionable.map((comment) => comment.id)),
|
|
106
|
+
...reviewSummaryIds,
|
|
107
|
+
], changesRequestedReviewsForWork, checks, prReference, botUsernames, ruleAutoResolveThreadIds, report.viewerAuthorization, allThreads, resolveOtherHumanThreads);
|
|
74
108
|
const botCrReviews = report.changesRequestedReviews.filter((r) => (!isHumanAuthor(r) || isConfiguredBotAuthor(r, botUsernames)) &&
|
|
75
109
|
report.viewerAuthorization?.viewerCanAdminister === true);
|
|
76
110
|
const botCrStateKey = { owner: repoOwner, repo: repoName, pr: prNumber };
|
|
@@ -79,13 +113,16 @@ export async function handleFixCode(ctx) {
|
|
|
79
113
|
const { next: nextBotCrState, staleIds: staleBotCrIds } = updateBotCrSeenState(previousBotCrState, botCrReviews, nowSeconds, stallTimeoutSeconds);
|
|
80
114
|
await writeBotCrSeenState(botCrStateKey, nextBotCrState);
|
|
81
115
|
if (staleBotCrIds.length > 0) {
|
|
82
|
-
const
|
|
83
|
-
const
|
|
116
|
+
const { resolveCommand, resolveOnlyCommand } = buildReviewCommands(toAgentChecks(failingChecks));
|
|
117
|
+
const pending = pendingReviewCommands(resolveCommand, resolveOnlyCommand);
|
|
84
118
|
const escalateBase = {
|
|
85
119
|
triggers: ["bot-cr-not-dismissed"],
|
|
86
120
|
unresolvedThreads: [...report.threads.actionable, ...report.threads.resolutionOnly].map(toAgentThread),
|
|
87
121
|
ambiguousComments: report.comments.actionable.map(toAgentComment),
|
|
88
|
-
changesRequestedReviews:
|
|
122
|
+
changesRequestedReviews: report.changesRequestedReviews,
|
|
123
|
+
...(firstLookSummaries.length > 0 && { firstLookSummaries }),
|
|
124
|
+
...(editedSummaries.length > 0 && { editedSummaries }),
|
|
125
|
+
...(pending && { pendingReviewCommands: pending }),
|
|
89
126
|
suggestion: buildEscalateSuggestion(["bot-cr-not-dismissed"], staleBotCrIds.join(", ")),
|
|
90
127
|
};
|
|
91
128
|
return {
|
|
@@ -99,14 +136,21 @@ export async function handleFixCode(ctx) {
|
|
|
99
136
|
},
|
|
100
137
|
};
|
|
101
138
|
}
|
|
102
|
-
const escalateTriggers =
|
|
139
|
+
const escalateTriggers = countFixCodeAttempt
|
|
140
|
+
? checkEscalateTriggers(retryableActionableThreads, priorThreadAttempts)
|
|
141
|
+
: { triggers: [], thrashHistory: undefined };
|
|
103
142
|
if (escalateTriggers.triggers.length > 0) {
|
|
143
|
+
const { resolveCommand, resolveOnlyCommand } = buildReviewCommands(toAgentChecks(failingChecks));
|
|
144
|
+
const pending = pendingReviewCommands(resolveCommand, resolveOnlyCommand);
|
|
104
145
|
const escalateBase = {
|
|
105
146
|
triggers: escalateTriggers.triggers,
|
|
106
147
|
unresolvedThreads: [...report.threads.actionable, ...report.threads.resolutionOnly].map(toAgentThread),
|
|
107
148
|
ambiguousComments: report.comments.actionable.map(toAgentComment),
|
|
108
149
|
changesRequestedReviews: report.changesRequestedReviews,
|
|
150
|
+
...(firstLookSummaries.length > 0 && { firstLookSummaries }),
|
|
151
|
+
...(editedSummaries.length > 0 && { editedSummaries }),
|
|
109
152
|
thrashHistory: escalateTriggers.thrashHistory,
|
|
153
|
+
...(pending && { pendingReviewCommands: pending }),
|
|
110
154
|
suggestion: buildEscalateSuggestion(escalateTriggers.triggers),
|
|
111
155
|
};
|
|
112
156
|
return {
|
|
@@ -120,7 +164,6 @@ export async function handleFixCode(ctx) {
|
|
|
120
164
|
},
|
|
121
165
|
};
|
|
122
166
|
}
|
|
123
|
-
await writeFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber }, { headSha, threadAttempts, threadBodyHashes });
|
|
124
167
|
// GitHub does not expose a per-run viewer capability for cancellation, so Shepherd never
|
|
125
168
|
// issues or recommends a cancellation regardless of repository role. A rerun is different:
|
|
126
169
|
// GitHub's Actions rerun API requires actions:write, which rides with WRITE+ repo access, so
|
|
@@ -163,14 +206,6 @@ export async function handleFixCode(ctx) {
|
|
|
163
206
|
...toAgentChecks(annotatedExtra).map((c) => ({ ...c, annotationOnly: true })),
|
|
164
207
|
];
|
|
165
208
|
const { changesRequestedReviews } = report;
|
|
166
|
-
const actionableChangesRequestedReviews = changesRequestedReviews.filter((review) => review.staleReview !== true ||
|
|
167
|
-
!isHumanAuthor(review) ||
|
|
168
|
-
isConfiguredBotAuthor(review, botUsernames));
|
|
169
|
-
const skippedDismissalIds = new Set(unauthorizedDismissals.map((review) => review.id));
|
|
170
|
-
const changesRequestedReviewsForWork = actionableChangesRequestedReviews.filter((review) => !skippedDismissalIds.has(review.id));
|
|
171
|
-
const resolutionOnlyThreadsForWork = resolutionOnlyThreads.filter((thread) => !skippedThreadIds.has(thread.id) &&
|
|
172
|
-
((thread.path !== null && thread.line !== null) ||
|
|
173
|
-
threadHasAuthorizedMutation(thread, replyIdSet, resolveIdSet)));
|
|
174
209
|
const hasConflicts = report.mergeStatus.status === "CONFLICTS";
|
|
175
210
|
const isBehind = report.mergeStatus.status === "BEHIND";
|
|
176
211
|
const { behindBaseHint } = loadConfig().iterate;
|
|
@@ -179,7 +214,6 @@ export async function handleFixCode(ctx) {
|
|
|
179
214
|
// unnecessary cancellation.
|
|
180
215
|
const inProgressRunIds = [];
|
|
181
216
|
const commentMinimizeIds = report.comments.minimizeIds ?? actionableComments.map((c) => c.id);
|
|
182
|
-
const allCommentIds = [...commentMinimizeIds, ...reviewSummaryIds];
|
|
183
217
|
const belongsToActiveWorkflowRun = (check) => check.runId !== null && inProgressWorkflowRunIds.has(check.runId);
|
|
184
218
|
const manualFollowUpChecks = failingAgentChecks.filter((check) => !belongsToActiveWorkflowRun(check) && checkRequiresHumanFollowUp(check));
|
|
185
219
|
const exhaustedAttempts = manualFollowUpChecks.filter((check) => check.runAttempt !== undefined && check.runAttempt > 1);
|
|
@@ -199,6 +233,8 @@ export async function handleFixCode(ctx) {
|
|
|
199
233
|
checks.some((check) => (check.annotations?.length ?? 0) > 0) ||
|
|
200
234
|
failingAgentChecks.some((check) => belongsToActiveWorkflowRun(check) || !checkRequiresHumanFollowUp(check));
|
|
201
235
|
if (manualFollowUpChecks.length > 0 && !hasAutonomousWork) {
|
|
236
|
+
const { resolveCommand, resolveOnlyCommand } = buildReviewCommands(failingAgentChecks);
|
|
237
|
+
const pending = pendingReviewCommands(resolveCommand, resolveOnlyCommand);
|
|
202
238
|
const checkSuggestion = exhaustedAttempts.length > 0
|
|
203
239
|
? `GitHub reports a later workflow attempt (${exhaustedAttempts
|
|
204
240
|
.map((check) => `${check.runId ?? check.name}: attempt ${check.runAttempt}`)
|
|
@@ -209,7 +245,10 @@ export async function handleFixCode(ctx) {
|
|
|
209
245
|
unresolvedThreads: [],
|
|
210
246
|
ambiguousComments: [],
|
|
211
247
|
changesRequestedReviews,
|
|
248
|
+
...(firstLookSummaries.length > 0 && { firstLookSummaries }),
|
|
249
|
+
...(editedSummaries.length > 0 && { editedSummaries }),
|
|
212
250
|
checks: manualFollowUpChecks,
|
|
251
|
+
...(pending && { pendingReviewCommands: pending }),
|
|
213
252
|
suggestion: checkSuggestion,
|
|
214
253
|
};
|
|
215
254
|
return {
|
|
@@ -225,9 +264,7 @@ export async function handleFixCode(ctx) {
|
|
|
225
264
|
}
|
|
226
265
|
// Push access to the PR head branch is a usage precondition. Build review mutations for
|
|
227
266
|
// conflict ticks normally so the caller can push and complete the same fix_code cycle.
|
|
228
|
-
const
|
|
229
|
-
const mutationAgentThreads = threads.filter((thread) => mutationActionableIds.has(thread.id));
|
|
230
|
-
const { resolveCommand, resolveOnlyCommand } = buildResolveCommand(mutationAgentThreads, resolutionOnlyThreadsForWork, allCommentIds, changesRequestedReviewsForWork, failingAgentChecks, prReference, botUsernames, ruleAutoResolveThreadIds, report.viewerAuthorization, allThreads, resolveOtherHumanThreads);
|
|
267
|
+
const { resolveCommand, resolveOnlyCommand } = buildReviewCommands(failingAgentChecks);
|
|
231
268
|
// Safety: if the base branch is unknown, escalate when a push is plausible — the agent
|
|
232
269
|
// would need the correct base to rebase safely. This is a conservative guard, not a
|
|
233
270
|
// prediction that the agent *will* push. Located resolution-only threads retain that guard;
|
|
@@ -241,11 +278,15 @@ export async function handleFixCode(ctx) {
|
|
|
241
278
|
actionableComments.length > 0 ||
|
|
242
279
|
locatedResolutionOnlyThreadsForWork.length > 0;
|
|
243
280
|
if (baseLookup.isFallback && pushIsPlausible) {
|
|
281
|
+
const pending = pendingReviewCommands(resolveCommand, resolveOnlyCommand);
|
|
244
282
|
const fallbackEscalateBase = {
|
|
245
283
|
triggers: ["base-branch-unknown"],
|
|
246
284
|
unresolvedThreads: [...threads, ...resolutionOnlyThreads.map(toAgentThread)],
|
|
247
285
|
ambiguousComments: actionableComments,
|
|
248
286
|
changesRequestedReviews,
|
|
287
|
+
...(firstLookSummaries.length > 0 && { firstLookSummaries }),
|
|
288
|
+
...(editedSummaries.length > 0 && { editedSummaries }),
|
|
289
|
+
...(pending && { pendingReviewCommands: pending }),
|
|
249
290
|
suggestion: buildEscalateSuggestion(["base-branch-unknown"], baseLookup.failureReason),
|
|
250
291
|
};
|
|
251
292
|
return {
|
|
@@ -288,6 +329,7 @@ export async function handleFixCode(ctx) {
|
|
|
288
329
|
};
|
|
289
330
|
const result = await applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, prospectiveResult, report, reviewSummaryIds);
|
|
290
331
|
if (result.action === "fix_code" && opts.persistSeen !== false) {
|
|
332
|
+
await writeFixAttempts({ owner: repoOwner, repo: repoName, pr: prNumber }, { headSha, threadAttempts, threadBodyHashes });
|
|
291
333
|
await Promise.allSettled(result.fix.checks.flatMap((ch) => (ch.annotations ?? []).map((a) => markSeen(stallKey, a.id, annotationMarkerBody(a)))));
|
|
292
334
|
}
|
|
293
335
|
return result;
|
|
@@ -69,7 +69,7 @@ isBehind = false, viewerCanUpdate = false, hasExhaustedWorkflowRerun = false) {
|
|
|
69
69
|
instructions.push(`Apply every warranted review fix in ${filesRef}.`);
|
|
70
70
|
}
|
|
71
71
|
if (resolutionOnlyThreads.length > 0) {
|
|
72
|
-
instructions.push(
|
|
72
|
+
instructions.push("Review the threads under `## Review threads to resolve` before running the generated mutations.");
|
|
73
73
|
}
|
|
74
74
|
instructions.push(...buildFailingCheckInstructions(failingChecks), ...repeatedWorkflowBranchRecoveryInstructions);
|
|
75
75
|
if (hasAnnotations) {
|
|
@@ -1,8 +1,32 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
import { readStallState, writeStallState } from "../../state/iterate-stall.mjs";
|
|
2
3
|
import { toAgentThread, toAgentComment, toAgentStalledCheck } from "../../reporters/agent.mjs";
|
|
3
4
|
import { buildEscalateSuggestion, buildEscalateHumanMessage, formatDurationApprox, } from "./escalate.mjs";
|
|
4
5
|
import { checksWithActionableAnnotations } from "../check-annotations.mjs";
|
|
5
6
|
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
7
|
+
function pendingReviewCommandsFromResult(result) {
|
|
8
|
+
if (result.action !== "fix_code")
|
|
9
|
+
return undefined;
|
|
10
|
+
const pending = {
|
|
11
|
+
...(result.fix.resolveOnlyCommand?.hasMutations && {
|
|
12
|
+
resolveOnlyCommand: result.fix.resolveOnlyCommand,
|
|
13
|
+
}),
|
|
14
|
+
...(result.fix.resolveCommand.hasMutations && { resolveCommand: result.fix.resolveCommand }),
|
|
15
|
+
};
|
|
16
|
+
return Object.keys(pending).length > 0 ? pending : undefined;
|
|
17
|
+
}
|
|
18
|
+
function surfacedSummariesFromResult(result) {
|
|
19
|
+
if (result.action !== "fix_code")
|
|
20
|
+
return {};
|
|
21
|
+
return {
|
|
22
|
+
...(result.fix.firstLookSummaries.length > 0 && {
|
|
23
|
+
firstLookSummaries: result.fix.firstLookSummaries,
|
|
24
|
+
}),
|
|
25
|
+
...(result.fix.editedSummaries.length > 0 && {
|
|
26
|
+
editedSummaries: result.fix.editedSummaries,
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
6
30
|
function computeStallFingerprint(action, headSha, base, report, reviewSummaryIds) {
|
|
7
31
|
const checks = [
|
|
8
32
|
...report.checks.failing.map((f) => `failing:${f.name}:${f.conclusion}:${f.runId ?? "no-run"}:${f.runAttempt ?? "unknown"}`),
|
|
@@ -44,13 +68,16 @@ export async function applyStallGuard(stallKey, stallTimeoutSeconds, headSha, ba
|
|
|
44
68
|
action: prospectiveResult.action,
|
|
45
69
|
});
|
|
46
70
|
if (stalledChecks.length > 0) {
|
|
71
|
+
const pending = pendingReviewCommandsFromResult(prospectiveResult);
|
|
47
72
|
const stalledDuration = formatDurationApprox(Math.max(...stalledChecks.map((c) => c.ageSeconds)));
|
|
48
73
|
const escalateBase = {
|
|
49
74
|
triggers: ["stall-timeout"],
|
|
50
75
|
unresolvedThreads: [],
|
|
51
76
|
ambiguousComments: [],
|
|
52
77
|
changesRequestedReviews: [],
|
|
78
|
+
...surfacedSummariesFromResult(prospectiveResult),
|
|
53
79
|
stalledChecks,
|
|
80
|
+
...(pending && { pendingReviewCommands: pending }),
|
|
54
81
|
suggestion: buildEscalateSuggestion(["stall-timeout"], stalledDuration),
|
|
55
82
|
};
|
|
56
83
|
return {
|
|
@@ -76,11 +103,14 @@ export async function applyStallGuard(stallKey, stallTimeoutSeconds, headSha, ba
|
|
|
76
103
|
}
|
|
77
104
|
else if (ageSeconds >= stallTimeoutSeconds) {
|
|
78
105
|
const stalledDuration = formatDurationApprox(ageSeconds);
|
|
106
|
+
const pending = pendingReviewCommandsFromResult(prospectiveResult);
|
|
79
107
|
const escalateBase = {
|
|
80
108
|
triggers: ["stall-timeout"],
|
|
81
109
|
unresolvedThreads: [...report.threads.actionable, ...report.threads.resolutionOnly].map(toAgentThread),
|
|
82
110
|
ambiguousComments: report.comments.actionable.map(toAgentComment),
|
|
83
111
|
changesRequestedReviews: report.changesRequestedReviews,
|
|
112
|
+
...surfacedSummariesFromResult(prospectiveResult),
|
|
113
|
+
...(pending && { pendingReviewCommands: pending }),
|
|
84
114
|
suggestion: buildEscalateSuggestion(["stall-timeout"], stalledDuration),
|
|
85
115
|
};
|
|
86
116
|
return {
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { type NormalizedBotUsernames } from "../../comments/authors.mts";
|
|
2
|
-
import { shouldResolveOtherHumanThread } from "../../comments/thread-resolve-policy.mts";
|
|
3
2
|
import type { ResolveOtherHumanThreads } from "../../config/load.mts";
|
|
4
3
|
import type { AgentThread, ReviewThread } from "../../types.mts";
|
|
5
|
-
export { shouldResolveOtherHumanThread };
|
|
6
4
|
export type RoutableThread = AgentThread | ReviewThread;
|
|
7
5
|
export interface ThreadMutationRouting {
|
|
8
6
|
replyThreadIds: string[];
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { isConfiguredBotAuthor, isHumanAuthor, isViewerAuthoredHuman, } from "../../comments/authors.mjs";
|
|
2
2
|
import { threadEndedByShepherd } from "../../comments/marker.mjs";
|
|
3
3
|
import { shouldResolveOtherHumanThread } from "../../comments/thread-resolve-policy.mjs";
|
|
4
|
-
export { shouldResolveOtherHumanThread };
|
|
5
4
|
function dedupeIds(ids) {
|
|
6
5
|
return [...new Set(ids)];
|
|
7
6
|
}
|
|
@@ -32,7 +31,7 @@ export function buildThreadMutationRouting(threads, botUsernames, ruleAutoResolv
|
|
|
32
31
|
.filter((thread) => shouldPairResolve(thread, botUsernames, policy) && !threadEndedByShepherd(thread))
|
|
33
32
|
.map((thread) => thread.id));
|
|
34
33
|
const pairedResolveIdSet = new Set(pairedResolveThreadIds);
|
|
35
|
-
// Rule-matched threads bypass
|
|
34
|
+
// Rule-matched threads bypass the generated-command author routing.
|
|
36
35
|
const standaloneResolveThreadIds = dedupeIds([
|
|
37
36
|
...threads
|
|
38
37
|
.filter((thread) => shouldPairResolve(thread, botUsernames, policy) && threadEndedByShepherd(thread))
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { getRepoInfo, getCurrentPrNumber } from "../github/client.mjs";
|
|
2
2
|
import { applyResolveOptions } from "../comments/resolve.mjs";
|
|
3
3
|
import { fetchPrBatch } from "../github/batch.mjs";
|
|
4
|
-
import { loadConfig } from "../config/load.mjs";
|
|
5
|
-
import { isConfiguredBotAuthor, isHumanAuthor, isViewerAuthoredHuman, normalizeBotUsernames, } from "../comments/authors.mjs";
|
|
6
|
-
import { shouldResolveOtherHumanThread } from "./iterate/thread-mutation-routing.mjs";
|
|
7
4
|
import { markReplySeen } from "../state/seen-comments.mjs";
|
|
8
5
|
import { threadTranscriptBody } from "../threads/transcript.mjs";
|
|
9
|
-
import { addPrShepherdMarker
|
|
6
|
+
import { addPrShepherdMarker } from "../comments/marker.mjs";
|
|
10
7
|
import { EXIT, ShepherdError } from "../exit-codes.mjs";
|
|
11
8
|
/** @deprecated Hidden implementation for `resolve`; use `apply review`. */
|
|
12
9
|
export async function runResolveMutate(opts) {
|
|
@@ -15,73 +12,30 @@ export async function runResolveMutate(opts) {
|
|
|
15
12
|
if (prNumber === null) {
|
|
16
13
|
throw new ShepherdError("No open PR found for current branch. Pass a PR number explicitly.", EXIT.UNAVAILABLE);
|
|
17
14
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// authoritative and this path must not second-guess that intent.
|
|
34
|
-
const requestedReplyIds = new Set(opts.replyThreadIds ?? []);
|
|
35
|
-
const policy = config.iterate?.resolveOtherHumanThreads ?? "none";
|
|
36
|
-
const allowedHumanResolveIds = new Set(data.reviewThreads
|
|
37
|
-
.filter((thread) => {
|
|
38
|
-
if (!humanThreadIds.has(thread.id))
|
|
39
|
-
return false;
|
|
40
|
-
const paired = requestedReplyIds.has(thread.id) || threadEndedByShepherd(thread);
|
|
41
|
-
if (!paired)
|
|
42
|
-
return false;
|
|
43
|
-
if (isViewerAuthoredHuman(thread, botUsernames))
|
|
44
|
-
return true;
|
|
45
|
-
return shouldResolveOtherHumanThread(thread, policy);
|
|
46
|
-
})
|
|
47
|
-
.map((thread) => thread.id));
|
|
48
|
-
const resolveThreadIds = (opts.resolveThreadIds ?? []).filter((id) => !humanThreadIds.has(id) || allowedHumanResolveIds.has(id));
|
|
49
|
-
const skippedHumanResolves = (opts.resolveThreadIds ?? []).filter((id) => humanThreadIds.has(id) && !allowedHumanResolveIds.has(id));
|
|
50
|
-
const knownThreadIds = new Set(data.reviewThreads.map((thread) => thread.id));
|
|
51
|
-
const replyThreadIds = opts.replyThreadIds?.filter((id) => knownThreadIds.has(id));
|
|
52
|
-
const skippedNonHumanReplies = (opts.replyThreadIds ?? []).filter((id) => !knownThreadIds.has(id));
|
|
53
|
-
const minimizeCommentIds = (opts.minimizeCommentIds ?? []).filter((id) => !humanCommentIds.has(id) && !humanReviewIds.has(id));
|
|
54
|
-
const skippedHumanMinimizes = (opts.minimizeCommentIds ?? []).filter((id) => humanCommentIds.has(id) || humanReviewIds.has(id));
|
|
55
|
-
const dismissReviewIds = (opts.dismissReviewIds ?? []).filter((id) => !humanReviewIds.has(id) && data.changesRequestedReviews.some((review) => review.id === id));
|
|
56
|
-
const skippedHumanDismissals = (opts.dismissReviewIds ?? []).filter((id) => humanReviewIds.has(id));
|
|
57
|
-
const skippedIneligibleDismissals = (opts.dismissReviewIds ?? []).filter((id) => !humanReviewIds.has(id) && !dismissReviewIds.includes(id));
|
|
58
|
-
const hasMutation = resolveThreadIds.length > 0 ||
|
|
59
|
-
(replyThreadIds?.length ?? 0) > 0 ||
|
|
60
|
-
minimizeCommentIds.length > 0 ||
|
|
61
|
-
dismissReviewIds.length > 0;
|
|
15
|
+
// Fetch only to retain the pre-reply transcript for successful-reply seen
|
|
16
|
+
// markers. It never determines which user-supplied IDs are sent to GitHub.
|
|
17
|
+
let threadById;
|
|
18
|
+
if (opts.replyThreadIds?.length) {
|
|
19
|
+
try {
|
|
20
|
+
threadById = new Map((await fetchPrBatch(prNumber, repo, { paginateApprovedReviews: true })).data.reviewThreads.map((thread) => [thread.id, thread]));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Seen-marker bookkeeping is best-effort. A failed read must not block
|
|
24
|
+
// the explicit mutation request; GitHub's mutation response is authoritative.
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Iterate capability-checks and routes its generated commands. Direct apply
|
|
28
|
+
// requests are user-directed: forward every supplied ID unchanged and let
|
|
29
|
+
// GitHub report whether each requested mutation is permitted or applicable.
|
|
62
30
|
const result = await applyResolveOptions(prNumber, repo, {
|
|
63
|
-
resolveThreadIds,
|
|
64
|
-
replyThreadIds,
|
|
65
|
-
minimizeCommentIds,
|
|
66
|
-
dismissReviewIds,
|
|
31
|
+
resolveThreadIds: opts.resolveThreadIds,
|
|
32
|
+
replyThreadIds: opts.replyThreadIds,
|
|
33
|
+
minimizeCommentIds: opts.minimizeCommentIds,
|
|
34
|
+
dismissReviewIds: opts.dismissReviewIds,
|
|
67
35
|
dismissMessage: opts.dismissMessage,
|
|
68
|
-
requireSha:
|
|
36
|
+
requireSha: opts.requireSha,
|
|
69
37
|
});
|
|
70
|
-
if (
|
|
71
|
-
result.skippedHumanResolves = skippedHumanResolves;
|
|
72
|
-
if (skippedHumanMinimizes.length > 0)
|
|
73
|
-
result.skippedHumanMinimizes = skippedHumanMinimizes;
|
|
74
|
-
if (skippedHumanDismissals.length > 0)
|
|
75
|
-
result.skippedHumanDismissals = skippedHumanDismissals;
|
|
76
|
-
if (skippedNonHumanReplies.length > 0)
|
|
77
|
-
result.skippedNonHumanReplies = skippedNonHumanReplies;
|
|
78
|
-
if (skippedIneligibleDismissals.length > 0) {
|
|
79
|
-
result.skippedDismissals = [
|
|
80
|
-
...(result.skippedDismissals ?? []),
|
|
81
|
-
...skippedIneligibleDismissals,
|
|
82
|
-
];
|
|
83
|
-
}
|
|
84
|
-
if (opts.dismissMessage) {
|
|
38
|
+
if (opts.dismissMessage && threadById) {
|
|
85
39
|
const markedMessage = addPrShepherdMarker(opts.dismissMessage);
|
|
86
40
|
await Promise.all(result.repliedThreads.map((id) => {
|
|
87
41
|
const thread = threadById.get(id);
|
|
@@ -7,10 +7,15 @@ export interface ResolveResult {
|
|
|
7
7
|
minimizedComments: string[];
|
|
8
8
|
dismissedReviews: string[];
|
|
9
9
|
errors: string[];
|
|
10
|
+
/** @deprecated Direct apply forwards every supplied dismissal ID to GitHub. */
|
|
10
11
|
skippedDismissals?: string[];
|
|
12
|
+
/** @deprecated Direct apply no longer applies author policy. */
|
|
11
13
|
skippedHumanResolves?: string[];
|
|
14
|
+
/** @deprecated Direct apply no longer applies author policy. */
|
|
12
15
|
skippedHumanMinimizes?: string[];
|
|
16
|
+
/** @deprecated Direct apply no longer applies author policy. */
|
|
13
17
|
skippedHumanDismissals?: string[];
|
|
18
|
+
/** @deprecated Direct apply forwards every supplied reply ID to GitHub. */
|
|
14
19
|
skippedNonHumanReplies?: string[];
|
|
15
20
|
/** @deprecated Direct apply requests now rely on GitHub's mutation response. */
|
|
16
21
|
skippedUnauthorizedReplies?: string[];
|
package/bin/comments/resolve.mjs
CHANGED
|
@@ -35,9 +35,6 @@ export async function applyResolveOptions(pr, repo, opts) {
|
|
|
35
35
|
const replyThreadIds = dedupeIds(opts.replyThreadIds ?? []);
|
|
36
36
|
const minimizeCommentIds = opts.minimizeCommentIds ?? [];
|
|
37
37
|
const dismissReviewIds = dedupeIds(opts.dismissReviewIds ?? []);
|
|
38
|
-
const minimizeCommentIdSet = new Set(minimizeCommentIds);
|
|
39
|
-
const filteredDismissReviewIds = dismissReviewIds.filter((id) => !minimizeCommentIdSet.has(id));
|
|
40
|
-
const overlappingDismissIds = dismissReviewIds.filter((id) => minimizeCommentIdSet.has(id));
|
|
41
38
|
const result = {
|
|
42
39
|
repliedThreads: [],
|
|
43
40
|
resolvedThreads: [],
|
|
@@ -45,13 +42,7 @@ export async function applyResolveOptions(pr, repo, opts) {
|
|
|
45
42
|
dismissedReviews: [],
|
|
46
43
|
errors: [],
|
|
47
44
|
};
|
|
48
|
-
if (
|
|
49
|
-
result.skippedDismissals = [];
|
|
50
|
-
for (const id of overlappingDismissIds) {
|
|
51
|
-
result.skippedDismissals.push(id);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
if ((filteredDismissReviewIds.length > 0 || replyThreadIds.length > 0) && !opts.dismissMessage) {
|
|
45
|
+
if ((dismissReviewIds.length > 0 || replyThreadIds.length > 0) && !opts.dismissMessage) {
|
|
55
46
|
throw new Error("--message is required when replying to threads or dismissing reviews");
|
|
56
47
|
}
|
|
57
48
|
if (opts.requireSha) {
|
|
@@ -59,7 +50,7 @@ export async function applyResolveOptions(pr, repo, opts) {
|
|
|
59
50
|
// before reviewers see the fix.
|
|
60
51
|
await waitForSha(pr, repo, opts.requireSha);
|
|
61
52
|
}
|
|
62
|
-
await bulkApply(replyThreadIds, resolveThreadIds, minimizeCommentIds,
|
|
53
|
+
await bulkApply(replyThreadIds, resolveThreadIds, minimizeCommentIds, dismissReviewIds, opts.dismissMessage ?? "", result);
|
|
63
54
|
return result;
|
|
64
55
|
}
|
|
65
56
|
/** @deprecated Compatibility alias; use `autoResolveThreads`. */
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Persistent attempt counter for the iterate escalation guard.
|
|
3
3
|
*
|
|
4
|
-
* Tracks how many times each review thread has been dispatched to
|
|
5
|
-
* handler without being resolved.
|
|
6
|
-
* commit SHA changes (i.e. a new push landed).
|
|
4
|
+
* Tracks how many caller-visible times each review thread has been dispatched to
|
|
5
|
+
* the fix_code handler without being resolved. Body edits reset the count.
|
|
7
6
|
*
|
|
8
7
|
* State lives in `$TMPDIR/pr-shepherd-state/<owner>-<repo>/<pr>/fix-attempts.json`.
|
|
9
8
|
*/
|
|
10
9
|
export interface FixAttemptsState {
|
|
11
|
-
/** HEAD SHA at the time the counts were last written
|
|
10
|
+
/** HEAD SHA at the time the counts were last written, retained for observability/compatibility. */
|
|
12
11
|
headSha: string;
|
|
13
12
|
/** Map from thread ID → number of fix_code dispatches that included this thread. */
|
|
14
13
|
threadAttempts: Record<string, number>;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Persistent attempt counter for the iterate escalation guard.
|
|
3
3
|
*
|
|
4
|
-
* Tracks how many times each review thread has been dispatched to
|
|
5
|
-
* handler without being resolved.
|
|
6
|
-
* commit SHA changes (i.e. a new push landed).
|
|
4
|
+
* Tracks how many caller-visible times each review thread has been dispatched to
|
|
5
|
+
* the fix_code handler without being resolved. Body edits reset the count.
|
|
7
6
|
*
|
|
8
7
|
* State lives in `$TMPDIR/pr-shepherd-state/<owner>-<repo>/<pr>/fix-attempts.json`.
|
|
9
8
|
*/
|
package/bin/types/escalate.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentCheck, AgentComment, AgentThread } from "./report.mts";
|
|
2
|
+
import type { ResolveCommand } from "./iterate.mts";
|
|
2
3
|
import type { CheckStatus, Review } from "./github.mts";
|
|
3
4
|
import type { MergeQueueRemovalStatus, StackStatus } from "./merge-requirements.mts";
|
|
4
5
|
export type EscalateTrigger = "fix-thrash" | "base-branch-unknown" | "stall-timeout" | "check-follow-up-unavailable" | "authorization-required" | "bot-cr-not-dismissed" | "merge-queue-removed" | "stacked-pr";
|
|
@@ -19,6 +20,10 @@ export interface EscalateDetails {
|
|
|
19
20
|
unresolvedThreads: AgentThread[];
|
|
20
21
|
ambiguousComments: AgentComment[];
|
|
21
22
|
changesRequestedReviews: Review[];
|
|
23
|
+
/** First-look review summaries that must be shown before any pending minimization. */
|
|
24
|
+
firstLookSummaries?: Review[];
|
|
25
|
+
/** Previously seen review summaries whose edited bodies must be shown again. */
|
|
26
|
+
editedSummaries?: Review[];
|
|
22
27
|
/** Failing checks whose next step requires human attention. */
|
|
23
28
|
checks?: AgentCheck[];
|
|
24
29
|
stalledChecks?: AgentStalledCheck[];
|
|
@@ -26,6 +31,11 @@ export interface EscalateDetails {
|
|
|
26
31
|
threadId: string;
|
|
27
32
|
attempts: number;
|
|
28
33
|
}>;
|
|
34
|
+
/** Review mutations generated for this tick, retained so an escalation cannot strand them. */
|
|
35
|
+
pendingReviewCommands?: {
|
|
36
|
+
resolveOnlyCommand?: ResolveCommand;
|
|
37
|
+
resolveCommand?: ResolveCommand;
|
|
38
|
+
};
|
|
29
39
|
suggestion: string;
|
|
30
40
|
humanMessage: string;
|
|
31
41
|
mergeQueueRemoval?: MergeQueueRemovalStatus;
|
package/package.json
CHANGED
|
@@ -51,7 +51,7 @@ annotations, or CI log excerpts.
|
|
|
51
51
|
- The command builds from the fetched PR head and accepts a clean local descendant only when the complete ordered patch stream passes `git apply --check`.
|
|
52
52
|
- If the command refuses because a suggestion is unsafe or no longer applies, inspect the current source, the displayed replacement block, and reviewer intent before editing manually. Do not apply a stale numeric range blindly or retry unchanged input.
|
|
53
53
|
- A returned patch was checked against the then-current worktree. If it later fails, re-inspect the worktree because it changed after validation.
|
|
54
|
-
-
|
|
54
|
+
- Use the generated thread IDs and flag placement returned with the patch command.
|
|
55
55
|
|
|
56
56
|
### CI failure triage
|
|
57
57
|
|
|
@@ -77,16 +77,11 @@ When several bullets share one runId (matrix jobs from the same run), the `rerun
|
|
|
77
77
|
|
|
78
78
|
Applies to every `apply review:` / `resolve-only:` command the CLI prints. Covers only what stays safe if you run the printed command **unmodified** — `$HEAD_SHA`/`$DISMISS_MESSAGE` substitution remains a separate CLI-printed step because the command is unsafe by default without those placeholders.
|
|
79
79
|
|
|
80
|
-
The CLI only includes IDs whose per-object GitHub viewer capability and semantic routing authorize the corresponding generated action.
|
|
80
|
+
The CLI only includes IDs whose per-object GitHub viewer capability and semantic routing authorize the corresponding generated action. Generated commands are pre-populated; omission is not a prohibition. A separate, user-directed `apply review` request may supply any reply, resolve, minimize, or dismiss IDs; it forwards them without Shepherd author, capability, or current-state filtering, and GitHub's per-operation response is authoritative.
|
|
81
81
|
|
|
82
|
-
-
|
|
83
|
-
- Never add first-look-only or check-annotation IDs to `--reply-thread-ids`, `--resolve-thread-ids`, `--dismiss-review-ids`, or `--minimize-comment-ids` — those flags are pre-populated by the CLI.
|
|
82
|
+
- When `## Instructions` says to run a generated `apply review:` / `resolve-only:` command, run it even when no code change is warranted. An `[ESCALATE]` instruction may require user direction first. The command records the agent's disposition of the included review items; skipping it leaves authorized threads active and can eventually trigger `fix-thrash`.
|
|
84
83
|
- Keep every existing `--dismiss-review-ids` ID the CLI already included. Each is a bot or non-human review that must be dismissed; omitting one leaves the PR in `CHANGES_REQUESTED`.
|
|
85
84
|
|
|
86
|
-
### Review-mutation routing
|
|
87
|
-
|
|
88
|
-
For threads under both `## Review threads` and `## Review threads to resolve`, evaluate every thread before running mutations. Keep unmarked bot/non-human and viewer-authored IDs in both `--reply-thread-ids` and `--resolve-thread-ids`, including when the feedback is advisory, already satisfied, or otherwise warrants no code change: the reply runs before the resolve. Unmarked other-human IDs use `--reply-thread-ids` only unless the CLI also put them in `--resolve-thread-ids`. When the latest comment begins `<!-- pr-shepherd -->`, it is an established Shepherd reply—not merely a same-account comment. A marked thread that is still being resolved is resolve-only for retry. Do not add IDs the CLI omitted, and do not move IDs between flags.
|
|
89
|
-
|
|
90
85
|
### Shepherd Journal
|
|
91
86
|
|
|
92
87
|
Link threads and comments in a journal entry from their headings in the CLI output. Cite reviews by ID.
|