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.
Files changed (54) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +3 -3
  3. package/bin/classify/types.d.mts +1 -1
  4. package/bin/cli/help-command-pages.d.mts +2 -2
  5. package/bin/cli/help-iterate-poll-pages.d.mts +2 -2
  6. package/bin/cli/help-iterate-poll-pages.mjs +5 -5
  7. package/bin/cli/help-top-page.d.mts +1 -1
  8. package/bin/cli/help-top-page.mjs +2 -2
  9. package/bin/cli/help.d.mts +3 -3
  10. package/bin/cli/iterate-instructions.mjs +1 -31
  11. package/bin/cli/iterate-lean.mjs +0 -1
  12. package/bin/commands/check.mjs +0 -2
  13. package/bin/commands/event-mode.mjs +3 -7
  14. package/bin/commands/iterate/fix-loop-instruction.d.mts +2 -8
  15. package/bin/commands/iterate/fix-loop-instruction.mjs +8 -29
  16. package/bin/commands/iterate/index.mjs +12 -3
  17. package/bin/commands/iterate/render.d.mts +1 -1
  18. package/bin/commands/iterate/render.mjs +17 -13
  19. package/bin/commands/next-check.d.mts +0 -2
  20. package/bin/commands/next-check.mjs +1 -1
  21. package/bin/commands/poll-summary-explicit-instructions.d.mts +2 -0
  22. package/bin/commands/poll-summary-explicit-instructions.mjs +18 -2
  23. package/bin/commands/poll-summary-instructions.mjs +33 -11
  24. package/bin/commands/rule-auto-resolve-format.d.mts +0 -12
  25. package/bin/commands/rule-auto-resolve-format.mjs +2 -11
  26. package/bin/commands/rule-auto-resolve.d.mts +0 -5
  27. package/bin/commands/rule-auto-resolve.mjs +1 -42
  28. package/bin/commands/shepherd-journal.d.mts +0 -9
  29. package/bin/commands/shepherd-journal.mjs +0 -15
  30. package/bin/commands/stack-drain.mjs +1 -2
  31. package/bin/commands/stack-work.d.mts +8 -21
  32. package/bin/commands/stack-work.mjs +14 -38
  33. package/bin/config/load.d.mts +2 -2
  34. package/bin/exit-codes.d.mts +1 -1
  35. package/bin/exit-codes.mjs +2 -1
  36. package/bin/github/poll-summary-projector.mjs +3 -1
  37. package/bin/github/poll-summary-route.mjs +6 -7
  38. package/bin/mcp/server.mjs +1 -1
  39. package/bin/types/iterate.d.mts +2 -3
  40. package/bin/types/merge-requirements.d.mts +0 -7
  41. package/package.json +1 -1
  42. package/plugins/pr-shepherd/.claude-plugin/plugin.json +1 -1
  43. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  44. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  45. package/plugins/pr-shepherd/.mcp.json +1 -1
  46. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +8 -6
  47. package/plugins/pr-shepherd/skills/pr-shepherd/references/cloud-event-loop.md +1 -1
  48. package/plugins/pr-shepherd/skills/pr-shepherd/references/draft-handoff.md +38 -0
  49. package/plugins/pr-shepherd/skills/pr-shepherd/references/fix-code-loop.md +1 -2
  50. package/plugins/pr-shepherd/skills/pr-shepherd/references/merge-queue-ejection.md +1 -1
  51. package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +1 -1
  52. package/src/classify/types.mts +1 -1
  53. package/bin/commands/iterate/parent-first.d.mts +0 -8
  54. 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 ["1. Stop — every selected PR is terminal."];
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 (isHandedOffSelection(result)) {
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 { appendMarkReadyInstructions, appendStackRerunInstruction, splitStackWork, } from "./stack-work.mjs";
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 || work.markReady.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 = [...runnableCandidates, ...work.markReady].some((item) => item.owned);
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
- appendStackRerunInstruction(instructions, [...runnableCandidates, ...work.markReady], "After the listed one-PR sessions, rerun this same `--stack` selector. Stop for the human handoff only when no autonomous shepherding remains.");
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, [...runnableCandidates, ...work.markReady], "After the listed one-PR sessions, rerun this same `--stack` selector. Stop for the human handoff only when no autonomous shepherding remains.");
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
- appendMarkReadyInstructions(instructions, work.markReady);
133
- const action = appendStackRerunInstruction(instructions, [...work.sessions, ...work.markReady], "After the selected one-PR sessions, rerun this same `--stack` selector.");
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
- export function reasonClause(reasons) {
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
- export function ruleAutoResolveBody(counts) {
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;
@@ -13,9 +13,4 @@ export declare function applySuppressedRuleAutoResolve(input: {
13
13
  enabled: boolean;
14
14
  partition: BatchPartition;
15
15
  batch: BatchPrData;
16
- prNumber: number;
17
- repo: {
18
- owner: string;
19
- name: string;
20
- };
21
16
  }): Promise<RuleAutoResolveApplied>;
@@ -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, formatRuleAutoResolveJournalItem, minimizedItem, reasonClause, reasonsForError, resolvedThread, ruleAutoResolveBody, uniqueReasons, } from "./rule-auto-resolve-format.mjs";
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 { appendMarkReadyInstructions, splitStackWork } from "./stack-work.mjs";
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, unblocked drafts that the agent marks ready because the poll loop will not. */
16
- markReady: ProbedLayer[];
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: ProbedLayer[];
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 bounded probe cannot
22
- * mark a draft ready, so rerunning it for a waiting layer cannot change the stack. A probed wait
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
- import { buildPrShepherdCommand } from "../cli/runner.mjs";
2
- import { formatPrUrl } from "../pr-reference.mjs";
3
- /**
4
- * The one-PR hold reason for a `READY` draft that only the disabled setting keeps in draft. The
5
- * probe re-reads the draft, so this phrase, not a bare `[WAIT]`, proves it is still clean.
6
- */
7
- export const AUTO_MARK_READY_DISABLED_HOLD = "automatic mark-ready is disabled for this session";
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 bounded probe cannot
10
- * mark a draft ready, so rerunning it for a waiting layer cannot change the stack. A probed wait
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: [], markReady: [], idle: [] };
14
+ const work = { sessions: [], held: [], idle: [] };
16
15
  for (const item of candidates) {
17
- if (!isProbed(item) || item.action !== "wait" || staleChildren.has(item.pr)) {
18
- work.sessions.push(item);
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.reasons.includes("draft-auto-mark-ready-disabled")) {
21
- work.markReady.push(item);
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
  }
@@ -81,8 +81,8 @@ export interface PrShepherdConfig {
81
81
  */
82
82
  resolveOtherHumanThreads: ResolveOtherHumanThreads;
83
83
  /**
84
- * `inline` (default) prints the commit/push and journal steps of `fix_code` instructions
85
- * every tick; `playbook` folds them into one `Playbook: "Fix-code loop"` pointer. The
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;
@@ -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
- /** `cancel` + `merged` or `ready-delay-elapsed` — shepherd finished cleanly. */
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;
@@ -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
- /** `cancel` + `merged` or `ready-delay-elapsed` — shepherd finished cleanly. */
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" || reasons.includes("draft-auto-mark-ready-disabled"))) {
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
- // The stack selector asks the agent to mark a disabled draft ready, so
44
- // that transition needs the same capability as the automatic one.
45
- if (!canGenerateGithubMutation(raw.viewerCanUpdate, "ready") &&
46
- (!autoMarkReadyDisabled || opts.stackPrNumber !== undefined)) {
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 autoMarkReadyDisabled
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 {
@@ -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, push and journal into the Fix-code loop playbook."),
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()
@@ -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, StackDraftHold } from "./merge-requirements.mts";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.59.1",
3
+ "version": "0.59.3",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
5
5
  "keywords": [
6
6
  "automation",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
3
  "description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
4
- "version": "0.59.1",
4
+ "version": "0.59.3",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
- "version": "0.59.1",
3
+ "version": "0.59.3",
4
4
  "description": "Autonomous PR CI monitor and review-comment resolver for Codex.",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "pr-shepherd": {
4
4
  "command": "npx",
5
- "args": ["--yes", "--package", "pr-shepherd@0.59.1", "pr-shepherd-mcp"]
5
+ "args": ["--yes", "--package", "pr-shepherd@0.59.3", "pr-shepherd-mcp"]
6
6
  }
7
7
  }
8
8
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "pr-shepherd": {
3
3
  "command": "npx",
4
- "args": ["--yes", "--package", "pr-shepherd@0.59.1", "pr-shepherd-mcp"]
4
+ "args": ["--yes", "--package", "pr-shepherd@0.59.3", "pr-shepherd-mcp"]
5
5
  }
6
6
  }