pr-shepherd 0.59.1 → 0.59.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -3
- package/bin/classify/types.d.mts +1 -1
- package/bin/cli/help-command-pages.d.mts +2 -2
- package/bin/cli/help-iterate-poll-pages.d.mts +2 -2
- package/bin/cli/help-iterate-poll-pages.mjs +5 -5
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +2 -2
- package/bin/cli/help.d.mts +3 -3
- package/bin/cli/iterate-instructions.mjs +1 -31
- package/bin/cli/iterate-lean.mjs +0 -1
- package/bin/commands/check.mjs +0 -2
- package/bin/commands/event-mode.mjs +3 -7
- package/bin/commands/iterate/fix-loop-instruction.d.mts +2 -8
- package/bin/commands/iterate/fix-loop-instruction.mjs +8 -29
- package/bin/commands/iterate/index.mjs +12 -3
- package/bin/commands/iterate/render.d.mts +1 -1
- package/bin/commands/iterate/render.mjs +17 -13
- package/bin/commands/next-check.d.mts +0 -2
- package/bin/commands/next-check.mjs +1 -1
- package/bin/commands/poll-summary-explicit-instructions.d.mts +2 -0
- package/bin/commands/poll-summary-explicit-instructions.mjs +18 -2
- package/bin/commands/poll-summary-instructions.mjs +33 -11
- package/bin/commands/rule-auto-resolve-format.d.mts +0 -12
- package/bin/commands/rule-auto-resolve-format.mjs +2 -11
- package/bin/commands/rule-auto-resolve.d.mts +0 -5
- package/bin/commands/rule-auto-resolve.mjs +1 -42
- package/bin/commands/shepherd-journal.d.mts +0 -9
- package/bin/commands/shepherd-journal.mjs +0 -15
- package/bin/commands/stack-drain.mjs +1 -2
- package/bin/commands/stack-work.d.mts +8 -21
- package/bin/commands/stack-work.mjs +14 -38
- package/bin/config/load.d.mts +2 -2
- package/bin/exit-codes.d.mts +1 -1
- package/bin/exit-codes.mjs +2 -1
- package/bin/github/poll-summary-projector.mjs +3 -1
- package/bin/github/poll-summary-route.mjs +6 -7
- package/bin/mcp/server.mjs +1 -1
- package/bin/types/iterate.d.mts +2 -3
- package/bin/types/merge-requirements.d.mts +0 -7
- package/package.json +1 -1
- package/plugins/pr-shepherd/.claude-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- package/plugins/pr-shepherd/.mcp.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +8 -6
- package/plugins/pr-shepherd/skills/pr-shepherd/references/cloud-event-loop.md +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/references/draft-handoff.md +38 -0
- package/plugins/pr-shepherd/skills/pr-shepherd/references/fix-code-loop.md +1 -2
- package/plugins/pr-shepherd/skills/pr-shepherd/references/merge-queue-ejection.md +1 -1
- package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +1 -1
- package/src/classify/types.mts +1 -1
- package/bin/commands/iterate/parent-first.d.mts +0 -8
- package/bin/commands/iterate/parent-first.mjs +0 -13
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import { buildQuotaAwareContinuation } from "../quota-warning.mjs";
|
|
2
|
+
import { isDraftReadinessVerification } from "./stack-work.mjs";
|
|
2
3
|
/** Every selected row already returned `CANCEL` or `ESCALATE`: no autonomous work remains. */
|
|
3
4
|
export function isHandedOffSelection(result) {
|
|
4
5
|
return (result.prs.length > 0 &&
|
|
5
6
|
result.prs.every((item) => item.action === "cancel" || item.action === "escalate"));
|
|
6
7
|
}
|
|
8
|
+
/** A final full-review probe can finish this selection without another aggregate read. */
|
|
9
|
+
export function onlyDraftVerificationSelection(result) {
|
|
10
|
+
return (result.prs.some(isDraftReadinessVerification) &&
|
|
11
|
+
result.prs.every((item) => item.action === "cancel" ||
|
|
12
|
+
item.action === "escalate" ||
|
|
13
|
+
isDraftReadinessVerification(item)));
|
|
14
|
+
}
|
|
7
15
|
export function explicitInstructions(result) {
|
|
8
16
|
if (result.reason === "all_terminal")
|
|
9
|
-
return [
|
|
17
|
+
return [
|
|
18
|
+
result.prs.some((item) => item.reasons.includes("draft-auto-mark-ready-disabled"))
|
|
19
|
+
? "1. Stop — every selected PR is complete for this Shepherd run."
|
|
20
|
+
: "1. Stop — every selected PR is terminal.",
|
|
21
|
+
];
|
|
10
22
|
if (result.quotaWarning && result.reason !== "actionable") {
|
|
11
23
|
return [
|
|
12
24
|
buildQuotaAwareContinuation(result.quotaWarning, "1. This aggregate selection is non-terminal. Before continuing,"),
|
|
@@ -20,7 +32,11 @@ export function explicitInstructions(result) {
|
|
|
20
32
|
"2. Follow each selected one-PR poll's `## Instructions` until it returns `CANCEL` or `ESCALATE`.",
|
|
21
33
|
"3. Run this aggregate poll again after selected work completes; one row's `ESCALATE` does not stop work on other rows.",
|
|
22
34
|
];
|
|
23
|
-
if (
|
|
35
|
+
if (onlyDraftVerificationSelection(result)) {
|
|
36
|
+
instructions[2] =
|
|
37
|
+
"3. If every draft verification probe returns `[CANCEL]` with reason `auto-mark-ready-disabled`, stop this selection and report any `ESCALATE` rows; leave the drafts unchanged. If any probe returns `[ESCALATE]`, report that handoff and stop after the other probes finish. Otherwise follow the probes' instructions and rerun this aggregate selector when that work completes.";
|
|
38
|
+
}
|
|
39
|
+
else if (isHandedOffSelection(result)) {
|
|
24
40
|
instructions[2] =
|
|
25
41
|
"3. Stop after reporting each `ESCALATE` handoff; every selected row is `CANCEL` or `ESCALATE`, so do not run this aggregate poll again.";
|
|
26
42
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { buildQuotaAwareContinuation } from "../quota-warning.mjs";
|
|
3
3
|
import { explicitInstructions } from "./poll-summary-explicit-instructions.mjs";
|
|
4
4
|
import { appendAutonomousInstructions, appendHumanHandoffInstructions, findHumanHandoffs, idleWaitPlan, isStackLayerReady, planPrefixDrain, retargetWaitPlan, stackPosition, } from "./stack-drain.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { appendStackRerunInstruction, onlyDraftReadinessVerification, splitStackWork, } from "./stack-work.mjs";
|
|
6
6
|
/** Keep aggregate JSON, Markdown, and MCP instructions on one projection. */
|
|
7
7
|
export function withPollSummaryInstructions(result, mergeRequested) {
|
|
8
8
|
return planPollSummary(result, mergeRequested).result;
|
|
@@ -19,7 +19,8 @@ export function planPollSummary(result, mergeRequested) {
|
|
|
19
19
|
prs: prs.map((item) => {
|
|
20
20
|
const needsOwnSession = item.state === "OPEN" &&
|
|
21
21
|
(!isStackLayerReady(item) || staleChildren.has(item.pr)) &&
|
|
22
|
-
["cancel", "merge"].includes(item.action)
|
|
22
|
+
["cancel", "merge"].includes(item.action) &&
|
|
23
|
+
(!item.reasons.includes("draft-auto-mark-ready-disabled") || staleChildren.has(item.pr));
|
|
23
24
|
const layer = needsOwnSession
|
|
24
25
|
? {
|
|
25
26
|
...item,
|
|
@@ -51,6 +52,11 @@ export function planPollSummary(result, mergeRequested) {
|
|
|
51
52
|
reasons: [...layer.reasons, "unverified-stack-state"],
|
|
52
53
|
};
|
|
53
54
|
}
|
|
55
|
+
if (layer.reasons.includes("draft-auto-mark-ready-disabled") &&
|
|
56
|
+
!staleChildren.has(layer.pr)) {
|
|
57
|
+
const { pollCommand: _pollCommand, pollProbe: _pollProbe, ...held } = layer;
|
|
58
|
+
return held;
|
|
59
|
+
}
|
|
54
60
|
return layer;
|
|
55
61
|
}),
|
|
56
62
|
};
|
|
@@ -84,9 +90,9 @@ function planStack(result, mergeRequested) {
|
|
|
84
90
|
const work = splitStackWork(open.filter((item) => (!isStackLayerReady(item) || staleChildren.has(item.pr)) && item.action !== "escalate"), staleChildren);
|
|
85
91
|
const runnableCandidates = work.sessions.filter((item) => item.pollCommand);
|
|
86
92
|
const missingCommands = work.sessions.filter((item) => !item.pollCommand);
|
|
87
|
-
const agentWork = runnableCandidates.length > 0
|
|
93
|
+
const agentWork = runnableCandidates.length > 0;
|
|
88
94
|
// Only owned work defers a human handoff: rerunning cannot advance another author's layer.
|
|
89
|
-
const ownedWork =
|
|
95
|
+
const ownedWork = runnableCandidates.some((item) => item.owned);
|
|
90
96
|
const drain = planPrefixDrain(result, mergeRequested);
|
|
91
97
|
if (drain)
|
|
92
98
|
return drain;
|
|
@@ -94,14 +100,17 @@ function planStack(result, mergeRequested) {
|
|
|
94
100
|
if (handoffs) {
|
|
95
101
|
const instructions = [];
|
|
96
102
|
appendAutonomousInstructions(instructions, runnableCandidates);
|
|
97
|
-
appendMarkReadyInstructions(instructions, work.markReady);
|
|
98
103
|
const stop = !ownedWork;
|
|
99
104
|
appendHumanHandoffInstructions(instructions, handoffs, stop);
|
|
100
105
|
for (const item of missingCommands) {
|
|
101
106
|
instructions.push(`${instructions.length + 1}. PR #${item.pr} needs a one-PR session, but Shepherd could not produce its command. Ask for direction.`);
|
|
102
107
|
}
|
|
103
108
|
if (ownedWork) {
|
|
104
|
-
|
|
109
|
+
const rerun = onlyDraftReadinessVerification(work.sessions, work.idle) &&
|
|
110
|
+
work.sessions.every((item) => item.owned)
|
|
111
|
+
? "If every listed one-PR probe returns `[CANCEL]` with reason `auto-mark-ready-disabled`, report the human handoff above and stop this selector. If any probe returns `[ESCALATE]`, report that handoff and stop after the other probes finish. Otherwise follow each probe's instructions and rerun this same `--stack` selector when that work completes."
|
|
112
|
+
: "After the listed one-PR sessions, rerun this same `--stack` selector. Stop for the human handoff only when no autonomous shepherding remains.";
|
|
113
|
+
appendStackRerunInstruction(instructions, runnableCandidates, rerun);
|
|
105
114
|
}
|
|
106
115
|
return { action: stop ? "escalate" : "shepherd", stackMergeable: false, instructions };
|
|
107
116
|
}
|
|
@@ -116,21 +125,34 @@ function planStack(result, mergeRequested) {
|
|
|
116
125
|
if (missingCommands.length > 0) {
|
|
117
126
|
const instructions = [];
|
|
118
127
|
appendAutonomousInstructions(instructions, runnableCandidates);
|
|
119
|
-
appendMarkReadyInstructions(instructions, work.markReady);
|
|
120
128
|
for (const item of missingCommands) {
|
|
121
129
|
instructions.push(`${instructions.length + 1}. PR #${item.pr} needs a one-PR session, but Shepherd could not produce its command. ${ownedWork ? "After autonomous shepherding, ask" : "Stop and ask"} for direction.`);
|
|
122
130
|
}
|
|
123
131
|
if (ownedWork) {
|
|
124
|
-
appendStackRerunInstruction(instructions,
|
|
132
|
+
appendStackRerunInstruction(instructions, runnableCandidates, "After the listed one-PR sessions, rerun this same `--stack` selector. Stop for the human handoff only when no autonomous shepherding remains.");
|
|
125
133
|
}
|
|
126
134
|
return { action: ownedWork ? "shepherd" : "escalate", stackMergeable: false, instructions };
|
|
127
135
|
}
|
|
128
|
-
if (!agentWork)
|
|
136
|
+
if (!agentWork) {
|
|
137
|
+
if (work.held.length > 0 && work.idle.length === 0) {
|
|
138
|
+
return {
|
|
139
|
+
action: "cancel",
|
|
140
|
+
stackMergeable: false,
|
|
141
|
+
instructions: [
|
|
142
|
+
`1. Stop shepherding this stack. Automatic mark-ready is disabled and ${work.held.map((item) => `PR #${item.pr}`).join(", ")} ${work.held.length === 1 ? "is" : "are"} clean but still in draft.`,
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
129
146
|
return idleWaitPlan(work.idle);
|
|
147
|
+
}
|
|
130
148
|
const instructions = [];
|
|
131
149
|
appendAutonomousInstructions(instructions, work.sessions);
|
|
132
|
-
|
|
133
|
-
|
|
150
|
+
if (onlyDraftReadinessVerification(work.sessions, work.idle) &&
|
|
151
|
+
work.sessions.every((item) => item.owned)) {
|
|
152
|
+
instructions.push(`${instructions.length + 1}. If every listed one-PR probe returns \`[CANCEL]\` with reason \`auto-mark-ready-disabled\`, stop shepherding this stack and leave those drafts unchanged. If any probe returns \`[ESCALATE]\`, report that handoff and stop after the other probes finish. Otherwise follow each probe's instructions and rerun this same \`--stack\` selector when that work completes.`);
|
|
153
|
+
return { action: "shepherd", stackMergeable: false, instructions };
|
|
154
|
+
}
|
|
155
|
+
const action = appendStackRerunInstruction(instructions, work.sessions, "After the selected one-PR sessions, rerun this same `--stack` selector.");
|
|
134
156
|
return { action, stackMergeable: false, instructions };
|
|
135
157
|
}
|
|
136
158
|
if (open.some((item) => item.isInMergeQueue)) {
|
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
import type { AutoMinimizedItem, AutoResolvedThread, RuleAutoResolveReport, ShepherdReport } from "../types.mts";
|
|
2
2
|
export declare function uniqueReasons(reasons: readonly string[]): string[];
|
|
3
|
-
export declare function reasonClause(reasons: readonly string[]): string | undefined;
|
|
4
|
-
export declare function ruleAutoResolveBody(counts: {
|
|
5
|
-
threads: number;
|
|
6
|
-
comments: number;
|
|
7
|
-
reviewSummaries: number;
|
|
8
|
-
}): string | undefined;
|
|
9
|
-
export declare function formatRuleAutoResolveJournalItem(input: {
|
|
10
|
-
body: string;
|
|
11
|
-
reasons: readonly string[];
|
|
12
|
-
viewerLogin?: string;
|
|
13
|
-
urls: readonly string[];
|
|
14
|
-
}): string;
|
|
15
3
|
export declare function decorateAutoResolveError(error: string, reasons: readonly string[]): string;
|
|
16
4
|
export declare function reasonsForError(error: string, ids: readonly string[], ruleReasons: ReadonlyMap<string, readonly string[]>): string[];
|
|
17
5
|
export declare function ruleAutoResolveFromReport(report: ShepherdReport): RuleAutoResolveReport | undefined;
|
|
@@ -7,7 +7,7 @@ export function uniqueReasons(reasons) {
|
|
|
7
7
|
}
|
|
8
8
|
return unique;
|
|
9
9
|
}
|
|
10
|
-
|
|
10
|
+
function reasonClause(reasons) {
|
|
11
11
|
const unique = uniqueReasons(reasons);
|
|
12
12
|
if (unique.length === 0)
|
|
13
13
|
return undefined;
|
|
@@ -21,7 +21,7 @@ function joinedReason(reasons) {
|
|
|
21
21
|
function plural(count, singular, pluralForm = `${singular}s`) {
|
|
22
22
|
return `${count} ${count === 1 ? singular : pluralForm}`;
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
function ruleAutoResolveBody(counts) {
|
|
25
25
|
const parts = [];
|
|
26
26
|
if (counts.threads > 0)
|
|
27
27
|
parts.push(`auto-resolved ${plural(counts.threads, "thread")}`);
|
|
@@ -39,15 +39,6 @@ function withReasonClause(body, reasons) {
|
|
|
39
39
|
const clause = reasonClause(reasons);
|
|
40
40
|
return clause ? `${body} ${clause}` : body;
|
|
41
41
|
}
|
|
42
|
-
export function formatRuleAutoResolveJournalItem(input) {
|
|
43
|
-
const login = input.viewerLogin?.trim();
|
|
44
|
-
const actor = login ? `as @${login}` : "(token login unavailable)";
|
|
45
|
-
const clause = reasonClause(input.reasons);
|
|
46
|
-
const links = input.urls.map((url) => url.trim()).filter(Boolean);
|
|
47
|
-
const reasonSuffix = clause ? ` ${clause}` : "";
|
|
48
|
-
const linkSuffix = links.length > 0 ? `: ${links.join(", ")}` : "";
|
|
49
|
-
return `- ${input.body} ${actor}${reasonSuffix}${linkSuffix}`;
|
|
50
|
-
}
|
|
51
42
|
export function decorateAutoResolveError(error, reasons) {
|
|
52
43
|
const clause = reasonClause(reasons);
|
|
53
44
|
return clause ? `${error} ${clause}` : error;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { autoMinimizeComments, autoResolveThreads } from "../comments/resolve.mjs";
|
|
2
|
-
import { runJournal } from "./journal/index.mjs";
|
|
3
2
|
import { canGenerateGithubMutation } from "../github/mutation-policy.mjs";
|
|
4
|
-
import { decorateAutoResolveError,
|
|
3
|
+
import { decorateAutoResolveError, minimizedItem, reasonsForError, resolvedThread, uniqueReasons, } from "./rule-auto-resolve-format.mjs";
|
|
5
4
|
const EMPTY = {
|
|
6
5
|
threadIds: [],
|
|
7
6
|
commentIds: [],
|
|
@@ -17,9 +16,6 @@ function idsIn(ids, confirmed) {
|
|
|
17
16
|
function decorateErrors(errors, ids, ruleReasons) {
|
|
18
17
|
return errors.map((error) => decorateAutoResolveError(error, reasonsForError(error, ids, ruleReasons)));
|
|
19
18
|
}
|
|
20
|
-
function successReasons(ids, ruleReasons) {
|
|
21
|
-
return uniqueReasons(ids.flatMap((id) => [...(ruleReasons.get(id) ?? [])]));
|
|
22
|
-
}
|
|
23
19
|
export async function applySuppressedRuleAutoResolve(input) {
|
|
24
20
|
if (!input.enabled) {
|
|
25
21
|
return {
|
|
@@ -70,9 +66,6 @@ export async function applySuppressedRuleAutoResolve(input) {
|
|
|
70
66
|
...minimized.errors.flatMap((error) => reasonsForError(error, minimizeIds, input.partition.ruleReasons)),
|
|
71
67
|
...resolved.errors.flatMap((error) => reasonsForError(error, threadIds, input.partition.ruleReasons)),
|
|
72
68
|
]);
|
|
73
|
-
const journalError = await journalSuccesses(input, autoResolved, autoMinimized);
|
|
74
|
-
if (journalError)
|
|
75
|
-
autoResolveErrors.push(journalError);
|
|
76
69
|
return {
|
|
77
70
|
threadIds: input.partition.ruleAutoResolveThreadIds.filter((id) => !resolvedSet.has(id)),
|
|
78
71
|
commentIds: input.partition.ruleAutoResolveCommentIds.filter((id) => !minimizedSet.has(id)),
|
|
@@ -83,37 +76,3 @@ export async function applySuppressedRuleAutoResolve(input) {
|
|
|
83
76
|
errorReasons,
|
|
84
77
|
};
|
|
85
78
|
}
|
|
86
|
-
async function journalSuccesses(input, autoResolved, autoMinimized) {
|
|
87
|
-
const body = ruleAutoResolveBody({
|
|
88
|
-
threads: autoResolved.length,
|
|
89
|
-
comments: autoMinimized.filter((item) => item.kind === "pr-comment").length,
|
|
90
|
-
reviewSummaries: autoMinimized.filter((item) => item.kind === "review-summary").length,
|
|
91
|
-
});
|
|
92
|
-
if (!body)
|
|
93
|
-
return undefined;
|
|
94
|
-
const ids = [...autoResolved.map((thread) => thread.id), ...autoMinimized.map((item) => item.id)];
|
|
95
|
-
const reasons = successReasons(ids, input.partition.ruleReasons);
|
|
96
|
-
const item = formatRuleAutoResolveJournalItem({
|
|
97
|
-
body,
|
|
98
|
-
reasons,
|
|
99
|
-
...(input.batch.viewerLogin ? { viewerLogin: input.batch.viewerLogin } : {}),
|
|
100
|
-
urls: [
|
|
101
|
-
...autoResolved.map((thread) => thread.url),
|
|
102
|
-
...autoMinimized.map((item) => item.url ?? ""),
|
|
103
|
-
],
|
|
104
|
-
});
|
|
105
|
-
try {
|
|
106
|
-
await runJournal({
|
|
107
|
-
prNumber: input.prNumber,
|
|
108
|
-
targetRepository: { owner: input.repo.owner, name: input.repo.name },
|
|
109
|
-
rawItem: item,
|
|
110
|
-
dryRun: false,
|
|
111
|
-
});
|
|
112
|
-
return undefined;
|
|
113
|
-
}
|
|
114
|
-
catch (err) {
|
|
115
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
116
|
-
const clause = reasonClause(reasons);
|
|
117
|
-
return clause ? `journal: ${message} ${clause}` : `journal: ${message}`;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
@@ -4,12 +4,3 @@ export declare const SHEPHERD_JOURNAL_DETAILS_OPEN = "<details>";
|
|
|
4
4
|
export declare const SHEPHERD_JOURNAL_DETAILS_SUMMARY = "<summary>Shepherd Journal</summary>";
|
|
5
5
|
export declare const SHEPHERD_JOURNAL_DETAILS_CLOSE = "</details>";
|
|
6
6
|
export declare const SHEPHERD_JOURNAL_APPEND_HINT = "If Shepherd Journal details already exist, append entries inside them instead of creating another container.";
|
|
7
|
-
export declare const SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE = "Read each body under `## Review summaries (first look)` and journal any warranted note before review mutations.";
|
|
8
|
-
/** The concrete `apply journal` command. The placeholder is single-quoted so a substituted decision stays literal in the shell. */
|
|
9
|
-
export declare function buildShepherdJournalCommand(prReference: string | number): string;
|
|
10
|
-
/**
|
|
11
|
-
* Build the Shepherd Journal instruction step. The reference-citation convention (link
|
|
12
|
-
* threads/comments by heading URL, cite reviews by ID) is a short clause, so it stays inline:
|
|
13
|
-
* a separate playbook would cost the agent an extra read turn that outweighs the clause.
|
|
14
|
-
*/
|
|
15
|
-
export declare function buildShepherdJournalInstruction(prReference: string | number): string;
|
|
@@ -1,21 +1,6 @@
|
|
|
1
|
-
import { buildPrShepherdCommand } from "../cli/runner.mjs";
|
|
2
1
|
export const SHEPHERD_JOURNAL_SECTION = "Shepherd Journal";
|
|
3
2
|
export const SHEPHERD_JOURNAL_SECTION_PATTERN = /^##\s+Shepherd\s+Journal$/;
|
|
4
3
|
export const SHEPHERD_JOURNAL_DETAILS_OPEN = "<details>";
|
|
5
4
|
export const SHEPHERD_JOURNAL_DETAILS_SUMMARY = "<summary>Shepherd Journal</summary>";
|
|
6
5
|
export const SHEPHERD_JOURNAL_DETAILS_CLOSE = "</details>";
|
|
7
6
|
export const SHEPHERD_JOURNAL_APPEND_HINT = "If Shepherd Journal details already exist, append entries inside them instead of creating another container.";
|
|
8
|
-
export const SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE = `Read each body under \`## Review summaries (first look)\` and journal any warranted note before review mutations.`;
|
|
9
|
-
/** The concrete `apply journal` command. The placeholder is single-quoted so a substituted decision stays literal in the shell. */
|
|
10
|
-
export function buildShepherdJournalCommand(prReference) {
|
|
11
|
-
return `${buildPrShepherdCommand(["apply", "journal", String(prReference)]).text} '- <decision>'`;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Build the Shepherd Journal instruction step. The reference-citation convention (link
|
|
15
|
-
* threads/comments by heading URL, cite reviews by ID) is a short clause, so it stays inline:
|
|
16
|
-
* a separate playbook would cost the agent an extra read turn that outweighs the clause.
|
|
17
|
-
*/
|
|
18
|
-
export function buildShepherdJournalInstruction(prReference) {
|
|
19
|
-
const command = buildShepherdJournalCommand(prReference);
|
|
20
|
-
return `Journal key decisions or rejections with URLs or IDs: \`${command}\``;
|
|
21
|
-
}
|
|
@@ -2,7 +2,7 @@ import { findHumanHandoffs, appendHumanHandoffInstructions } from "./stack-hando
|
|
|
2
2
|
export { findHumanHandoffs, appendHumanHandoffInstructions } from "./stack-handoffs.mjs";
|
|
3
3
|
import { stackMergeFlag } from "./stack-merge-flag.mjs";
|
|
4
4
|
import { stackLayerBlockReason } from "./stack-layer-readiness.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { splitStackWork } from "./stack-work.mjs";
|
|
6
6
|
import { getGithubTransport } from "../github/transport.mjs";
|
|
7
7
|
import { buildMergeCommandPlan, renderMergeCommand } from "./iterate/merge.mjs";
|
|
8
8
|
function readyPrefixTop(result) {
|
|
@@ -89,7 +89,6 @@ export function planPrefixDrain(result, mergeRequested) {
|
|
|
89
89
|
instructions[0] = `1. PR #${top.pr} is the highest open layer of stack #${top.stack.number} in \`${result.repo}\` whose open lower layers are all ready. Run \`GH_REPO=${result.repo} gh stack merge ${top.pr} --yes ${method.flag}\` to merge ${span}. If \`gh stack\` is an unknown command, run \`gh extension install github/gh-stack\`, then rerun that merge command. Do not rebase, push, or run \`gh stack push\`.`;
|
|
90
90
|
}
|
|
91
91
|
appendAutonomousInstructions(instructions, above.sessions);
|
|
92
|
-
appendMarkReadyInstructions(instructions, above.markReady);
|
|
93
92
|
const handoffs = findHumanHandoffs(result);
|
|
94
93
|
if (handoffs)
|
|
95
94
|
appendHumanHandoffInstructions(instructions, handoffs, false);
|
|
@@ -1,37 +1,24 @@
|
|
|
1
1
|
import type { PollSummaryItem } from "../types.mts";
|
|
2
|
-
type ProbedLayer = PollSummaryItem & {
|
|
3
|
-
pollCommand: string;
|
|
4
|
-
pollProbe: true;
|
|
5
|
-
};
|
|
6
|
-
/**
|
|
7
|
-
* The one-PR hold reason for a `READY` draft that only the disabled setting keeps in draft. The
|
|
8
|
-
* probe re-reads the draft, so this phrase, not a bare `[WAIT]`, proves it is still clean.
|
|
9
|
-
*/
|
|
10
|
-
export declare const AUTO_MARK_READY_DISABLED_HOLD = "automatic mark-ready is disabled for this session";
|
|
11
2
|
/** How the stack selector routes the autonomous layers that still need work. */
|
|
12
3
|
export interface StackWork {
|
|
13
4
|
/** Layers that need a one-PR Shepherd session, with or without a known command. */
|
|
14
5
|
sessions: PollSummaryItem[];
|
|
15
|
-
/** Clean
|
|
16
|
-
|
|
6
|
+
/** Clean drafts held by the disabled ready transition. No more work is scheduled for them. */
|
|
7
|
+
held: PollSummaryItem[];
|
|
17
8
|
/** Layers whose bounded probe could only report that they are still waiting. */
|
|
18
|
-
idle:
|
|
9
|
+
idle: PollSummaryItem[];
|
|
19
10
|
}
|
|
11
|
+
export declare function isDraftReadinessVerification(item: PollSummaryItem): boolean;
|
|
12
|
+
/** A full one-PR check is the final task when the compact review sample was incomplete. */
|
|
13
|
+
export declare function onlyDraftReadinessVerification(sessions: PollSummaryItem[], idle: PollSummaryItem[]): boolean;
|
|
20
14
|
/**
|
|
21
|
-
* Split autonomous stack candidates by what the agent can do for each. A
|
|
22
|
-
* mark
|
|
23
|
-
* kept in draft only by the disabled mark-ready setting is the agent's ready-for-review step.
|
|
15
|
+
* Split autonomous stack candidates by what the agent can do for each. A clean draft held by
|
|
16
|
+
* disabled mark-ready is complete for this run. Other bounded waits stay idle.
|
|
24
17
|
* A stale-ancestry child keeps its session, which returns the ancestry repair.
|
|
25
18
|
*/
|
|
26
19
|
export declare function splitStackWork(candidates: PollSummaryItem[], staleChildren: ReadonlySet<number>): StackWork;
|
|
27
|
-
/**
|
|
28
|
-
* The poll loop leaves a disabled draft's ready transition to the agent. The bounded probe runs
|
|
29
|
-
* first because only the one-PR session reads the full review context.
|
|
30
|
-
*/
|
|
31
|
-
export declare function appendMarkReadyInstructions(instructions: string[], layers: ProbedLayer[]): void;
|
|
32
20
|
/**
|
|
33
21
|
* The closing step after routed sessions: rerun the selector, or stop when nothing is owned.
|
|
34
22
|
* Returns the plan action: a stop is a handoff, so no rerun, quota cadence, or wake-up follows it.
|
|
35
23
|
*/
|
|
36
24
|
export declare function appendStackRerunInstruction(instructions: string[], layers: PollSummaryItem[], rerun: string): "shepherd" | "escalate";
|
|
37
|
-
export {};
|
|
@@ -1,24 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
export function isDraftReadinessVerification(item) {
|
|
2
|
+
return item.reasons?.includes("draft-readiness-verification-required") === true;
|
|
3
|
+
}
|
|
4
|
+
/** A full one-PR check is the final task when the compact review sample was incomplete. */
|
|
5
|
+
export function onlyDraftReadinessVerification(sessions, idle) {
|
|
6
|
+
return sessions.length > 0 && idle.length === 0 && sessions.every(isDraftReadinessVerification);
|
|
7
|
+
}
|
|
8
8
|
/**
|
|
9
|
-
* Split autonomous stack candidates by what the agent can do for each. A
|
|
10
|
-
* mark
|
|
11
|
-
* kept in draft only by the disabled mark-ready setting is the agent's ready-for-review step.
|
|
9
|
+
* Split autonomous stack candidates by what the agent can do for each. A clean draft held by
|
|
10
|
+
* disabled mark-ready is complete for this run. Other bounded waits stay idle.
|
|
12
11
|
* A stale-ancestry child keeps its session, which returns the ancestry repair.
|
|
13
12
|
*/
|
|
14
13
|
export function splitStackWork(candidates, staleChildren) {
|
|
15
|
-
const work = { sessions: [],
|
|
14
|
+
const work = { sessions: [], held: [], idle: [] };
|
|
16
15
|
for (const item of candidates) {
|
|
17
|
-
if (
|
|
18
|
-
work.
|
|
16
|
+
if (item.reasons.includes("draft-auto-mark-ready-disabled") && !staleChildren.has(item.pr)) {
|
|
17
|
+
work.held.push(item);
|
|
19
18
|
}
|
|
20
|
-
else if (item.
|
|
21
|
-
work.
|
|
19
|
+
else if (!isProbed(item) || item.action !== "wait" || staleChildren.has(item.pr)) {
|
|
20
|
+
work.sessions.push(item);
|
|
22
21
|
}
|
|
23
22
|
else {
|
|
24
23
|
work.idle.push(item);
|
|
@@ -26,29 +25,6 @@ export function splitStackWork(candidates, staleChildren) {
|
|
|
26
25
|
}
|
|
27
26
|
return work;
|
|
28
27
|
}
|
|
29
|
-
/**
|
|
30
|
-
* The poll loop leaves a disabled draft's ready transition to the agent. The bounded probe runs
|
|
31
|
-
* first because only the one-PR session reads the full review context.
|
|
32
|
-
*/
|
|
33
|
-
export function appendMarkReadyInstructions(instructions, layers) {
|
|
34
|
-
for (const item of layers) {
|
|
35
|
-
if (!item.owned) {
|
|
36
|
-
instructions.push(`${instructions.length + 1}. PR #${item.pr} is not owned. Leave its draft state unchanged.`);
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
if (item.transport === "rest") {
|
|
40
|
-
const command = buildPrShepherdCommand([
|
|
41
|
-
"iterate",
|
|
42
|
-
formatPrUrl(item.repo, item.pr),
|
|
43
|
-
"--transport",
|
|
44
|
-
"rest",
|
|
45
|
-
]).text;
|
|
46
|
-
instructions.push(`${instructions.length + 1}. Run \`${item.pollCommand}\` first for PR #${item.pr}. If it returns \`[WAIT]\` saying the draft is held only because automatic mark-ready is disabled, run \`${command}\` to authorize the CCR ready transition; otherwise follow the probe's instructions.`);
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
instructions.push(`${instructions.length + 1}. Automatic mark-ready is disabled, so marking PR #${item.pr} ready for review is your step. Run \`${item.pollCommand}\` first. If it returns \`[WAIT]\` saying PR #${item.pr} stays in draft because ${AUTO_MARK_READY_DISABLED_HOLD}, run \`gh pr ready ${item.pr} -R ${item.repo}\`; otherwise complete its instructions and leave PR #${item.pr} in draft this round.`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
28
|
function isProbed(item) {
|
|
53
29
|
return item.pollProbe === true && item.pollCommand !== undefined;
|
|
54
30
|
}
|
package/bin/config/load.d.mts
CHANGED
|
@@ -81,8 +81,8 @@ export interface PrShepherdConfig {
|
|
|
81
81
|
*/
|
|
82
82
|
resolveOtherHumanThreads: ResolveOtherHumanThreads;
|
|
83
83
|
/**
|
|
84
|
-
* `inline` (default) prints the commit/push
|
|
85
|
-
* every tick; `playbook` folds
|
|
84
|
+
* `inline` (default) prints the commit/push step of `fix_code` instructions
|
|
85
|
+
* every tick; `playbook` folds it into one `Playbook: "Fix-code loop"` pointer. The
|
|
86
86
|
* default is inline because the token bench measured playbook as a net cost: it adds a read. The `--instructions` flag and MCP `instructions` input override per call.
|
|
87
87
|
*/
|
|
88
88
|
instructions: InstructionStyle;
|
package/bin/exit-codes.d.mts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import type { IterateResult } from "./types.mts";
|
|
13
13
|
import type { ResolveResult } from "./comments/resolve.mts";
|
|
14
14
|
export declare const EXIT: Readonly<{
|
|
15
|
-
/**
|
|
15
|
+
/** Clean terminal stop, including a draft held by disabled automatic mark-ready. */
|
|
16
16
|
readonly OK: 0;
|
|
17
17
|
/** Nothing to do yet, or a clean PR whose ready-delay is still counting. */
|
|
18
18
|
readonly WAIT: 10;
|
package/bin/exit-codes.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* ran to completion and is reporting PR state. See docs/exit-codes.md.
|
|
11
11
|
*/
|
|
12
12
|
export const EXIT = Object.freeze({
|
|
13
|
-
/**
|
|
13
|
+
/** Clean terminal stop, including a draft held by disabled automatic mark-ready. */
|
|
14
14
|
OK: 0,
|
|
15
15
|
/** Nothing to do yet, or a clean PR whose ready-delay is still counting. */
|
|
16
16
|
WAIT: 10,
|
|
@@ -55,6 +55,7 @@ export class ShepherdError extends Error {
|
|
|
55
55
|
const CANCEL_REASON_EXIT_CODE = {
|
|
56
56
|
merged: EXIT.OK,
|
|
57
57
|
"ready-delay-elapsed": EXIT.OK,
|
|
58
|
+
"auto-mark-ready-disabled": EXIT.OK,
|
|
58
59
|
closed: EXIT.CLOSED,
|
|
59
60
|
};
|
|
60
61
|
export function iterateResultToExitCode(result) {
|
|
@@ -28,7 +28,9 @@ export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdministe
|
|
|
28
28
|
let remainingSeconds;
|
|
29
29
|
if (raw.isDraft &&
|
|
30
30
|
blockingReviewerInProgress &&
|
|
31
|
-
(action === "mark_ready" ||
|
|
31
|
+
(action === "mark_ready" ||
|
|
32
|
+
reasons.includes("draft-auto-mark-ready-disabled") ||
|
|
33
|
+
reasons.includes("draft-readiness-verification-required"))) {
|
|
32
34
|
action = "wait";
|
|
33
35
|
reasons = ["blocking-reviewer-in-progress"];
|
|
34
36
|
}
|
|
@@ -40,18 +40,17 @@ export function routePollSummary(raw, checks, review, opts) {
|
|
|
40
40
|
}
|
|
41
41
|
if (raw.isDraft) {
|
|
42
42
|
const autoMarkReadyDisabled = opts.noAutoMarkReady || actions.autoMarkReady === false;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
43
|
+
if (autoMarkReadyDisabled && (checks.incomplete || review.incomplete))
|
|
44
|
+
return { action: "fix_code", reasons: ["draft-readiness-verification-required"] };
|
|
45
|
+
if (autoMarkReadyDisabled)
|
|
46
|
+
return { action: "cancel", reasons: ["draft-auto-mark-ready-disabled"] };
|
|
47
|
+
if (!canGenerateGithubMutation(raw.viewerCanUpdate, "ready")) {
|
|
47
48
|
const unsupported = getGithubTransport() === "rest" && raw.viewerCanUpdate !== false && !isCcrTransport();
|
|
48
49
|
if (unsupported)
|
|
49
50
|
return { action: "escalate", reasons: ["transport-unsupported"] };
|
|
50
51
|
return { action: "escalate", reasons: ["mark-ready-authorization-required"] };
|
|
51
52
|
}
|
|
52
|
-
return
|
|
53
|
-
? { action: "wait", reasons: ["draft-auto-mark-ready-disabled"] }
|
|
54
|
-
: { action: "mark_ready", reasons: ["draft-appears-ready"] };
|
|
53
|
+
return { action: "mark_ready", reasons: ["draft-appears-ready"] };
|
|
55
54
|
}
|
|
56
55
|
if (opts.merge && (raw.isInMergeQueue || raw.autoMergeRequest)) {
|
|
57
56
|
return {
|
package/bin/mcp/server.mjs
CHANGED
|
@@ -33,7 +33,7 @@ const iterateInputSchema = z
|
|
|
33
33
|
instructions: z
|
|
34
34
|
.enum(["playbook", "inline"])
|
|
35
35
|
.optional()
|
|
36
|
-
.describe("fix_code instruction style. inline (default) prints every step each tick; playbook folds commit
|
|
36
|
+
.describe("fix_code instruction style. inline (default) prints every step each tick; playbook folds commit and push into the Fix-code loop playbook."),
|
|
37
37
|
pollMode: z
|
|
38
38
|
.enum(["auto", "poll", "event"])
|
|
39
39
|
.optional()
|
package/bin/types/iterate.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentThread, AgentComment, AgentCheck, GlobalOptions, RelevantCheck, ShepherdStatus, FirstLookThread, FirstLookComment, RuleAutoResolveReport } from "./report.mts";
|
|
2
2
|
import type { ActiveCheck, PrActivitySummary } from "./activity.mts";
|
|
3
3
|
import type { BranchProtection, MergeStateStatus, MergedBasePullRequest, Review, ReviewDecision, ReviewThread, ShepherdMergeStatus } from "./github.mts";
|
|
4
|
-
import type { MergeRequirements
|
|
4
|
+
import type { MergeRequirements } from "./merge-requirements.mts";
|
|
5
5
|
import type { EscalateDetails } from "./escalate.mts";
|
|
6
6
|
import type { MergeCommandPlan } from "./merge-action.mts";
|
|
7
7
|
import type { ProtectedRun } from "./protected-run.mts";
|
|
@@ -75,14 +75,13 @@ interface IterateResultWait extends IterateResultBase {
|
|
|
75
75
|
action: "wait";
|
|
76
76
|
log: string;
|
|
77
77
|
deferredWork?: import("./merge-queue.mts").IterateDeferredWork;
|
|
78
|
-
stackDraftHold?: StackDraftHold;
|
|
79
78
|
}
|
|
80
79
|
/** Clean PR whose ready-delay is still counting. `--until-terminal` returns this instead of sleeping. */
|
|
81
80
|
interface IterateResultReady extends IterateResultBase {
|
|
82
81
|
action: "ready";
|
|
83
82
|
log: string;
|
|
84
83
|
}
|
|
85
|
-
export type CancelReason = "merged" | "closed" | "ready-delay-elapsed";
|
|
84
|
+
export type CancelReason = "merged" | "closed" | "ready-delay-elapsed" | "auto-mark-ready-disabled";
|
|
86
85
|
interface IterateResultCancel extends IterateResultBase {
|
|
87
86
|
action: "cancel";
|
|
88
87
|
reason: CancelReason;
|
|
@@ -41,13 +41,6 @@ export interface StackStatus {
|
|
|
41
41
|
}
|
|
42
42
|
/** Why an open native stack layer is not ready to merge. */
|
|
43
43
|
export type StackLayerBlockReason = "closed" | "draft" | "conflicting" | "queue-removal" | "failing-checks" | "review-work" | "checks-in-progress" | "merge-state" | "no-ready-receipt" | "stale-ancestry";
|
|
44
|
-
/**
|
|
45
|
-
* A native stack draft stays in draft on a WAIT tick because this session will not
|
|
46
|
-
* mark it ready. The caller returns to the stack selector.
|
|
47
|
-
*/
|
|
48
|
-
export type StackDraftHold = {
|
|
49
|
-
kind: "auto-mark-ready-disabled";
|
|
50
|
-
};
|
|
51
44
|
/** Extra batch-PR fields for merge-queue, stacks, and folded branch rules. */
|
|
52
45
|
export interface BatchPrMergeFields {
|
|
53
46
|
branchRules?: BranchRules;
|
package/package.json
CHANGED