pr-shepherd 0.25.4 → 0.26.1

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 (47) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +23 -0
  3. package/bin/checks/classify.mjs +10 -2
  4. package/bin/classify/apply.mjs +110 -0
  5. package/bin/classify/loader.mjs +85 -0
  6. package/bin/classify/types.mjs +1 -0
  7. package/bin/cli/args.mjs +1 -0
  8. package/bin/cli/default-poll.mjs +1 -0
  9. package/bin/cli/fix-formatter.mjs +3 -4
  10. package/bin/cli/help-command-pages.mjs +20 -1
  11. package/bin/cli/help-top-page.mjs +3 -0
  12. package/bin/cli/iterate-formatter.mjs +42 -7
  13. package/bin/cli/iterate-instructions.mjs +3 -22
  14. package/bin/cli/iterate-lean.mjs +32 -9
  15. package/bin/cli/journal-handler.mjs +79 -0
  16. package/bin/cli/poll-handler.mjs +1 -0
  17. package/bin/cli-parser.mjs +5 -23
  18. package/bin/commands/check-terminal-report.mjs +1 -0
  19. package/bin/commands/check.mjs +36 -8
  20. package/bin/commands/iterate/classify.mjs +14 -5
  21. package/bin/commands/iterate/fix-code.mjs +2 -2
  22. package/bin/commands/iterate/helpers.mjs +15 -0
  23. package/bin/commands/iterate/index.mjs +8 -2
  24. package/bin/commands/iterate/render.mjs +15 -9
  25. package/bin/commands/iterate/stall.mjs +4 -0
  26. package/bin/commands/journal/index.mjs +26 -0
  27. package/bin/commands/journal/transform.mjs +112 -0
  28. package/bin/commands/poll.mjs +45 -3
  29. package/bin/commands/ready-mergeability.mjs +2 -2
  30. package/bin/commands/shepherd-journal.mjs +2 -2
  31. package/bin/config/load.mjs +7 -0
  32. package/bin/config.json +1 -0
  33. package/bin/github/activity.mjs +57 -0
  34. package/bin/github/batch-parsers.mjs +20 -34
  35. package/bin/github/branch-protection.mjs +12 -0
  36. package/bin/github/client.mjs +17 -1
  37. package/bin/github/gql/batch-pr.gql +8 -0
  38. package/bin/github/gql/get-pr-body.gql +8 -0
  39. package/bin/github/gql/update-pr-body.gql +7 -0
  40. package/bin/github/queries.mjs +4 -0
  41. package/bin/types/activity.mjs +1 -0
  42. package/bin/types/iterate.mjs +0 -1
  43. package/bin/types.mjs +1 -0
  44. package/package.json +12 -2
  45. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  46. package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +7 -22
  47. package/src/classify/types.mts +48 -0
@@ -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.25.4",
4
+ "version": "0.26.1",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
package/README.md CHANGED
@@ -89,6 +89,7 @@ Direct CLI:
89
89
  ```sh
90
90
  pr-shepherd 42 # poll until non-WAIT or timeout
91
91
  pr-shepherd 42 --interval 45s --timeout 4m
92
+ pr-shepherd 42 --quiet-status # print only changed WAIT status snapshots
92
93
  pr-shepherd 42 --ready-delay 15m
93
94
  pr-shepherd iterate 42 # single tick
94
95
  pr-shepherd poll 42 # explicit poll command
@@ -167,6 +168,8 @@ After adding the marketplace, install/enable the `pr-shepherd` plugin from Codex
167
168
  Create `.pr-shepherdrc.yml` in your project root or an ancestor directory.
168
169
 
169
170
  ```yaml
171
+ ignoreChecks:
172
+ - "Kilo Code Review"
170
173
  iterate:
171
174
  fixAttemptsPerThread: 5
172
175
  stallTimeoutMinutes: 60
@@ -189,6 +192,26 @@ Environment variables:
189
192
 
190
193
  See [docs/configuration.md](docs/configuration.md) for the full reference.
191
194
 
195
+ ### Classification rules
196
+
197
+ Drop `.ts` / `.mts` / `.mjs` / `.js` files under `.pr-shepherd/classification/` to suppress and/or auto-resolve specific bot comments — useful for silencing repetitive noise like rate-limit notices from `gemini-code-assist` or "Reviews paused" from `coderabbitai`.
198
+
199
+ ```ts
200
+ // .pr-shepherd/classification/gemini-quota.mts
201
+ import type { ClassifyRule } from "pr-shepherd/classify";
202
+
203
+ const rule: ClassifyRule = (item) => {
204
+ if (item.author !== "gemini-code-assist") return null;
205
+ if (!/You have reached your daily quota limit/i.test(item.body)) return null;
206
+ return { suppress: true, autoResolve: true };
207
+ };
208
+ export default rule;
209
+ ```
210
+
211
+ `suppress: true` hides the item from agent output. `autoResolve: true` queues it for the minimize/resolve mutation. Both can apply together.
212
+
213
+ Ready-to-use examples for common patterns are in [`examples/classification/`](examples/classification/).
214
+
192
215
  ## Requirements
193
216
 
194
217
  - Node.js >= 22.0.0
@@ -9,6 +9,7 @@
9
9
  * pass/fail tally. Report them as "skipped" for transparency but don't block on them.
10
10
  */
11
11
  import { loadConfig } from "../config/load.mjs";
12
+ import picomatch from "picomatch";
12
13
  /**
13
14
  * Classify a list of raw check runs into shepherd categories.
14
15
  *
@@ -16,8 +17,15 @@ import { loadConfig } from "../config/load.mjs";
16
17
  * @returns Classified checks. "filtered" items were excluded from the tally.
17
18
  */
18
19
  export function classifyChecks(checks) {
19
- const relevantEvents = new Set(loadConfig().checks.ciTriggerEvents);
20
- return checks.map((c) => classify(c, relevantEvents));
20
+ const config = loadConfig();
21
+ const relevantEvents = new Set(config.checks.ciTriggerEvents);
22
+ const isIgnored = buildIgnoreMatcher(config.ignoreChecks ?? []);
23
+ return checks.filter((c) => !isIgnored(c.name)).map((c) => classify(c, relevantEvents));
24
+ }
25
+ function buildIgnoreMatcher(patterns) {
26
+ if (patterns.length === 0)
27
+ return () => false;
28
+ return picomatch(patterns, { nocase: true });
21
29
  }
22
30
  function classify(check, relevantEvents) {
23
31
  // Filter: runs from non-PR events don't count toward PR readiness.
@@ -0,0 +1,110 @@
1
+ function applyRules(rules, item) {
2
+ let autoResolve = false;
3
+ let suppress = false;
4
+ for (const { rule, name } of rules) {
5
+ let action;
6
+ try {
7
+ action = rule(item);
8
+ }
9
+ catch (err) {
10
+ const msg = err instanceof Error ? err.message : String(err);
11
+ process.stderr.write(`pr-shepherd: classification rule ${name}: threw during evaluation: ${msg} — skipped\n`);
12
+ continue;
13
+ }
14
+ if (!action)
15
+ continue;
16
+ if (action.autoResolve)
17
+ autoResolve = true;
18
+ if (action.suppress)
19
+ suppress = true;
20
+ }
21
+ return { autoResolve, suppress };
22
+ }
23
+ function threadToItem(t) {
24
+ return {
25
+ kind: "review-thread",
26
+ id: t.id,
27
+ author: t.author,
28
+ authorType: t.authorType,
29
+ body: t.body,
30
+ url: t.url,
31
+ path: t.path,
32
+ };
33
+ }
34
+ function commentToItem(c) {
35
+ return {
36
+ kind: "pr-comment",
37
+ id: c.id,
38
+ author: c.author,
39
+ authorType: c.authorType,
40
+ body: c.body,
41
+ url: c.url,
42
+ };
43
+ }
44
+ function reviewSummaryToItem(r) {
45
+ return {
46
+ kind: "review-summary",
47
+ id: r.id,
48
+ author: r.author,
49
+ authorType: r.authorType,
50
+ body: r.body,
51
+ };
52
+ }
53
+ function changesRequestedToItem(r) {
54
+ return {
55
+ kind: "changes-requested",
56
+ id: r.id,
57
+ author: r.author,
58
+ authorType: r.authorType,
59
+ body: r.body,
60
+ };
61
+ }
62
+ function addToIndex(id, { suppress, autoResolve }, suppressedIds, autoResolveIds) {
63
+ if (suppress)
64
+ suppressedIds.add(id);
65
+ if (autoResolve)
66
+ autoResolveIds.add(id);
67
+ }
68
+ export function buildClassifyIndex(rules, batch) {
69
+ if (rules.length === 0)
70
+ return { suppressedIds: new Set(), autoResolveIds: new Set() };
71
+ const suppressedIds = new Set();
72
+ const autoResolveIds = new Set();
73
+ for (const t of batch.reviewThreads)
74
+ addToIndex(t.id, applyRules(rules, threadToItem(t)), suppressedIds, autoResolveIds);
75
+ for (const c of batch.comments)
76
+ addToIndex(c.id, applyRules(rules, commentToItem(c)), suppressedIds, autoResolveIds);
77
+ for (const r of batch.reviewSummaries)
78
+ addToIndex(r.id, applyRules(rules, reviewSummaryToItem(r)), suppressedIds, autoResolveIds);
79
+ // autoResolve for changes-requested requires a dismiss message; not supported
80
+ for (const r of batch.changesRequestedReviews) {
81
+ if (applyRules(rules, changesRequestedToItem(r)).suppress)
82
+ suppressedIds.add(r.id);
83
+ }
84
+ return { suppressedIds, autoResolveIds };
85
+ }
86
+ export function partitionBatch(index, batch) {
87
+ const { suppressedIds, autoResolveIds } = index;
88
+ const suppressedCommentIds = new Set(batch.comments.filter((c) => suppressedIds.has(c.id)).map((c) => c.id));
89
+ const suppressedThreadIds = new Set(batch.reviewThreads.filter((t) => suppressedIds.has(t.id)).map((t) => t.id));
90
+ const suppressedReviewSummaryIds = new Set(batch.reviewSummaries.filter((r) => suppressedIds.has(r.id)).map((r) => r.id));
91
+ const suppressedChangesRequestedIds = new Set(batch.changesRequestedReviews.filter((r) => suppressedIds.has(r.id)).map((r) => r.id));
92
+ const ruleAutoResolveCommentIds = batch.comments
93
+ .filter((c) => !c.isMinimized && autoResolveIds.has(c.id))
94
+ .map((c) => c.id);
95
+ const ruleAutoResolveThreadIds = batch.reviewThreads
96
+ .filter((t) => !t.isResolved && !t.isOutdated && autoResolveIds.has(t.id))
97
+ .map((t) => t.id);
98
+ const ruleAutoResolveReviewSummaryIds = batch.reviewSummaries
99
+ .filter((r) => autoResolveIds.has(r.id))
100
+ .map((r) => r.id);
101
+ return {
102
+ suppressedCommentIds,
103
+ suppressedThreadIds,
104
+ suppressedReviewSummaryIds,
105
+ suppressedChangesRequestedIds,
106
+ ruleAutoResolveCommentIds,
107
+ ruleAutoResolveThreadIds,
108
+ ruleAutoResolveReviewSummaryIds,
109
+ };
110
+ }
@@ -0,0 +1,85 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { readdirSync, statSync } from "node:fs";
10
+ import { join, dirname, extname, basename } from "node:path";
11
+ import { homedir } from "node:os";
12
+ import { pathToFileURL } from "node:url";
13
+ const CLASSIFICATION_DIR = ".pr-shepherd/classification";
14
+ const VALID_EXTENSIONS = new Set([".ts", ".mts", ".mjs", ".js"]);
15
+ export function discoverRuleFiles(cwd) {
16
+ const home = homedir();
17
+ let current = cwd;
18
+ while (true) {
19
+ const candidate = join(current, CLASSIFICATION_DIR);
20
+ if (statSync(candidate, { throwIfNoEntry: false })?.isDirectory()) {
21
+ return collectRuleFiles(candidate);
22
+ }
23
+ if (current === home || current === dirname(current))
24
+ return [];
25
+ current = dirname(current);
26
+ }
27
+ }
28
+ function collectRuleFiles(dir) {
29
+ return readdirSync(dir)
30
+ .filter((name) => !name.startsWith("_") && !name.startsWith("."))
31
+ .filter((name) => VALID_EXTENSIONS.has(extname(name)))
32
+ .map((name) => join(dir, name))
33
+ .sort();
34
+ }
35
+ let tsxAttempted = false;
36
+ async function ensureTsxRegistered() {
37
+ if (tsxAttempted)
38
+ return;
39
+ tsxAttempted = true;
40
+ try {
41
+ const { register } = await import("tsx/esm/api");
42
+ register();
43
+ /* c8 ignore start */
44
+ }
45
+ catch (err) {
46
+ const msg = err instanceof Error ? err.message : String(err);
47
+ process.stderr.write(`pr-shepherd: failed to register tsx — .ts/.mts classification rules will not load: ${msg}\n`);
48
+ }
49
+ /* c8 ignore stop */
50
+ }
51
+ const ruleCache = new Map();
52
+ export async function loadRules(files) {
53
+ if (files.length === 0)
54
+ return [];
55
+ const cacheKey = [...files].sort((a, b) => a.localeCompare(b)).join("\0");
56
+ const cached = ruleCache.get(cacheKey);
57
+ if (cached !== undefined)
58
+ return cached;
59
+ const hasTs = files.some((f) => f.endsWith(".ts") || f.endsWith(".mts"));
60
+ if (hasTs)
61
+ await ensureTsxRegistered();
62
+ const rules = [];
63
+ for (const file of files) {
64
+ try {
65
+ // eslint-disable-next-line no-await-in-loop
66
+ const mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(file).href)));
67
+ if (typeof mod.default !== "function") {
68
+ process.stderr.write(`pr-shepherd: classification rule ${file}: default export is not a function — skipped\n`);
69
+ continue;
70
+ }
71
+ const name = basename(file).replace(/\.[^.]+$/, "");
72
+ rules.push({ name, file, rule: mod.default });
73
+ }
74
+ catch (err) {
75
+ const msg = err instanceof Error ? err.message : String(err);
76
+ process.stderr.write(`pr-shepherd: classification rule ${file}: failed to load: ${msg} — skipped\n`);
77
+ }
78
+ }
79
+ ruleCache.set(cacheKey, rules);
80
+ return rules;
81
+ }
82
+ /** Reset the rule cache — for use in tests. */
83
+ export function _resetRuleCache() {
84
+ ruleCache.clear();
85
+ }
@@ -0,0 +1 @@
1
+ export {};
package/bin/cli/args.mjs CHANGED
@@ -27,6 +27,7 @@ const FLAGS_WITH_VALUES = new Set([
27
27
  const BOOLEAN_FLAGS = new Set([
28
28
  "--no-auto-mark-ready",
29
29
  "--no-auto-cancel-actionable",
30
+ "--quiet-status",
30
31
  "--dry-run",
31
32
  "--verbose",
32
33
  ]);
@@ -12,6 +12,7 @@ const DEFAULT_POLL_BOOLEAN_FLAGS = new Set([
12
12
  "--verbose",
13
13
  "--no-auto-mark-ready",
14
14
  "--no-auto-cancel-actionable",
15
+ "--quiet-status",
15
16
  ]);
16
17
  export function isDefaultPollInvocation(subcommand) {
17
18
  if (subcommand === "--help" || subcommand === "-h")
@@ -2,9 +2,8 @@ import { renderResolveCommand } from "../commands/iterate/render.mjs";
2
2
  import { joinSections } from "../util/markdown.mjs";
3
3
  import { renderSuggestionBlock, renderLineRange } from "./suggestion-renderer.mjs";
4
4
  import { renderThreadBullet, renderReviewBullet, renderThreadResolutionStatusTag, renderAuthor, buildFirstLookBullets, renderThreadConversation, blockquote, } from "./list-formatters.mjs";
5
- import { adaptFixCodeInstructions, numberInstructions } from "./iterate-instructions.mjs";
6
- export function formatFixCodeResult(header, result, opts) {
7
- const readyDelaySuffix = opts?.readyDelaySuffix;
5
+ import { numberInstructions } from "./iterate-instructions.mjs";
6
+ export function formatFixCodeResult(header, result) {
8
7
  const sections = [header];
9
8
  if (result.fix.threads.length > 0) {
10
9
  sections.push("## Review threads");
@@ -128,7 +127,7 @@ export function formatFixCodeResult(header, result, opts) {
128
127
  }
129
128
  sections.push(postFixLines.join("\n"));
130
129
  sections.push("## Instructions");
131
- sections.push(numberInstructions(adaptFixCodeInstructions(result.fix.instructions, result.pr, readyDelaySuffix)));
130
+ sections.push(numberInstructions(result.fix.instructions));
132
131
  return joinSections(sections);
133
132
  }
134
133
  function renderCheckAnnotation(a) {
@@ -111,6 +111,7 @@ Usage:
111
111
  Poll flags:
112
112
  --interval <duration> Sleep between WAIT ticks. Default: 30s.
113
113
  --timeout <duration> Maximum wall-clock wait. Default: 5m.
114
+ --quiet-status During WAIT polling, print only changed status snapshots.
114
115
 
115
116
  Forwarded iterate flags:
116
117
  --ready-delay <duration> Settle window before a clean PR cancels. Example: 15m.
@@ -122,7 +123,7 @@ Forwarded iterate flags:
122
123
  --help, -h Print this help and exit before GitHub, git, config, or log I/O.
123
124
 
124
125
  Durations accept seconds, minutes, or hours: 30s, 2m, 1h, or bare seconds.
125
- Each WAIT tick writes a single dot to stderr; --verbose emits the detailed per-tick line.
126
+ Each WAIT tick writes a single dot to stderr by default; --quiet-status prints only changed WAIT snapshots, and --verbose emits detailed per-tick lines.
126
127
 
127
128
  Exit codes:
128
129
  0 WAIT timeout or MARK_READY
@@ -154,6 +155,24 @@ Flags:
154
155
  --help, -h Print this help and exit before any cleanup.
155
156
 
156
157
  Exit code: 0 on success; 1 on validation or cleanup failure.`,
158
+ journal: `pr-shepherd journal
159
+
160
+ Append a list item to the ## Shepherd Journal section of a PR body.
161
+ Creates the section at the end if absent. Idempotent — duplicate items are skipped.
162
+
163
+ Usage:
164
+ pr-shepherd journal [PR] <item> [--dry-run] [--format text|json]
165
+
166
+ PR PR number or GitHub pull request URL. Defaults to current branch PR.
167
+ item Markdown list item: must start with "- " followed by non-whitespace text.
168
+ Example: '- Rejected suggestion: kept existing pattern for consistency.'
169
+
170
+ Flags:
171
+ --dry-run Preview the new PR body without writing it to GitHub.
172
+ --format text|json Output format. Default: text.
173
+ --help, -h Print this help and exit before any GitHub I/O.
174
+
175
+ Exit code: 0 on success (including no-change no-op); 1 on validation, lookup, or mutation failure.`,
157
176
  "log-file": `pr-shepherd log-file
158
177
 
159
178
  Print the per-worktree append-only debug log path for the current repository.
@@ -11,6 +11,7 @@ Usage:
11
11
  pr-shepherd resolve [PR] [resolve-flags]
12
12
  pr-shepherd commit-suggestion [PR] --thread-id ID --message MSG [flags]
13
13
  pr-shepherd mark-files-as-viewed [PR] [files...] [--tests] [--match REGEX]
14
+ pr-shepherd journal [PR] <item> [--dry-run] [--format text|json]
14
15
  pr-shepherd clean <pr|branch|current|repo|all> [value] [flags]
15
16
  pr-shepherd log-file [--format text|json]
16
17
 
@@ -21,6 +22,7 @@ Commands:
21
22
  resolve Apply review-state mutations (requires at least one action flag).
22
23
  commit-suggestion Convert one GitHub suggestion thread into a patch and commit instructions.
23
24
  mark-files-as-viewed Mark PR changed files as viewed in GitHub.
25
+ journal Append a list item to the ## Shepherd Journal section of a PR body.
24
26
  clean Remove pr-shepherd state files.
25
27
  log-file Print the per-worktree debug log path.
26
28
 
@@ -42,6 +44,7 @@ Iterate flags:
42
44
  Poll flags:
43
45
  --interval <duration> Sleep between WAIT ticks. Default: 30s.
44
46
  --timeout <duration> Maximum poll wall-clock wait. Default: 5m.
47
+ --quiet-status During WAIT polling, print only changed status snapshots.
45
48
 
46
49
  Clean variants:
47
50
  pr [number] Remove state for one PR. Defaults to current branch PR.
@@ -1,6 +1,32 @@
1
1
  import { formatFixCodeResult } from "./fix-formatter.mjs";
2
2
  import { joinSections } from "../util/markdown.mjs";
3
3
  import { adaptIterateLog, buildSimpleIterateInstructions, numberInstructions, } from "./iterate-instructions.mjs";
4
+ function formatActivityLine(result) {
5
+ const activity = result.activity ?? {
6
+ commitCount: 0,
7
+ reviewRoundCount: 0,
8
+ latestCommitCommittedAtUnix: null,
9
+ reviewItemsSinceLatestCommit: [],
10
+ };
11
+ const hasActiveChecks = (result.inProgressChecks?.length ?? 0) > 0;
12
+ if (activity.commitCount === 0 &&
13
+ activity.reviewRoundCount === 0 &&
14
+ activity.reviewItemsSinceLatestCommit.length === 0 &&
15
+ !hasActiveChecks) {
16
+ return null;
17
+ }
18
+ const parts = [`${activity.commitCount} commits`, `${activity.reviewRoundCount} review rounds`];
19
+ if (activity.reviewItemsSinceLatestCommit.length > 0) {
20
+ parts.push(`${activity.reviewItemsSinceLatestCommit.length} review items since latest commit`);
21
+ }
22
+ if (hasActiveChecks) {
23
+ parts.push(`active: ${result
24
+ .inProgressChecks.slice(0, 5)
25
+ .map((c) => `\`${c.name}\``)
26
+ .join(", ")}`);
27
+ }
28
+ return `**activity** ${parts.join(" · ")}`;
29
+ }
4
30
  /**
5
31
  * Format an IterateResult as human-readable Markdown.
6
32
  *
@@ -58,6 +84,12 @@ export function formatIterateResult(result, opts) {
58
84
  }
59
85
  summaryLine = segs.join(" · ");
60
86
  }
87
+ // Surface an explicit `--ready-delay` override (set only when the user passed the flag)
88
+ // so the active settle window stays visible on every tick. Replaces the rerun command
89
+ // that previously carried the suffix in the (now removed) recheck instruction.
90
+ if (readyDelaySuffix) {
91
+ summaryLine += ` · **ready-delay** \`${readyDelaySuffix}\` (override)`;
92
+ }
61
93
  const bp = result.branchProtection;
62
94
  const requiredParts = [];
63
95
  if (bp) {
@@ -80,39 +112,42 @@ export function formatIterateResult(result, opts) {
80
112
  const headerLines = [heading, "", baseLine, summaryLine];
81
113
  if (requiredLine)
82
114
  headerLines.push(requiredLine);
115
+ const activityLine = formatActivityLine(result);
116
+ if (activityLine)
117
+ headerLines.push(activityLine);
83
118
  const header = headerLines.join("\n");
84
119
  switch (result.action) {
85
120
  case "wait":
86
121
  return joinSections([
87
122
  header,
88
123
  adaptIterateLog(result.log),
89
- `## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, readyDelaySuffix))}`,
124
+ `## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`,
90
125
  ]);
91
126
  case "mark_ready":
92
127
  return joinSections([
93
128
  header,
94
129
  adaptIterateLog(result.log),
95
- `## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, readyDelaySuffix))}`,
130
+ `## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`,
96
131
  ]);
97
132
  case "cancel": {
98
133
  const cancelHeaderLines = [`${heading} — ${result.reason}`, "", baseLine, summaryLine];
99
134
  if (requiredLine)
100
135
  cancelHeaderLines.push(requiredLine);
136
+ if (activityLine)
137
+ cancelHeaderLines.push(activityLine);
101
138
  return joinSections([
102
139
  cancelHeaderLines.join("\n"),
103
140
  adaptIterateLog(result.log),
104
- `## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, readyDelaySuffix))}`,
141
+ `## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`,
105
142
  ]);
106
143
  }
107
144
  case "escalate":
108
145
  return joinSections([
109
146
  header,
110
147
  result.escalate.humanMessage,
111
- `## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result, readyDelaySuffix))}`,
148
+ `## Instructions\n\n${numberInstructions(buildSimpleIterateInstructions(result))}`,
112
149
  ]);
113
150
  case "fix_code":
114
- return formatFixCodeResult(header, result, {
115
- readyDelaySuffix,
116
- });
151
+ return formatFixCodeResult(header, result);
117
152
  }
118
153
  }
@@ -1,16 +1,10 @@
1
- import { FIX_INSTRUCTION_STOP } from "../commands/iterate/render.mjs";
2
- import { buildPrShepherdCommand } from "./runner.mjs";
3
- function buildRecheckInstruction(rerunCommand, purpose) {
4
- return `Recheck: rerun \`${rerunCommand}\` to ${purpose} once after a fresh 30s–4m delay.`;
5
- }
6
- export function buildSimpleIterateInstructions(result, readyDelaySuffix) {
7
- const rerunCommand = buildIterateCommand(result.pr, readyDelaySuffix);
1
+ export function buildSimpleIterateInstructions(result) {
8
2
  switch (result.action) {
9
3
  case "wait":
10
- return [buildRecheckInstruction(rerunCommand, "continue the active goal")];
4
+ return ["No action this tick — the poll loop reruns automatically."];
11
5
  case "mark_ready":
12
6
  return [
13
- `The CLI already marked the PR ready for review. ${buildRecheckInstruction(rerunCommand, "recheck")}`,
7
+ "The CLI already marked the PR ready for review. No further action this tick — the poll loop reruns automatically.",
14
8
  ];
15
9
  case "cancel":
16
10
  return ["Stop — the active goal is complete."];
@@ -20,22 +14,9 @@ export function buildSimpleIterateInstructions(result, readyDelaySuffix) {
20
14
  ];
21
15
  }
22
16
  }
23
- export function adaptFixCodeInstructions(instructions, pr, readyDelaySuffix) {
24
- const rerunCommand = buildIterateCommand(pr, readyDelaySuffix);
25
- return instructions.map((instruction) => {
26
- if (instruction === FIX_INSTRUCTION_STOP) {
27
- return `${instruction} ${buildRecheckInstruction(rerunCommand, "recheck")}`;
28
- }
29
- return instruction;
30
- });
31
- }
32
17
  export function adaptIterateLog(log) {
33
18
  return log.replace(/\s+—\s+\d+s until auto-cancel/g, "");
34
19
  }
35
- function buildIterateCommand(pr, readyDelaySuffix) {
36
- const suffix = readyDelaySuffix?.trim();
37
- return buildPrShepherdCommand([String(pr), ...(suffix ? ["--ready-delay", suffix] : [])]).text;
38
- }
39
20
  export function numberInstructions(instructions) {
40
21
  return instructions.map((instruction, i) => `${i + 1}. ${instruction}`).join("\n");
41
22
  }
@@ -1,4 +1,4 @@
1
- import { adaptIterateLog, adaptFixCodeInstructions, buildSimpleIterateInstructions, } from "./iterate-instructions.mjs";
1
+ import { adaptIterateLog, buildSimpleIterateInstructions } from "./iterate-instructions.mjs";
2
2
  /**
3
3
  * Project an IterateResult to a lean JSON shape for the default (non-verbose) output.
4
4
  * Omits fields that are the trivial default (false, 0, empty) or state-gated fields
@@ -6,7 +6,16 @@ import { adaptIterateLog, adaptFixCodeInstructions, buildSimpleIterateInstructio
6
6
  */
7
7
  export function projectIterateLean(result, opts) {
8
8
  const readyDelaySuffix = opts?.readyDelaySuffix;
9
- const simpleInstructions = (r) => buildSimpleIterateInstructions(r, readyDelaySuffix);
9
+ const simpleInstructions = (r) => buildSimpleIterateInstructions(r);
10
+ const activity = result.activity ?? {
11
+ commitCount: 0,
12
+ reviewRoundCount: 0,
13
+ latestCommitCommittedAtUnix: null,
14
+ reviewItemsSinceLatestCommit: [],
15
+ };
16
+ const hasActivity = activity.commitCount > 0 ||
17
+ activity.reviewRoundCount > 0 ||
18
+ activity.reviewItemsSinceLatestCommit.length > 0;
10
19
  const base = {
11
20
  action: result.action,
12
21
  pr: result.pr,
@@ -14,6 +23,7 @@ export function projectIterateLean(result, opts) {
14
23
  status: result.status,
15
24
  state: result.state,
16
25
  mergeStateStatus: result.mergeStateStatus,
26
+ ...(readyDelaySuffix && { readyDelayOverride: readyDelaySuffix }),
17
27
  ...(result.mergeStatus === "BLOCKED" &&
18
28
  result.reviewDecision !== null && { reviewDecision: result.reviewDecision }),
19
29
  ...(result.blockingBotReviewInProgress && { blockingBotReviewInProgress: true }),
@@ -24,13 +34,27 @@ export function projectIterateLean(result, opts) {
24
34
  ...(result.summary.filtered > 0 && { filtered: result.summary.filtered }),
25
35
  ...(result.summary.inProgress > 0 && { inProgress: result.summary.inProgress }),
26
36
  },
27
- // remainingSeconds: only when the ready-delay timer is actively counting down
28
37
  ...(result.status === "READY" &&
29
38
  result.remainingSeconds > 0 && {
30
39
  remainingSeconds: result.remainingSeconds,
31
40
  }),
32
41
  ...(result.baseBranch && { baseBranch: result.baseBranch }),
33
42
  ...(result.branchProtection !== null && { branchProtection: result.branchProtection }),
43
+ ...(hasActivity && {
44
+ activity: {
45
+ commitCount: activity.commitCount,
46
+ reviewRoundCount: activity.reviewRoundCount,
47
+ ...(activity.latestCommitCommittedAtUnix !== null && {
48
+ latestCommitCommittedAtUnix: activity.latestCommitCommittedAtUnix,
49
+ }),
50
+ ...(activity.reviewItemsSinceLatestCommit.length > 0 && {
51
+ reviewItemsSinceLatestCommit: activity.reviewItemsSinceLatestCommit,
52
+ }),
53
+ },
54
+ }),
55
+ ...((result.inProgressChecks?.length ?? 0) > 0 && {
56
+ inProgressChecks: result.inProgressChecks,
57
+ }),
34
58
  };
35
59
  switch (result.action) {
36
60
  case "wait":
@@ -96,7 +120,7 @@ export function projectIterateLean(result, opts) {
96
120
  resolveOnlyCommand: result.fix.resolveOnlyCommand,
97
121
  }),
98
122
  ...(result.fix.instructions.length > 0 && {
99
- instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, readyDelaySuffix),
123
+ instructions: result.fix.instructions,
100
124
  }),
101
125
  },
102
126
  };
@@ -131,19 +155,18 @@ export function projectIterateLean(result, opts) {
131
155
  }
132
156
  export function projectIterateVerbose(result, opts) {
133
157
  const readyDelaySuffix = opts?.readyDelaySuffix;
158
+ const readyDelayOverride = readyDelaySuffix ? { readyDelayOverride: readyDelaySuffix } : {};
134
159
  if (result.action === "fix_code") {
135
160
  return {
136
161
  ...result,
137
- fix: {
138
- ...result.fix,
139
- instructions: adaptFixCodeInstructions(result.fix.instructions, result.pr, readyDelaySuffix),
140
- },
162
+ ...readyDelayOverride,
141
163
  };
142
164
  }
143
165
  const log = "log" in result && typeof result.log === "string" ? { log: adaptIterateLog(result.log) } : {};
144
166
  return {
145
167
  ...result,
146
168
  ...log,
147
- instructions: buildSimpleIterateInstructions(result, readyDelaySuffix),
169
+ ...readyDelayOverride,
170
+ instructions: buildSimpleIterateInstructions(result),
148
171
  };
149
172
  }