pr-shepherd 0.46.5 → 0.46.7
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 +6 -3
- package/bin/api.d.mts +1 -1
- package/bin/checks/triage.mjs +12 -1
- package/bin/cli/body-truncate.d.mts +12 -0
- package/bin/cli/body-truncate.mjs +123 -0
- package/bin/cli/fix-formatter-extra.d.mts +2 -1
- package/bin/cli/fix-formatter-extra.mjs +18 -3
- package/bin/cli/fix-formatter.d.mts +3 -1
- package/bin/cli/fix-formatter.mjs +42 -16
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +2 -0
- package/bin/cli/help.d.mts +1 -1
- package/bin/cli/iterate-checks-formatter.mjs +8 -6
- package/bin/cli/iterate-formatter.mjs +10 -9
- package/bin/cli/iterate-lean.mjs +5 -0
- package/bin/cli/iterate-merge-formatter.d.mts +3 -1
- package/bin/cli/iterate-merge-formatter.mjs +15 -0
- package/bin/cli/list-formatters.d.mts +4 -3
- package/bin/cli/list-formatters.mjs +22 -14
- package/bin/commands/check-annotations.d.mts +15 -0
- package/bin/commands/check-annotations.mjs +23 -1
- package/bin/commands/check.d.mts +1 -0
- package/bin/commands/check.mjs +73 -40
- package/bin/commands/iterate/check-instructions.mjs +1 -1
- package/bin/commands/iterate/classify.d.mts +2 -2
- package/bin/commands/iterate/classify.mjs +2 -2
- package/bin/commands/iterate/escalate.mjs +9 -0
- package/bin/commands/iterate/fix-code.mjs +11 -9
- package/bin/commands/iterate/fix-instruction-threads.d.mts +14 -0
- package/bin/commands/iterate/fix-instruction-threads.mjs +32 -0
- package/bin/commands/iterate/index.mjs +20 -7
- package/bin/commands/iterate/merge-state.d.mts +7 -2
- package/bin/commands/iterate/merge-state.mjs +68 -2
- package/bin/commands/iterate/render.d.mts +1 -1
- package/bin/commands/iterate/render.mjs +12 -23
- package/bin/commands/iterate/thread-mutation-routing.d.mts +10 -2
- package/bin/commands/iterate/thread-mutation-routing.mjs +26 -14
- package/bin/commands/resolve-mutate.mjs +18 -7
- package/bin/comments/thread-resolve-policy.d.mts +6 -0
- package/bin/comments/thread-resolve-policy.mjs +9 -0
- package/bin/comments/thread-visibility.d.mts +2 -1
- package/bin/comments/thread-visibility.mjs +12 -5
- package/bin/config/load.d.mts +18 -0
- package/bin/config/load.mjs +27 -1
- package/bin/config.json +6 -3
- package/bin/mcp/server.mjs +18 -6
- package/bin/types/escalate.d.mts +3 -2
- package/bin/types/iterate.d.mts +10 -0
- package/bin/types/merge-queue.d.mts +16 -0
- package/package.json +9 -6
- 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 +17 -4
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Autonomous PR CI monitor and review-comment resolver for agentic coding tools, including Claude Code and Codex.
|
|
4
4
|
|
|
5
|
+
**[jongleberry.com/pr-shepherd](https://jongleberry.com/pr-shepherd/)** — the why and the principles behind the design, for humans and agents.
|
|
6
|
+
|
|
5
7
|
## Why
|
|
6
8
|
|
|
7
9
|
An agent finishing a PR should think about code, not reconstruct GitHub state or invent a next-step policy each tick. Without Shepherd it fans out across GitHub MCP, `gh`, and GraphQL, then guesses what to do with the result.
|
|
@@ -70,7 +72,7 @@ Conversations Resolved: No [Not Required]
|
|
|
70
72
|
2. Apply every warranted review fix in each file referenced above.
|
|
71
73
|
3. Triage every failure under `## Failing checks`. See "CI failure triage" in the pr-shepherd skill for read-only inspection rules.
|
|
72
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.
|
|
73
|
-
5. Run the generated thread IDs unchanged. A latest comment beginning `<!-- pr-shepherd -->` is an earlier Shepherd reply: a marked
|
|
75
|
+
5. Run the generated thread IDs unchanged. A latest comment beginning `<!-- pr-shepherd -->` is an earlier Shepherd reply: a marked thread that is still being resolved is emitted resolve-only when authorized.
|
|
74
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.
|
|
75
77
|
7. Replace `$DISMISS_MESSAGE` with one sentence describing what changed.
|
|
76
78
|
8. Run the `apply review:` command shown above. See "Review-mutation mechanics" in the pr-shepherd skill for dismiss-ID retention.
|
|
@@ -83,11 +85,12 @@ See [docs/actions.md](docs/actions.md) for the complete output contract and [doc
|
|
|
83
85
|
|
|
84
86
|
This system is opinionated and works best with PRs that use required status checks and conversation resolution.
|
|
85
87
|
|
|
86
|
-
- A human inline thread whose original comment has `viewerDidAuthor: true` is replied to and resolved when its latest comment is unmarked. An unmarked other-human inline thread remains reply-only
|
|
87
|
-
- Detected bots
|
|
88
|
+
- A human inline thread whose original comment has `viewerDidAuthor: true` is replied to and resolved when its latest comment is unmarked. Bot/non-human threads use the same reply-and-resolve pairing. An unmarked other-human inline thread remains reply-only unless `iterate.resolveOtherHumanThreads` is `outdated` or `always`. Human items are never minimized.
|
|
89
|
+
- Detected bots, configured `botUsernames`, and viewer-authored human review threads are returned until resolved when the required mutation is authorized. Reply and resolve mutations use the thread ID, so they still run when GitHub has cleared the source line. Unauthorized threads are surfaced once and then marker-gated until edited. Bot/non-human threads, PR comments, and review summaries can be resolved or minimized when eligible. Review summaries are not minimized while known inline child threads from that review remain unresolved.
|
|
88
90
|
- Shepherd identifies its own latest reply only when that comment begins `<!-- pr-shepherd -->`, not from author equality. A marked viewer-authored thread can be resolved without another reply as a retry.
|
|
89
91
|
- Every review thread/comment/review summary is surfaced at least once, even if already outdated, resolved, or minimized; edited items re-surface through seen markers.
|
|
90
92
|
- Draft PRs can be marked ready automatically when clean; disable with `actions.autoMarkReady: false` or `--no-auto-mark-ready`.
|
|
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.
|
|
91
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.
|
|
92
95
|
- Generated iterate mutations and automatic actions are capability-aware and omit unauthorized commands. Explicit `apply` operations honor the caller's intent and surface GitHub's result; semantic human-content protections still apply.
|
|
93
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.
|
package/bin/api.d.mts
CHANGED
|
@@ -8,7 +8,7 @@ export interface CreatePrShepherdOptions {
|
|
|
8
8
|
}
|
|
9
9
|
/** A positive PR number, GitHub pull-request URL, or owner/repo#number reference. */
|
|
10
10
|
export type PrReference = number | string;
|
|
11
|
-
export type IterateInput = Omit<IterateCommandOptions, "format" | "prNumber" | "targetRepository"> & {
|
|
11
|
+
export type IterateInput = Omit<IterateCommandOptions, "format" | "prNumber" | "targetRepository" | "persistSeen" | "deferQuotaWarning"> & {
|
|
12
12
|
pr?: PrReference;
|
|
13
13
|
};
|
|
14
14
|
export interface ReviewMutationsOperation {
|
package/bin/checks/triage.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable max-lines */
|
|
2
2
|
import { restWithRateLimit, restText } from "../github/http.mjs";
|
|
3
3
|
import { loadDerived, storeDerived } from "../state/rest-cache.mjs";
|
|
4
|
+
import { loadConfig } from "../config/load.mjs";
|
|
4
5
|
const STARTUP_FAILURE_STATUS = "startup_failure";
|
|
5
6
|
const LOG_EXCERPT_CONTEXT_LINES = 16;
|
|
6
7
|
const LOG_EXCERPT_TAIL_LINES = 28;
|
|
@@ -181,10 +182,11 @@ async function fetchJobLogExcerpt(jobId, repo, stateKey, cacheable = false) {
|
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
184
|
function buildLogExcerpt(raw) {
|
|
185
|
+
const ignorePatterns = compileIgnoreLogLinePatterns();
|
|
184
186
|
const lines = raw
|
|
185
187
|
.split(/\r?\n/)
|
|
186
188
|
.map(cleanLogLine)
|
|
187
|
-
.filter((line) => line.trim() !== "");
|
|
189
|
+
.filter((line) => line.trim() !== "" && !isNoiseLine(line, ignorePatterns));
|
|
188
190
|
if (lines.length === 0)
|
|
189
191
|
return undefined;
|
|
190
192
|
const aggregateExcerpt = buildAggregateJobResultsExcerpt(lines);
|
|
@@ -276,6 +278,15 @@ function truncateAnchoredExcerpt(lines, anchorIndex) {
|
|
|
276
278
|
return text;
|
|
277
279
|
return truncateLogExcerpt(`${TRUNCATED_SUFFIX.trim()}\n${lines.slice(anchorIndex).join("\n")}`);
|
|
278
280
|
}
|
|
281
|
+
// User-configured via `checks.ignoreLogLines` (regex source strings) — empty by
|
|
282
|
+
// default. What counts as noise varies by CI toolchain, so Shepherd ships no
|
|
283
|
+
// built-in patterns; a project opts in via `.pr-shepherdrc.yml`.
|
|
284
|
+
function compileIgnoreLogLinePatterns() {
|
|
285
|
+
return loadConfig().checks.ignoreLogLines.map((pattern) => new RegExp(pattern));
|
|
286
|
+
}
|
|
287
|
+
function isNoiseLine(line, patterns) {
|
|
288
|
+
return patterns.some((re) => re.test(line));
|
|
289
|
+
}
|
|
279
290
|
function cleanLogLine(line) {
|
|
280
291
|
return line
|
|
281
292
|
.replace(/^\uFEFF/, "")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const BODY_TRUNCATE_MAX_CHARS = 1200;
|
|
2
|
+
export declare const NESTED_BODY_TRUNCATE_MAX_CHARS = 600;
|
|
3
|
+
/**
|
|
4
|
+
* Truncates `body` to roughly `maxChars`, keeping a head and tail portion so an
|
|
5
|
+
* opening question and a trailing summary both survive. Never cuts inside a
|
|
6
|
+
* ``` or ~~~ fence: the cut points snap outward to the nearest fence-safe line
|
|
7
|
+
* boundary — an unbalanced fence here would swallow every section rendered
|
|
8
|
+
* after it in the same tick. A single line too long to fit a budget on its own
|
|
9
|
+
* (headEnd stays -1, or no line fits from the tail) falls back to a
|
|
10
|
+
* character-level slice of that line rather than dropping it entirely.
|
|
11
|
+
*/
|
|
12
|
+
export declare function truncateBody(body: string, maxChars: number, url?: string): string;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const HEAD_FRACTION = 0.7;
|
|
2
|
+
export const BODY_TRUNCATE_MAX_CHARS = 1200;
|
|
3
|
+
export const NESTED_BODY_TRUNCATE_MAX_CHARS = 600;
|
|
4
|
+
function fenceOpener(trimmed) {
|
|
5
|
+
const char = trimmed[0];
|
|
6
|
+
if (char !== "`" && char !== "~")
|
|
7
|
+
return null;
|
|
8
|
+
let len = 0;
|
|
9
|
+
while (trimmed[len] === char)
|
|
10
|
+
len++;
|
|
11
|
+
return len >= 3 ? { char, len } : null;
|
|
12
|
+
}
|
|
13
|
+
function isFenceCloser(trimmed, fence) {
|
|
14
|
+
return trimmed.length >= fence.len && [...trimmed].every((c) => c === fence.char);
|
|
15
|
+
}
|
|
16
|
+
// Backtick and tilde fences close only against their own kind — a run of one
|
|
17
|
+
// character never closes a fence opened with the other (CommonMark semantics).
|
|
18
|
+
function fenceStatesAfterEachLine(lines) {
|
|
19
|
+
const states = [];
|
|
20
|
+
let fence = null;
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
const trimmed = line.trim();
|
|
23
|
+
if (fence) {
|
|
24
|
+
if (isFenceCloser(trimmed, fence))
|
|
25
|
+
fence = null;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
fence = fenceOpener(trimmed);
|
|
29
|
+
}
|
|
30
|
+
states.push(fence !== null);
|
|
31
|
+
}
|
|
32
|
+
return states;
|
|
33
|
+
}
|
|
34
|
+
function cumulativeLengths(lines) {
|
|
35
|
+
const out = [];
|
|
36
|
+
let total = 0;
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
total += line.length + 1;
|
|
39
|
+
out.push(total);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/** Last line index keepable within `budget`, extended forward past any fence still open at that point. */
|
|
44
|
+
function findHeadEnd(states, cumLens, budget) {
|
|
45
|
+
let end = -1;
|
|
46
|
+
for (let i = 0; i < cumLens.length; i++) {
|
|
47
|
+
if (cumLens[i] > budget)
|
|
48
|
+
break;
|
|
49
|
+
end = i;
|
|
50
|
+
}
|
|
51
|
+
while (end >= 0 && end < states.length - 1 && states[end])
|
|
52
|
+
end++;
|
|
53
|
+
return end;
|
|
54
|
+
}
|
|
55
|
+
/** First line index keepable within `budget` counted from the end, extended backward past any fence open entering it. */
|
|
56
|
+
function findTailStart(states, cumLens, budget) {
|
|
57
|
+
const n = cumLens.length;
|
|
58
|
+
const total = cumLens[n - 1] ?? 0;
|
|
59
|
+
let start = n;
|
|
60
|
+
for (let j = n - 1; j >= 0; j--) {
|
|
61
|
+
const suffixLen = total - (j > 0 ? cumLens[j - 1] : 0);
|
|
62
|
+
if (suffixLen > budget)
|
|
63
|
+
break;
|
|
64
|
+
start = j;
|
|
65
|
+
}
|
|
66
|
+
while (start > 0 && states[start - 1])
|
|
67
|
+
start--;
|
|
68
|
+
return start;
|
|
69
|
+
}
|
|
70
|
+
const REVIEW_COMMENT_URL_RE = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/\d+#discussion_r(\d+)$/;
|
|
71
|
+
const ISSUE_COMMENT_URL_RE = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/(?:pull|issues)\/\d+#issuecomment-(\d+)$/;
|
|
72
|
+
// An agent can run this directly — no browser round-trip — to read the full body.
|
|
73
|
+
function commentViewCommand(url) {
|
|
74
|
+
const review = REVIEW_COMMENT_URL_RE.exec(url);
|
|
75
|
+
if (review) {
|
|
76
|
+
const [, owner, repo, id] = review;
|
|
77
|
+
return `gh api repos/${owner}/${repo}/pulls/comments/${id}`;
|
|
78
|
+
}
|
|
79
|
+
const issue = ISSUE_COMMENT_URL_RE.exec(url);
|
|
80
|
+
if (issue) {
|
|
81
|
+
const [, owner, repo, id] = issue;
|
|
82
|
+
return `gh api repos/${owner}/${repo}/issues/comments/${id}`;
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
function elisionMarker(charCount, url) {
|
|
87
|
+
const pointer = url ? (commentViewCommand(url) ?? url) : undefined;
|
|
88
|
+
return pointer
|
|
89
|
+
? `[…${charCount} chars elided — full text: ${pointer}]`
|
|
90
|
+
: `[…${charCount} chars elided]`;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Truncates `body` to roughly `maxChars`, keeping a head and tail portion so an
|
|
94
|
+
* opening question and a trailing summary both survive. Never cuts inside a
|
|
95
|
+
* ``` or ~~~ fence: the cut points snap outward to the nearest fence-safe line
|
|
96
|
+
* boundary — an unbalanced fence here would swallow every section rendered
|
|
97
|
+
* after it in the same tick. A single line too long to fit a budget on its own
|
|
98
|
+
* (headEnd stays -1, or no line fits from the tail) falls back to a
|
|
99
|
+
* character-level slice of that line rather than dropping it entirely.
|
|
100
|
+
*/
|
|
101
|
+
export function truncateBody(body, maxChars, url) {
|
|
102
|
+
if (body.length <= maxChars)
|
|
103
|
+
return body;
|
|
104
|
+
const lines = body.split("\n");
|
|
105
|
+
const states = fenceStatesAfterEachLine(lines);
|
|
106
|
+
const cumLens = cumulativeLengths(lines);
|
|
107
|
+
const headBudget = Math.ceil(maxChars * HEAD_FRACTION);
|
|
108
|
+
const tailBudget = maxChars - headBudget;
|
|
109
|
+
const headEnd = findHeadEnd(states, cumLens, headBudget);
|
|
110
|
+
const tailStart = findTailStart(states, cumLens, tailBudget);
|
|
111
|
+
if (tailStart <= headEnd + 1)
|
|
112
|
+
return body;
|
|
113
|
+
const head = headEnd >= 0 ? lines.slice(0, headEnd + 1).join("\n") : lines[0].slice(0, headBudget);
|
|
114
|
+
const tail = tailStart < lines.length
|
|
115
|
+
? lines.slice(tailStart).join("\n")
|
|
116
|
+
: tailBudget > 0
|
|
117
|
+
? lines[lines.length - 1].slice(-tailBudget)
|
|
118
|
+
: "";
|
|
119
|
+
const elidedChars = body.length - head.length - tail.length;
|
|
120
|
+
if (elidedChars <= 0)
|
|
121
|
+
return body;
|
|
122
|
+
return [head, elisionMarker(elidedChars, url), tail].join("\n\n");
|
|
123
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { CheckAnnotation, IterateResultFixCode } from "../types.mts";
|
|
2
|
-
|
|
2
|
+
/** Returns null when the annotation adds nothing beyond the check's own conclusion tag. */
|
|
3
|
+
export declare function renderCheckAnnotation(a: CheckAnnotation, logExcerpt?: string): string | null;
|
|
3
4
|
export declare function renderProtectedRun(run: IterateResultFixCode["fix"]["protectedRuns"][number]): string;
|
|
@@ -1,15 +1,30 @@
|
|
|
1
1
|
import { blockquote } from "./list-formatters.mjs";
|
|
2
|
-
|
|
2
|
+
// A bare "Process completed with exit code N." on its own carries nothing beyond
|
|
3
|
+
// the check's own [conclusion: FAILURE] tag, so the whole annotation is dropped.
|
|
4
|
+
const TRIVIAL_EXIT_CODE_RE = /^Process completed with exit code \d+\.?$/;
|
|
5
|
+
/** Returns null when the annotation adds nothing beyond the check's own conclusion tag. */
|
|
6
|
+
export function renderCheckAnnotation(a, logExcerpt) {
|
|
7
|
+
const hasRawDetails = a.rawDetails !== undefined && a.rawDetails.trim() !== "";
|
|
8
|
+
const hasTitle = a.title !== undefined && a.title.trim() !== "";
|
|
9
|
+
if (TRIVIAL_EXIT_CODE_RE.test(a.message.trim()) && !hasRawDetails && !hasTitle)
|
|
10
|
+
return null;
|
|
3
11
|
const loc = `${a.path}:${renderAnnotationRange(a)}`;
|
|
4
12
|
const link = a.blobUrl ? ` [↗](${a.blobUrl})` : "";
|
|
5
13
|
const title = a.title ? ` — ${a.title}` : "";
|
|
6
14
|
const lines = [`- \`${a.id}\`${link} \`${loc}\` [${a.level}]${title}`];
|
|
7
|
-
if (a.message.trim() !== "")
|
|
15
|
+
if (a.message.trim() !== "" && !duplicatesLog(a.message, logExcerpt)) {
|
|
8
16
|
lines.push(blockquote(a.message));
|
|
9
|
-
|
|
17
|
+
}
|
|
18
|
+
if (hasRawDetails && !duplicatesLog(a.rawDetails, logExcerpt)) {
|
|
10
19
|
lines.push(blockquote(a.rawDetails));
|
|
20
|
+
}
|
|
11
21
|
return lines.join("\n");
|
|
12
22
|
}
|
|
23
|
+
// The bullet's path:line + blob link already anchors this text; if the identical
|
|
24
|
+
// text is also in the check's log excerpt, only the blockquote body is redundant.
|
|
25
|
+
function duplicatesLog(text, logExcerpt) {
|
|
26
|
+
return logExcerpt !== undefined && logExcerpt.includes(text.trim());
|
|
27
|
+
}
|
|
13
28
|
export function renderProtectedRun(run) {
|
|
14
29
|
const label = run.workflowName
|
|
15
30
|
? `${run.workflowName} (${run.checkNames.join(", ")})`
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import type { IterateResultFixCode } from "../types.mts";
|
|
2
|
-
export declare function formatFixCodeResult(header: string, result: IterateResultFixCode
|
|
2
|
+
export declare function formatFixCodeResult(header: string, result: IterateResultFixCode, opts?: {
|
|
3
|
+
verbose?: boolean;
|
|
4
|
+
}): string;
|
|
@@ -3,11 +3,15 @@ import { renderResolveCommand } from "../commands/iterate/render.mjs";
|
|
|
3
3
|
import { inlineCode, joinSections } from "../util/markdown.mjs";
|
|
4
4
|
import { renderSuggestionBlock, renderLineRange } from "./suggestion-renderer.mjs";
|
|
5
5
|
import { renderThreadBullet, renderReviewBullet, renderThreadResolutionStatusTag, renderAuthor, buildFirstLookBullets, renderThreadConversation, blockquote, } from "./list-formatters.mjs";
|
|
6
|
+
import { BODY_TRUNCATE_MAX_CHARS } from "./body-truncate.mjs";
|
|
6
7
|
import { numberInstructions } from "./iterate-instructions.mjs";
|
|
7
8
|
import { renderCheckAnnotation, renderProtectedRun } from "./fix-formatter-extra.mjs";
|
|
8
9
|
import { isFailingAgentCheck } from "../checks/conclusions.mjs";
|
|
9
10
|
import { renderMergeCommand } from "../commands/iterate/merge.mjs";
|
|
10
|
-
|
|
11
|
+
import { partitionFixThreads } from "../commands/iterate/fix-instruction-threads.mjs";
|
|
12
|
+
export function formatFixCodeResult(header, result, opts = {}) {
|
|
13
|
+
const verbose = opts.verbose ?? false;
|
|
14
|
+
const topCap = verbose ? undefined : BODY_TRUNCATE_MAX_CHARS;
|
|
11
15
|
const sections = [header];
|
|
12
16
|
const renderThreads = (heading, threads) => {
|
|
13
17
|
if (threads.length === 0)
|
|
@@ -21,20 +25,19 @@ export function formatFixCodeResult(header, result) {
|
|
|
21
25
|
const suggestionMarker = t.suggestion ? " [suggestion]" : "";
|
|
22
26
|
const editedMarker = t.edited ? " [edited since first look]" : "";
|
|
23
27
|
sections.push(`### ${heading} — ${loc} (${renderAuthor(t.author, t.authorType, t.authorAssociation, t.viewerDidAuthor)})${reviewMarker}${suggestionMarker}${editedMarker}`);
|
|
24
|
-
sections.push(renderThreadConversation(t));
|
|
28
|
+
sections.push(renderThreadConversation(t, verbose));
|
|
25
29
|
if (t.suggestion) {
|
|
26
30
|
sections.push(renderSuggestionBlock(t.suggestion, ""));
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
33
|
};
|
|
30
|
-
const locatedThreads = result.fix.threads
|
|
31
|
-
|
|
32
|
-
renderThreads("## Review threads", locatedThreads);
|
|
34
|
+
const { locatedThreads, unlocatedMutatedThreads, unlocatedThreads } = partitionFixThreads(result.fix.threads, result.fix.resolveCommand, result.fix.resolveOnlyCommand);
|
|
35
|
+
renderThreads("## Review threads", [...locatedThreads, ...unlocatedMutatedThreads]);
|
|
33
36
|
renderThreads("## Unlocated review threads (logged once — no mutation)", unlocatedThreads);
|
|
34
37
|
if (result.fix.resolutionOnlyThreads.length > 0) {
|
|
35
38
|
sections.push("## Review threads to resolve");
|
|
36
39
|
sections.push(result.fix.resolutionOnlyThreads
|
|
37
|
-
.map((t) => renderThreadBullet(t, { statusTag: renderThreadResolutionStatusTag(t) }))
|
|
40
|
+
.map((t) => renderThreadBullet(t, { statusTag: renderThreadResolutionStatusTag(t), verbose }))
|
|
38
41
|
.join("\n"));
|
|
39
42
|
}
|
|
40
43
|
if (result.fix.actionableComments.length > 0) {
|
|
@@ -44,7 +47,7 @@ export function formatFixCodeResult(header, result) {
|
|
|
44
47
|
const editedMarker = c.edited ? " [edited since first look]" : "";
|
|
45
48
|
const authorizationMarker = c.viewerCanMinimize === false ? " [viewer cannot minimize]" : "";
|
|
46
49
|
sections.push(`### ${heading} (${renderAuthor(c.author, c.authorType, c.authorAssociation)})${authorizationMarker}${editedMarker}`);
|
|
47
|
-
sections.push(blockquote(c.body));
|
|
50
|
+
sections.push(blockquote(c.body, topCap, c.url));
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
const failingChecks = result.fix.checks.filter((ch) => isFailingAgentCheck(ch));
|
|
@@ -87,10 +90,17 @@ export function formatFixCodeResult(header, result) {
|
|
|
87
90
|
});
|
|
88
91
|
sections.push(bullets.join("\n\n"));
|
|
89
92
|
}
|
|
90
|
-
const
|
|
91
|
-
|
|
93
|
+
const annotatedChecks = result.fix.checks
|
|
94
|
+
.map((ch) => ({
|
|
95
|
+
ch,
|
|
96
|
+
rendered: (ch.annotations ?? [])
|
|
97
|
+
.map((a) => renderCheckAnnotation(a, ch.logExcerpt))
|
|
98
|
+
.filter((s) => s !== null),
|
|
99
|
+
}))
|
|
100
|
+
.filter(({ rendered }) => rendered.length > 0);
|
|
101
|
+
if (annotatedChecks.length > 0) {
|
|
92
102
|
sections.push("## Check annotations");
|
|
93
|
-
for (const ch of
|
|
103
|
+
for (const { ch, rendered } of annotatedChecks) {
|
|
94
104
|
const workflowPrefix = ch.workflowName ? `${ch.workflowName} › ` : "";
|
|
95
105
|
const jobLabel = ch.jobName ? ch.jobName : ch.name;
|
|
96
106
|
const locator = ch.runId
|
|
@@ -99,25 +109,41 @@ export function formatFixCodeResult(header, result) {
|
|
|
99
109
|
? `external \`${ch.detailsUrl}\``
|
|
100
110
|
: "(no runId)";
|
|
101
111
|
sections.push(`### ${locator} — \`${workflowPrefix}${jobLabel}\``);
|
|
102
|
-
sections.push(
|
|
112
|
+
sections.push(rendered.join("\n\n"));
|
|
103
113
|
}
|
|
104
114
|
}
|
|
105
115
|
if (result.fix.changesRequestedReviews.length > 0) {
|
|
106
116
|
sections.push("## Changes-requested reviews");
|
|
107
|
-
|
|
117
|
+
for (const r of result.fix.changesRequestedReviews) {
|
|
118
|
+
// A stale bot CR that already had its full body surfaced on a prior tick stays a terse
|
|
119
|
+
// one-line reminder (renderReviewBullet's staleBotCr branch) — repeating the full body
|
|
120
|
+
// every tick would be noise, not new content. Every other review (a bot CR's first
|
|
121
|
+
// emission, or any human CR) gets the same H3 + blockquote shape as the sibling
|
|
122
|
+
// review-summary/approval sections below, so the agent actually has a body to read —
|
|
123
|
+
// the bare bullet this replaced only ever rendered the reviewId, never the body.
|
|
124
|
+
if (r.staleBotCr) {
|
|
125
|
+
sections.push(renderReviewBullet(r));
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const staleTag = r.staleReview
|
|
129
|
+
? " [stale — review is on an old commit, all threads resolved; ask reviewer to re-review or dismiss]"
|
|
130
|
+
: "";
|
|
131
|
+
sections.push(`### \`reviewId=${r.id}\` (${renderAuthor(r.author, r.authorType, r.authorAssociation)})${staleTag}`);
|
|
132
|
+
sections.push(r.body.trim() === "" ? "(no review body)" : blockquote(r.body, topCap));
|
|
133
|
+
}
|
|
108
134
|
}
|
|
109
135
|
if (result.fix.firstLookSummaries.length > 0) {
|
|
110
136
|
sections.push("## Review summaries (first look)");
|
|
111
137
|
for (const r of result.fix.firstLookSummaries) {
|
|
112
138
|
sections.push(`### \`reviewId=${r.id}\` (${renderAuthor(r.author, r.authorType, r.authorAssociation)})${r.viewerCanMinimize === false ? " [viewer cannot minimize]" : ""}`);
|
|
113
|
-
sections.push(r.body.trim() === "" ? "(no review body)" : blockquote(r.body));
|
|
139
|
+
sections.push(r.body.trim() === "" ? "(no review body)" : blockquote(r.body, topCap));
|
|
114
140
|
}
|
|
115
141
|
}
|
|
116
142
|
if (result.fix.editedSummaries.length > 0) {
|
|
117
143
|
sections.push("## Review summaries (edited since first look — already minimized; do not re-minimize)");
|
|
118
144
|
for (const r of result.fix.editedSummaries) {
|
|
119
145
|
sections.push(`### \`reviewId=${r.id}\` (${renderAuthor(r.author, r.authorType, r.authorAssociation)})${r.viewerCanMinimize === false ? " [viewer cannot minimize]" : ""}`);
|
|
120
|
-
sections.push(r.body.trim() === "" ? "(no review body)" : blockquote(r.body));
|
|
146
|
+
sections.push(r.body.trim() === "" ? "(no review body)" : blockquote(r.body, topCap));
|
|
121
147
|
}
|
|
122
148
|
}
|
|
123
149
|
const firstLookSummaryIds = new Set(result.fix.firstLookSummaries.map((r) => r.id));
|
|
@@ -130,14 +156,14 @@ export function formatFixCodeResult(header, result) {
|
|
|
130
156
|
sections.push("## Approvals (surfaced — not minimized)");
|
|
131
157
|
for (const r of result.fix.surfacedApprovals) {
|
|
132
158
|
sections.push(`### \`reviewId=${r.id}\` (${renderAuthor(r.author, r.authorType, r.authorAssociation)})${r.viewerCanMinimize === false ? " [viewer cannot minimize]" : ""}`);
|
|
133
|
-
sections.push(r.body.trim() === "" ? "(no review body)" : blockquote(r.body));
|
|
159
|
+
sections.push(r.body.trim() === "" ? "(no review body)" : blockquote(r.body, topCap));
|
|
134
160
|
}
|
|
135
161
|
}
|
|
136
162
|
const firstLookTotal = result.fix.firstLookThreads.length + result.fix.firstLookComments.length;
|
|
137
163
|
if (firstLookTotal > 0) {
|
|
138
164
|
sections.push(`## First-look items (${firstLookTotal}) — acknowledge status before acting`);
|
|
139
165
|
const resolutionOnlyIds = new Set(result.fix.resolutionOnlyThreads.map((t) => t.id));
|
|
140
|
-
sections.push(buildFirstLookBullets(result.fix.firstLookThreads, resolutionOnlyIds, result.fix.firstLookComments).join("\n"));
|
|
166
|
+
sections.push(buildFirstLookBullets(result.fix.firstLookThreads, resolutionOnlyIds, result.fix.firstLookComments, verbose).join("\n"));
|
|
141
167
|
}
|
|
142
168
|
if (result.fix.inProgressRunIds.length > 0) {
|
|
143
169
|
sections.push("## In-progress runs");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const TOP_USAGE = "pr-shepherd\n\nAutonomous PR CI monitor and review-comment resolver for agentic coding tools.\n\nUsage:\n pr-shepherd --version | -v\n pr-shepherd --help | -h\n pr-shepherd [PR] [poll-flags] [iterate-flags]\n pr-shepherd iterate [PR] [iterate-flags]\n pr-shepherd apply review [PR] [review-flags]\n pr-shepherd apply files [PR] [files...] [--tests] [--match REGEX]\n pr-shepherd apply journal [PR] <item> [--dry-run] [--format text|json]\n pr-shepherd journal extract --body-file <path>\n pr-shepherd build-suggestion-patches [PR] --thread-id ID --message MSG [groups...]\n pr-shepherd admin clean <pr|branch|current|repo|all> [value] [flags]\n pr-shepherd admin log-file [--format text|json]\n\nCommands:\n [PR] Poll until non-WAIT or timeout. This is the default command.\n iterate Run one iterate tick (single-tick alias).\n apply review Apply review-state mutations after fixes.\n apply files Mark selected changed files as viewed.\n apply journal Append a list item to the Shepherd Journal details block of a PR body.\n journal extract Extract a validated Shepherd Journal from a local PR-body file as JSON.\n build-suggestion-patches\n Convert ordered GitHub suggestion threads into patches and commit instructions.\n admin clean Remove pr-shepherd state files.\n admin log-file Print the per-worktree debug log path.\n\nPR argument:\n PR may be a number such as 42 or a GitHub pull request URL.\n When omitted, pr-shepherd infers the current branch's pull request.\n\nCommon flags:\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed poll-tick lines.\n --help, -h Print help and exit before any GitHub, git, config, or log I/O.\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Legacy no-op; workflow runs are never cancelled.\n\nPolling flags:\n --interval <duration> Delay between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Poll wall-clock cap for WAIT ticks. Bare number = seconds. Default: 4.5m.\n --debounce <duration> Settle window after first FIX_CODE before returning. Bare number = seconds. Default: 60s. 0 disables.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nClean variants:\n pr [number] Remove state for one PR. Defaults to current branch PR.\n branch [name] Remove state for a branch's PR. Defaults to current branch.\n current Alias for branch against the current branch.\n repo Remove all state for the current repository.\n all Remove all pr-shepherd state.\n\nExit codes: 0 done, 10-19 PR state, 64-78 shepherd failed (sysexits.h).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\nSee docs/exit-codes.md for the full sysexits.h error-code table.\n\nDuration examples: 30s, 4.5m, 1h. A bare number uses each flag's default unit (see above); decimals are allowed with an explicit unit (4.5m).\n\nRun 'pr-shepherd <command> --help' for command-specific details.";
|
|
1
|
+
export declare const TOP_USAGE = "pr-shepherd\n\nAutonomous PR CI monitor and review-comment resolver for agentic coding tools.\n\nUsage:\n pr-shepherd --version | -v\n pr-shepherd --help | -h\n pr-shepherd [PR] [poll-flags] [iterate-flags]\n pr-shepherd iterate [PR] [iterate-flags]\n pr-shepherd apply review [PR] [review-flags]\n pr-shepherd apply files [PR] [files...] [--tests] [--match REGEX]\n pr-shepherd apply journal [PR] <item> [--dry-run] [--format text|json]\n pr-shepherd journal extract --body-file <path>\n pr-shepherd build-suggestion-patches [PR] --thread-id ID --message MSG [groups...]\n pr-shepherd admin clean <pr|branch|current|repo|all> [value] [flags]\n pr-shepherd admin log-file [--format text|json]\n\nCommands:\n [PR] Poll until non-WAIT or timeout. This is the default command.\n iterate Run one iterate tick (single-tick alias).\n apply review Apply review-state mutations after fixes.\n apply files Mark selected changed files as viewed.\n apply journal Append a list item to the Shepherd Journal details block of a PR body.\n journal extract Extract a validated Shepherd Journal from a local PR-body file as JSON.\n build-suggestion-patches\n Convert ordered GitHub suggestion threads into patches and commit instructions.\n admin clean Remove pr-shepherd state files.\n admin log-file Print the per-worktree debug log path.\n\nPR argument:\n PR may be a number such as 42 or a GitHub pull request URL.\n When omitted, pr-shepherd infers the current branch's pull request.\n\nCommon flags:\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed poll-tick lines.\n --help, -h Print help and exit before any GitHub, git, config, or log I/O.\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Legacy no-op; workflow runs are never cancelled.\n --merge Shepherd through readiness, then emit a merge or merge-queue command.\n\nPolling flags:\n --interval <duration> Delay between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Poll wall-clock cap for WAIT ticks. Bare number = seconds. Default: 4.5m.\n --debounce <duration> Settle window after first FIX_CODE before returning. Bare number = seconds. Default: 60s. 0 disables.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nClean variants:\n pr [number] Remove state for one PR. Defaults to current branch PR.\n branch [name] Remove state for a branch's PR. Defaults to current branch.\n current Alias for branch against the current branch.\n repo Remove all state for the current repository.\n all Remove all pr-shepherd state.\n\nExit codes: 0 done, 10-19 PR state, 64-78 shepherd failed (sysexits.h).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\n 15 MERGE\nSee docs/exit-codes.md for the full sysexits.h error-code table.\n\nDuration examples: 30s, 4.5m, 1h. A bare number uses each flag's default unit (see above); decimals are allowed with an explicit unit (4.5m).\n\nRun 'pr-shepherd <command> --help' for command-specific details.";
|
|
@@ -41,6 +41,7 @@ Iterate flags:
|
|
|
41
41
|
--stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.
|
|
42
42
|
--no-auto-mark-ready Do not convert draft PRs to ready for review.
|
|
43
43
|
--no-auto-cancel-actionable Legacy no-op; workflow runs are never cancelled.
|
|
44
|
+
--merge Shepherd through readiness, then emit a merge or merge-queue command.
|
|
44
45
|
|
|
45
46
|
Polling flags:
|
|
46
47
|
--interval <duration> Delay between WAIT ticks. Bare number = seconds. Default: 60s.
|
|
@@ -63,6 +64,7 @@ Exit codes: 0 done, 10-19 PR state, 64-78 shepherd failed (sysexits.h).
|
|
|
63
64
|
12 FIX_CODE
|
|
64
65
|
13 ESCALATE
|
|
65
66
|
14 CANCEL (closed without merging)
|
|
67
|
+
15 MERGE
|
|
66
68
|
See docs/exit-codes.md for the full sysexits.h error-code table.
|
|
67
69
|
|
|
68
70
|
Duration examples: 30s, 4.5m, 1h. A bare number uses each flag's default unit (see above); decimals are allowed with an explicit unit (4.5m).
|
package/bin/cli/help.d.mts
CHANGED
|
@@ -258,7 +258,7 @@ On POSIX, the final body-file path entry must be a readable regular file in a tr
|
|
|
258
258
|
symlinks, FIFOs, devices, and unreadable paths exit 66. Unsupported platforms fail closed with exit 66.
|
|
259
259
|
--help, -h Print this help and exit before any I/O.`;
|
|
260
260
|
readonly "log-file": "pr-shepherd log-file\n\nPrint the per-worktree append-only debug log path for the current repository.\nThe log is created by the first non-help pr-shepherd command that initializes logging.\n\nUsage:\n pr-shepherd log-file [--format text|json]\n\nFlags:\n --format text|json Print a raw path or {\"path\": \"...\"} JSON. Default: text.\n --help, -h Print this help and exit before logging setup.\n\nEnvironment:\n PR_SHEPHERD_LOG_DISABLED=1 disables logging.\n PR_SHEPHERD_STATE_DIR overrides the base state directory.\n\nExit code: 0 on success; 1 if repository identity cannot be resolved.";
|
|
261
|
-
readonly top: "pr-shepherd\n\nAutonomous PR CI monitor and review-comment resolver for agentic coding tools.\n\nUsage:\n pr-shepherd --version | -v\n pr-shepherd --help | -h\n pr-shepherd [PR] [poll-flags] [iterate-flags]\n pr-shepherd iterate [PR] [iterate-flags]\n pr-shepherd apply review [PR] [review-flags]\n pr-shepherd apply files [PR] [files...] [--tests] [--match REGEX]\n pr-shepherd apply journal [PR] <item> [--dry-run] [--format text|json]\n pr-shepherd journal extract --body-file <path>\n pr-shepherd build-suggestion-patches [PR] --thread-id ID --message MSG [groups...]\n pr-shepherd admin clean <pr|branch|current|repo|all> [value] [flags]\n pr-shepherd admin log-file [--format text|json]\n\nCommands:\n [PR] Poll until non-WAIT or timeout. This is the default command.\n iterate Run one iterate tick (single-tick alias).\n apply review Apply review-state mutations after fixes.\n apply files Mark selected changed files as viewed.\n apply journal Append a list item to the Shepherd Journal details block of a PR body.\n journal extract Extract a validated Shepherd Journal from a local PR-body file as JSON.\n build-suggestion-patches\n Convert ordered GitHub suggestion threads into patches and commit instructions.\n admin clean Remove pr-shepherd state files.\n admin log-file Print the per-worktree debug log path.\n\nPR argument:\n PR may be a number such as 42 or a GitHub pull request URL.\n When omitted, pr-shepherd infers the current branch's pull request.\n\nCommon flags:\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed poll-tick lines.\n --help, -h Print help and exit before any GitHub, git, config, or log I/O.\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Legacy no-op; workflow runs are never cancelled.\n\nPolling flags:\n --interval <duration> Delay between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Poll wall-clock cap for WAIT ticks. Bare number = seconds. Default: 4.5m.\n --debounce <duration> Settle window after first FIX_CODE before returning. Bare number = seconds. Default: 60s. 0 disables.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nClean variants:\n pr [number] Remove state for one PR. Defaults to current branch PR.\n branch [name] Remove state for a branch's PR. Defaults to current branch.\n current Alias for branch against the current branch.\n repo Remove all state for the current repository.\n all Remove all pr-shepherd state.\n\nExit codes: 0 done, 10-19 PR state, 64-78 shepherd failed (sysexits.h).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\nSee docs/exit-codes.md for the full sysexits.h error-code table.\n\nDuration examples: 30s, 4.5m, 1h. A bare number uses each flag's default unit (see above); decimals are allowed with an explicit unit (4.5m).\n\nRun 'pr-shepherd <command> --help' for command-specific details.";
|
|
261
|
+
readonly top: "pr-shepherd\n\nAutonomous PR CI monitor and review-comment resolver for agentic coding tools.\n\nUsage:\n pr-shepherd --version | -v\n pr-shepherd --help | -h\n pr-shepherd [PR] [poll-flags] [iterate-flags]\n pr-shepherd iterate [PR] [iterate-flags]\n pr-shepherd apply review [PR] [review-flags]\n pr-shepherd apply files [PR] [files...] [--tests] [--match REGEX]\n pr-shepherd apply journal [PR] <item> [--dry-run] [--format text|json]\n pr-shepherd journal extract --body-file <path>\n pr-shepherd build-suggestion-patches [PR] --thread-id ID --message MSG [groups...]\n pr-shepherd admin clean <pr|branch|current|repo|all> [value] [flags]\n pr-shepherd admin log-file [--format text|json]\n\nCommands:\n [PR] Poll until non-WAIT or timeout. This is the default command.\n iterate Run one iterate tick (single-tick alias).\n apply review Apply review-state mutations after fixes.\n apply files Mark selected changed files as viewed.\n apply journal Append a list item to the Shepherd Journal details block of a PR body.\n journal extract Extract a validated Shepherd Journal from a local PR-body file as JSON.\n build-suggestion-patches\n Convert ordered GitHub suggestion threads into patches and commit instructions.\n admin clean Remove pr-shepherd state files.\n admin log-file Print the per-worktree debug log path.\n\nPR argument:\n PR may be a number such as 42 or a GitHub pull request URL.\n When omitted, pr-shepherd infers the current branch's pull request.\n\nCommon flags:\n --format text|json Output Markdown text or JSON. Default: text.\n --verbose Include verbose iterate fields and detailed poll-tick lines.\n --help, -h Print help and exit before any GitHub, git, config, or log I/O.\n\nIterate flags:\n --ready-delay <duration> Settle window before a clean PR cancels. Bare number = minutes. Example: 15m.\n --stall-timeout <duration> Escalate repeated unchanged failures after this duration. Bare number = minutes. 0 disables.\n --no-auto-mark-ready Do not convert draft PRs to ready for review.\n --no-auto-cancel-actionable Legacy no-op; workflow runs are never cancelled.\n --merge Shepherd through readiness, then emit a merge or merge-queue command.\n\nPolling flags:\n --interval <duration> Delay between WAIT ticks. Bare number = seconds. Default: 60s.\n --timeout <duration> Poll wall-clock cap for WAIT ticks. Bare number = seconds. Default: 4.5m.\n --debounce <duration> Settle window after first FIX_CODE before returning. Bare number = seconds. Default: 60s. 0 disables.\n --quiet-status During WAIT polling, print only changed status snapshots.\n --until-terminal Continue through WAIT/MARK_READY until FIX_CODE/CANCEL/ESCALATE.\n\nClean variants:\n pr [number] Remove state for one PR. Defaults to current branch PR.\n branch [name] Remove state for a branch's PR. Defaults to current branch.\n current Alias for branch against the current branch.\n repo Remove all state for the current repository.\n all Remove all pr-shepherd state.\n\nExit codes: 0 done, 10-19 PR state, 64-78 shepherd failed (sysexits.h).\n 0 CANCEL (merged or ready-delay elapsed)\n 10 WAIT\n 11 MARK_READY\n 12 FIX_CODE\n 13 ESCALATE\n 14 CANCEL (closed without merging)\n 15 MERGE\nSee docs/exit-codes.md for the full sysexits.h error-code table.\n\nDuration examples: 30s, 4.5m, 1h. A bare number uses each flag's default unit (see above); decimals are allowed with an explicit unit (4.5m).\n\nRun 'pr-shepherd <command> --help' for command-specific details.";
|
|
262
262
|
};
|
|
263
263
|
/** Resolve help keys for nested public commands before any command I/O. */
|
|
264
264
|
export declare function helpKeyForArgs(args: string[]): keyof typeof USAGE;
|
|
@@ -14,7 +14,7 @@ function formatRelevantCheck(check) {
|
|
|
14
14
|
const lines = [`- \`${workflow}${job}\` [conclusion: ${check.conclusion}]`];
|
|
15
15
|
appendCheckFields(lines, check);
|
|
16
16
|
appendLogExcerpt(lines, check.logExcerpt);
|
|
17
|
-
appendAnnotations(lines, check.annotations);
|
|
17
|
+
appendAnnotations(lines, check.annotations, check.logExcerpt);
|
|
18
18
|
return lines;
|
|
19
19
|
}
|
|
20
20
|
function appendCheckFields(lines, check) {
|
|
@@ -43,13 +43,15 @@ function appendLogExcerpt(lines, logExcerpt) {
|
|
|
43
43
|
for (const line of logExcerpt.split("\n"))
|
|
44
44
|
lines.push(` > ${line}`);
|
|
45
45
|
}
|
|
46
|
-
function appendAnnotations(lines, annotations) {
|
|
47
|
-
|
|
46
|
+
function appendAnnotations(lines, annotations, logExcerpt) {
|
|
47
|
+
const rendered = (annotations ?? [])
|
|
48
|
+
.map((a) => renderCheckAnnotation(a, logExcerpt))
|
|
49
|
+
.filter((s) => s !== null);
|
|
50
|
+
if (rendered.length === 0)
|
|
48
51
|
return;
|
|
49
52
|
lines.push(" - annotations:");
|
|
50
|
-
for (const
|
|
51
|
-
for (const line of
|
|
53
|
+
for (const text of rendered) {
|
|
54
|
+
for (const line of text.split("\n"))
|
|
52
55
|
lines.push(` ${line}`);
|
|
53
|
-
}
|
|
54
56
|
}
|
|
55
57
|
}
|
|
@@ -3,7 +3,7 @@ import { formatRelevantChecks } from "./iterate-checks-formatter.mjs";
|
|
|
3
3
|
import { joinSections } from "../util/markdown.mjs";
|
|
4
4
|
import { adaptIterateLog, buildSimpleIterateInstructions, numberInstructions, } from "./iterate-instructions.mjs";
|
|
5
5
|
import { formatMergeRequirementLines } from "../merge-status/requirements-format.mjs";
|
|
6
|
-
import { appendMergeQueueHeader, formatMergeAction } from "./iterate-merge-formatter.mjs";
|
|
6
|
+
import { appendMergeQueueHeader, formatDeferredWorkLine, formatMergeAction, } from "./iterate-merge-formatter.mjs";
|
|
7
7
|
import { formatApiUsage, formatQuotaWarning } from "./api-usage-formatter.mjs";
|
|
8
8
|
import { formatActivityLine } from "./iterate-activity-formatter.mjs";
|
|
9
9
|
/**
|
|
@@ -115,13 +115,13 @@ export function formatIterateResult(result, opts) {
|
|
|
115
115
|
const verboseChecks = verbose ? formatRelevantChecks(result.checks) : null;
|
|
116
116
|
const telemetrySections = [quotaWarning, apiUsage, verboseChecks];
|
|
117
117
|
switch (result.action) {
|
|
118
|
-
case "wait":
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
118
|
+
case "wait": {
|
|
119
|
+
const waitLines = [header, ...telemetrySections, adaptIterateLog(result.log)];
|
|
120
|
+
if (result.deferredWork)
|
|
121
|
+
waitLines.push(formatDeferredWorkLine(result.deferredWork));
|
|
122
|
+
waitLines.push(`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`);
|
|
123
|
+
return joinSections(waitLines);
|
|
124
|
+
}
|
|
125
125
|
case "mark_ready":
|
|
126
126
|
return joinSections([
|
|
127
127
|
header,
|
|
@@ -147,6 +147,7 @@ export function formatIterateResult(result, opts) {
|
|
|
147
147
|
const supersededStr = result.supersededNames.map((n) => "`" + n + "`").join(", ");
|
|
148
148
|
cancelHeaderLines.push(`**superseded** ${supersededStr}`);
|
|
149
149
|
}
|
|
150
|
+
appendMergeQueueHeader(cancelHeaderLines, result);
|
|
150
151
|
if (activityLine)
|
|
151
152
|
cancelHeaderLines.push(activityLine);
|
|
152
153
|
return joinSections([
|
|
@@ -166,6 +167,6 @@ export function formatIterateResult(result, opts) {
|
|
|
166
167
|
`## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`,
|
|
167
168
|
]);
|
|
168
169
|
case "fix_code":
|
|
169
|
-
return formatFixCodeResult(joinSections([header, ...telemetrySections]), result);
|
|
170
|
+
return formatFixCodeResult(joinSections([header, ...telemetrySections]), result, { verbose });
|
|
170
171
|
}
|
|
171
172
|
}
|
package/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -23,6 +23,7 @@ export function projectIterateLean(result, opts) {
|
|
|
23
23
|
status: result.status,
|
|
24
24
|
state: result.state,
|
|
25
25
|
mergeStateStatus: result.mergeStateStatus,
|
|
26
|
+
...(result.mergeStatus !== "CLEAN" && { mergeStatus: result.mergeStatus }), // mergeStateStatus alone can't always reconstruct this
|
|
26
27
|
...(readyDelaySuffix && { readyDelayOverride: readyDelaySuffix }),
|
|
27
28
|
...(result.mergeStatus === "BLOCKED" &&
|
|
28
29
|
result.reviewDecision !== null && { reviewDecision: result.reviewDecision }),
|
|
@@ -70,6 +71,7 @@ export function projectIterateLean(result, opts) {
|
|
|
70
71
|
case "wait":
|
|
71
72
|
return {
|
|
72
73
|
...base,
|
|
74
|
+
...(result.deferredWork && { deferredWork: result.deferredWork }),
|
|
73
75
|
log: adaptIterateLog(result.log),
|
|
74
76
|
instructions: simpleInstructions(result),
|
|
75
77
|
};
|
|
@@ -173,6 +175,9 @@ export function projectIterateLean(result, opts) {
|
|
|
173
175
|
...(result.escalate.mergeQueueRemoval && {
|
|
174
176
|
mergeQueueRemoval: result.escalate.mergeQueueRemoval,
|
|
175
177
|
}),
|
|
178
|
+
...(result.escalate.stack && {
|
|
179
|
+
stack: result.escalate.stack,
|
|
180
|
+
}),
|
|
176
181
|
...(result.escalate.authorization &&
|
|
177
182
|
result.escalate.authorization.length > 0 && {
|
|
178
183
|
authorization: result.escalate.authorization,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
import type { IterateResult, IterateResultMerge } from "../types.mts";
|
|
1
|
+
import type { IterateDeferredWork, IterateResult, IterateResultMerge } from "../types.mts";
|
|
2
|
+
/** One inline rollup line of the non-CI work held back while the PR sits in the merge queue. */
|
|
3
|
+
export declare function formatDeferredWorkLine(dw: IterateDeferredWork): string;
|
|
2
4
|
export declare function appendMergeQueueHeader(lines: string[], result: IterateResult): void;
|
|
3
5
|
export declare function formatMergeAction(header: string, result: IterateResultMerge): string;
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { renderMergeCommand } from "../commands/iterate/merge.mjs";
|
|
2
2
|
import { inlineCode, joinSections } from "../util/markdown.mjs";
|
|
3
3
|
import { buildSimpleIterateInstructions, numberInstructions } from "./iterate-instructions.mjs";
|
|
4
|
+
/** One inline rollup line of the non-CI work held back while the PR sits in the merge queue. */
|
|
5
|
+
export function formatDeferredWorkLine(dw) {
|
|
6
|
+
const parts = [];
|
|
7
|
+
if (dw.threads > 0)
|
|
8
|
+
parts.push(`${dw.threads} thread${dw.threads === 1 ? "" : "s"}`);
|
|
9
|
+
if (dw.comments > 0)
|
|
10
|
+
parts.push(`${dw.comments} comment${dw.comments === 1 ? "" : "s"}`);
|
|
11
|
+
if (dw.changesRequestedReviews > 0) {
|
|
12
|
+
parts.push(`${dw.changesRequestedReviews} changes-requested review${dw.changesRequestedReviews === 1 ? "" : "s"}`);
|
|
13
|
+
}
|
|
14
|
+
if (dw.reviewSummaries > 0) {
|
|
15
|
+
parts.push(`${dw.reviewSummaries} review summar${dw.reviewSummaries === 1 ? "y" : "ies"}`);
|
|
16
|
+
}
|
|
17
|
+
return `**deferred (in merge queue)** ${parts.join(", ")}`;
|
|
18
|
+
}
|
|
4
19
|
export function appendMergeQueueHeader(lines, result) {
|
|
5
20
|
const queue = result.mergeQueue;
|
|
6
21
|
if (!queue)
|