pr-shepherd 0.25.4 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +23 -0
- package/bin/checks/classify.mjs +10 -2
- package/bin/classify/apply.mjs +110 -0
- package/bin/classify/loader.mjs +85 -0
- package/bin/classify/types.mjs +1 -0
- package/bin/cli/args.mjs +1 -0
- package/bin/cli/default-poll.mjs +1 -0
- package/bin/cli/fix-formatter.mjs +3 -4
- package/bin/cli/help-command-pages.mjs +20 -1
- package/bin/cli/help-top-page.mjs +3 -0
- package/bin/cli/iterate-formatter.mjs +42 -7
- package/bin/cli/iterate-instructions.mjs +3 -22
- package/bin/cli/iterate-lean.mjs +32 -9
- package/bin/cli/journal-handler.mjs +79 -0
- package/bin/cli/poll-handler.mjs +1 -0
- package/bin/cli-parser.mjs +5 -23
- package/bin/commands/check-terminal-report.mjs +1 -0
- package/bin/commands/check.mjs +36 -8
- package/bin/commands/iterate/classify.mjs +14 -5
- package/bin/commands/iterate/fix-code.mjs +2 -2
- package/bin/commands/iterate/helpers.mjs +15 -0
- package/bin/commands/iterate/index.mjs +8 -2
- package/bin/commands/iterate/render.mjs +15 -9
- package/bin/commands/iterate/stall.mjs +4 -0
- package/bin/commands/journal/index.mjs +26 -0
- package/bin/commands/journal/transform.mjs +112 -0
- package/bin/commands/poll.mjs +45 -3
- package/bin/commands/ready-mergeability.mjs +2 -2
- package/bin/commands/shepherd-journal.mjs +2 -2
- package/bin/config/load.mjs +7 -0
- package/bin/config.json +1 -0
- package/bin/github/activity.mjs +57 -0
- package/bin/github/batch-parsers.mjs +20 -34
- package/bin/github/branch-protection.mjs +12 -0
- package/bin/github/client.mjs +17 -1
- package/bin/github/gql/batch-pr.gql +8 -0
- package/bin/github/gql/get-pr-body.gql +8 -0
- package/bin/github/gql/update-pr-body.gql +7 -0
- package/bin/github/queries.mjs +4 -0
- package/bin/types/activity.mjs +1 -0
- package/bin/types/iterate.mjs +0 -1
- package/bin/types.mjs +1 -0
- package/package.json +12 -2
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +7 -22
- package/src/classify/types.mts +48 -0
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
|
package/bin/checks/classify.mjs
CHANGED
|
@@ -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
|
|
20
|
-
|
|
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) => 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
package/bin/cli/default-poll.mjs
CHANGED
|
@@ -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 {
|
|
6
|
-
export function formatFixCodeResult(header, result
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 [
|
|
4
|
+
return ["No action this tick — the poll loop reruns automatically."];
|
|
11
5
|
case "mark_ready":
|
|
12
6
|
return [
|
|
13
|
-
|
|
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
|
}
|
package/bin/cli/iterate-lean.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { adaptIterateLog,
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
169
|
+
...readyDelayOverride,
|
|
170
|
+
instructions: buildSimpleIterateInstructions(result),
|
|
148
171
|
};
|
|
149
172
|
}
|