issue-flow 0.5.2 → 0.7.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/dist/{analyze-QVQ4CTCU.js → analyze-WRFFG2R5.js} +20 -6
- package/dist/analyze-WRFFG2R5.js.map +1 -0
- package/dist/{chunk-O5XI2CFF.js → chunk-2Y3UO7H2.js} +81 -21
- package/dist/chunk-2Y3UO7H2.js.map +1 -0
- package/dist/{chunk-KMX3DUJN.js → chunk-75K5IKAC.js} +19 -7
- package/dist/chunk-75K5IKAC.js.map +1 -0
- package/dist/{chunk-JMSRJ43G.js → chunk-BTBSVBE3.js} +19 -7
- package/dist/chunk-BTBSVBE3.js.map +1 -0
- package/dist/chunk-CW6UE3NW.js +72 -0
- package/dist/chunk-CW6UE3NW.js.map +1 -0
- package/dist/chunk-EWMSQQL2.js +725 -0
- package/dist/chunk-EWMSQQL2.js.map +1 -0
- package/dist/chunk-HS4QWZAO.js +707 -0
- package/dist/chunk-HS4QWZAO.js.map +1 -0
- package/dist/{chunk-3ULIAC2J.js → chunk-JV7P6UGG.js} +2 -2
- package/dist/{chunk-KLBAHTXX.js → chunk-KUDAY57D.js} +2 -2
- package/dist/{chunk-PA3GI7LR.js → chunk-OVFYJLOH.js} +130 -41
- package/dist/chunk-OVFYJLOH.js.map +1 -0
- package/dist/{chunk-LJI45O5B.js → chunk-UX6YAVLT.js} +40 -7
- package/dist/chunk-UX6YAVLT.js.map +1 -0
- package/dist/{chunk-VM7C7L3A.js → chunk-VLSZFMLZ.js} +21 -9
- package/dist/chunk-VLSZFMLZ.js.map +1 -0
- package/dist/{chunk-DLWRUTRL.js → chunk-YXJPKBPV.js} +43 -68
- package/dist/chunk-YXJPKBPV.js.map +1 -0
- package/dist/{chunk-OEJSMZMO.js → chunk-Z3UIGLZG.js} +18 -6
- package/dist/chunk-Z3UIGLZG.js.map +1 -0
- package/dist/cli.js +177 -31
- package/dist/cli.js.map +1 -1
- package/dist/config-UHQ6XXX2.js +31 -0
- package/dist/execute-K5B4UMUT.js +13 -0
- package/dist/generate-DUKQCADD.js +145 -0
- package/dist/generate-DUKQCADD.js.map +1 -0
- package/dist/init-W4HBLW7R.js +9 -0
- package/dist/plan-NKPU45NP.js +15 -0
- package/dist/pr-JYGLLIEF.js +15 -0
- package/dist/pr-review-6H6F4XUY.js +15 -0
- package/dist/prd-FC2ZCEOX.js +15 -0
- package/dist/prd-FC2ZCEOX.js.map +1 -0
- package/dist/review-X4NGDZZI.js +13 -0
- package/dist/review-X4NGDZZI.js.map +1 -0
- package/dist/{run-3GJC54DB.js → run-VC647DQI.js} +177 -63
- package/dist/run-VC647DQI.js.map +1 -0
- package/package.json +2 -1
- package/prompts/analyze.md +18 -5
- package/prompts/generate.md +27 -6
- package/prompts/plan.md +19 -2
- package/prompts/pr-review.md +144 -0
- package/prompts/pr.md +23 -6
- package/prompts/prd.md +18 -5
- package/prompts/review.md +18 -6
- package/dist/analyze-QVQ4CTCU.js.map +0 -1
- package/dist/chunk-DLWRUTRL.js.map +0 -1
- package/dist/chunk-JMSRJ43G.js.map +0 -1
- package/dist/chunk-KMX3DUJN.js.map +0 -1
- package/dist/chunk-LJI45O5B.js.map +0 -1
- package/dist/chunk-O5XI2CFF.js.map +0 -1
- package/dist/chunk-OEJSMZMO.js.map +0 -1
- package/dist/chunk-PA3GI7LR.js.map +0 -1
- package/dist/chunk-VM7C7L3A.js.map +0 -1
- package/dist/execute-TEJ26L4O.js +0 -12
- package/dist/generate-KBQ4KW4T.js +0 -48
- package/dist/generate-KBQ4KW4T.js.map +0 -1
- package/dist/init-4VMPBRMA.js +0 -9
- package/dist/plan-PWYZP5AN.js +0 -13
- package/dist/pr-CJQWZUDR.js +0 -13
- package/dist/prd-4KLQY7BC.js +0 -13
- package/dist/review-YFCWBBKY.js +0 -11
- package/dist/run-3GJC54DB.js.map +0 -1
- /package/dist/{chunk-3ULIAC2J.js.map → chunk-JV7P6UGG.js.map} +0 -0
- /package/dist/{chunk-KLBAHTXX.js.map → chunk-KUDAY57D.js.map} +0 -0
- /package/dist/{execute-TEJ26L4O.js.map → config-UHQ6XXX2.js.map} +0 -0
- /package/dist/{init-4VMPBRMA.js.map → execute-K5B4UMUT.js.map} +0 -0
- /package/dist/{plan-PWYZP5AN.js.map → init-W4HBLW7R.js.map} +0 -0
- /package/dist/{pr-CJQWZUDR.js.map → plan-NKPU45NP.js.map} +0 -0
- /package/dist/{prd-4KLQY7BC.js.map → pr-JYGLLIEF.js.map} +0 -0
- /package/dist/{review-YFCWBBKY.js.map → pr-review-6H6F4XUY.js.map} +0 -0
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
listPullRequests
|
|
4
|
+
} from "./chunk-CW6UE3NW.js";
|
|
5
|
+
import {
|
|
6
|
+
runHeadless
|
|
7
|
+
} from "./chunk-KUDAY57D.js";
|
|
8
|
+
import {
|
|
9
|
+
applyPlaceholders,
|
|
10
|
+
loadPrompt
|
|
11
|
+
} from "./chunk-SLHZPB3C.js";
|
|
12
|
+
import {
|
|
13
|
+
getCurrentBranch,
|
|
14
|
+
getIssueDir,
|
|
15
|
+
loadPrReviewConfig,
|
|
16
|
+
run
|
|
17
|
+
} from "./chunk-OVFYJLOH.js";
|
|
18
|
+
import {
|
|
19
|
+
getGlobalTimeout,
|
|
20
|
+
getSessionPublisher,
|
|
21
|
+
isoNow,
|
|
22
|
+
loadTaskPlan,
|
|
23
|
+
printError,
|
|
24
|
+
printInfo,
|
|
25
|
+
printSuccess,
|
|
26
|
+
printWarning,
|
|
27
|
+
saveTaskPlan,
|
|
28
|
+
writeFileAtomic
|
|
29
|
+
} from "./chunk-2Y3UO7H2.js";
|
|
30
|
+
|
|
31
|
+
// src/commands/pr-review.ts
|
|
32
|
+
import { join as join4 } from "path";
|
|
33
|
+
|
|
34
|
+
// src/core/pr-review/discovery.ts
|
|
35
|
+
import { join } from "path";
|
|
36
|
+
import { createInterface } from "readline";
|
|
37
|
+
var PrDiscoveryError = class extends Error {
|
|
38
|
+
exitCode;
|
|
39
|
+
constructor(message, exitCode = 1) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = "PrDiscoveryError";
|
|
42
|
+
this.exitCode = exitCode;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var MAX_PROMPT_ATTEMPTS = 3;
|
|
46
|
+
var SOURCE_LABELS = {
|
|
47
|
+
argument: "the command argument",
|
|
48
|
+
session: "the current session",
|
|
49
|
+
plan: "tasks.json",
|
|
50
|
+
branch: "the current branch"
|
|
51
|
+
};
|
|
52
|
+
async function loadPlanPullRequest(issue) {
|
|
53
|
+
try {
|
|
54
|
+
const plan = await loadTaskPlan(join(await getIssueDir(issue), "tasks.json"));
|
|
55
|
+
return plan.pullRequest ?? null;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
var defaultSources = {
|
|
61
|
+
sessionPullRequests: () => getSessionPublisher().snapshot().pullRequests,
|
|
62
|
+
planPullRequest: loadPlanPullRequest,
|
|
63
|
+
currentBranch: getCurrentBranch,
|
|
64
|
+
branchPullRequests: listPullRequests
|
|
65
|
+
};
|
|
66
|
+
function mostRecent(pullRequests) {
|
|
67
|
+
let latest = null;
|
|
68
|
+
for (const pr of pullRequests) {
|
|
69
|
+
if (latest === null || pr.number > latest.number) {
|
|
70
|
+
latest = pr;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return latest;
|
|
74
|
+
}
|
|
75
|
+
function parsePrArgument(arg) {
|
|
76
|
+
const trimmed = arg.trim();
|
|
77
|
+
const fromUrl = trimmed.match(/\/pull\/(\d+)/);
|
|
78
|
+
if (fromUrl?.[1] !== void 0) {
|
|
79
|
+
return Number(fromUrl[1]);
|
|
80
|
+
}
|
|
81
|
+
const plain = trimmed.match(/^#?(\d+)$/);
|
|
82
|
+
return plain?.[1] === void 0 ? null : Number(plain[1]);
|
|
83
|
+
}
|
|
84
|
+
function isInteractiveByDefault() {
|
|
85
|
+
const ci = process.env.CI;
|
|
86
|
+
const inCi = ci !== void 0 && ci !== "" && ci !== "0" && ci.toLowerCase() !== "false";
|
|
87
|
+
return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) && !inCi;
|
|
88
|
+
}
|
|
89
|
+
function describe(pr) {
|
|
90
|
+
const parts = [`#${pr.number}`];
|
|
91
|
+
if (pr.title !== null) parts.push(`"${pr.title}"`);
|
|
92
|
+
if (pr.headBranch !== null) parts.push(`(${pr.headBranch})`);
|
|
93
|
+
return parts.join(" ");
|
|
94
|
+
}
|
|
95
|
+
async function confirm(query, stdin, stdout, warn) {
|
|
96
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
97
|
+
const queued = [];
|
|
98
|
+
let pending = null;
|
|
99
|
+
let closed = false;
|
|
100
|
+
rl.on("line", (line) => {
|
|
101
|
+
if (pending !== null) {
|
|
102
|
+
const resolve = pending;
|
|
103
|
+
pending = null;
|
|
104
|
+
resolve(line);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
queued.push(line);
|
|
108
|
+
});
|
|
109
|
+
rl.on("close", () => {
|
|
110
|
+
closed = true;
|
|
111
|
+
if (pending !== null) {
|
|
112
|
+
const resolve = pending;
|
|
113
|
+
pending = null;
|
|
114
|
+
resolve(null);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
const ask = () => new Promise((resolve) => {
|
|
118
|
+
const buffered = queued.shift();
|
|
119
|
+
if (buffered !== void 0) {
|
|
120
|
+
resolve(buffered);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (closed) {
|
|
124
|
+
resolve(null);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
stdout.write(query);
|
|
128
|
+
pending = resolve;
|
|
129
|
+
});
|
|
130
|
+
try {
|
|
131
|
+
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt++) {
|
|
132
|
+
const answer = await ask();
|
|
133
|
+
if (answer === null) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const normalized = answer.trim().toLowerCase();
|
|
137
|
+
if (normalized === "" || normalized === "y" || normalized === "yes") {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
if (normalized === "n" || normalized === "no") {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
warn(`Invalid answer: '${answer.trim()}'. Enter y or n.`);
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
} finally {
|
|
147
|
+
rl.close();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function resolvePullRequest(prArg, opts = {}) {
|
|
151
|
+
const info = opts.info ?? printInfo;
|
|
152
|
+
const warn = opts.warn ?? printWarning;
|
|
153
|
+
const sources = { ...defaultSources, ...opts.sources };
|
|
154
|
+
if (prArg !== void 0 && prArg.trim() !== "") {
|
|
155
|
+
const number = parsePrArgument(prArg);
|
|
156
|
+
if (number === null) {
|
|
157
|
+
throw new PrDiscoveryError(
|
|
158
|
+
`Invalid Pull Request reference '${prArg.trim()}'. Use a number (e.g. 184) or a GitHub Pull Request URL.`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
const url = prArg.trim().includes("/pull/") ? prArg.trim() : null;
|
|
162
|
+
return { number, url, title: null, headBranch: null, source: "argument" };
|
|
163
|
+
}
|
|
164
|
+
const discovered = await discover(sources, opts.issue, warn);
|
|
165
|
+
if (discovered === null) {
|
|
166
|
+
throw new PrDiscoveryError(
|
|
167
|
+
"No Pull Request found for this branch, in tasks.json or in the current session. Run `issue-flow pr-review <number>` with the Pull Request number."
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
const interactive = opts.yes === true ? false : opts.interactive ?? isInteractiveByDefault();
|
|
171
|
+
if (!interactive) {
|
|
172
|
+
info(
|
|
173
|
+
`Reviewing Pull Request ${describe(discovered)}, found in ${SOURCE_LABELS[discovered.source]}.`
|
|
174
|
+
);
|
|
175
|
+
return discovered;
|
|
176
|
+
}
|
|
177
|
+
info(`Pull Request found in ${SOURCE_LABELS[discovered.source]}:`);
|
|
178
|
+
info(` number: #${discovered.number}`);
|
|
179
|
+
info(` title: ${discovered.title ?? "(unknown)"}`);
|
|
180
|
+
info(` branch: ${discovered.headBranch ?? "(unknown)"}`);
|
|
181
|
+
const accepted = await confirm(
|
|
182
|
+
`Review Pull Request #${discovered.number}? (Y/n): `,
|
|
183
|
+
opts.stdin ?? process.stdin,
|
|
184
|
+
opts.stdout ?? process.stdout,
|
|
185
|
+
warn
|
|
186
|
+
);
|
|
187
|
+
if (!accepted) {
|
|
188
|
+
throw new PrDiscoveryError(
|
|
189
|
+
`Cancelled: Pull Request #${discovered.number} was not confirmed. Run \`issue-flow pr-review <number>\` to review a specific Pull Request.`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return discovered;
|
|
193
|
+
}
|
|
194
|
+
async function discover(sources, issue, warn) {
|
|
195
|
+
if (issue !== void 0 && issue !== "") {
|
|
196
|
+
try {
|
|
197
|
+
const fromPlan = await sources.planPullRequest(issue);
|
|
198
|
+
if (fromPlan !== null) {
|
|
199
|
+
return {
|
|
200
|
+
number: fromPlan.number,
|
|
201
|
+
url: fromPlan.url,
|
|
202
|
+
title: null,
|
|
203
|
+
headBranch: fromPlan.headBranch,
|
|
204
|
+
source: "plan"
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
let sessionPrs = [];
|
|
211
|
+
try {
|
|
212
|
+
sessionPrs = sources.sessionPullRequests();
|
|
213
|
+
} catch {
|
|
214
|
+
}
|
|
215
|
+
const fromSession = mostRecent(sessionPrs);
|
|
216
|
+
if (fromSession !== null) {
|
|
217
|
+
return {
|
|
218
|
+
number: fromSession.number,
|
|
219
|
+
url: fromSession.url,
|
|
220
|
+
title: fromSession.title,
|
|
221
|
+
headBranch: null,
|
|
222
|
+
source: "session"
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
let branch = "";
|
|
226
|
+
try {
|
|
227
|
+
branch = await sources.currentBranch();
|
|
228
|
+
} catch (err) {
|
|
229
|
+
warn(
|
|
230
|
+
`Could not detect the current branch: ${err instanceof Error ? err.message : String(err)}`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
if (branch === "") {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
let branchPrs = [];
|
|
237
|
+
try {
|
|
238
|
+
branchPrs = await sources.branchPullRequests(branch);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
warn(
|
|
241
|
+
`Could not list Pull Requests for branch '${branch}': ${err instanceof Error ? err.message : String(err)}`
|
|
242
|
+
);
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
const fromBranch = mostRecent(branchPrs);
|
|
246
|
+
if (fromBranch === null) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
number: fromBranch.number,
|
|
251
|
+
url: fromBranch.url,
|
|
252
|
+
title: fromBranch.title,
|
|
253
|
+
headBranch: branch,
|
|
254
|
+
source: "branch"
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/core/pr-review/publisher.ts
|
|
259
|
+
import { mkdir } from "fs/promises";
|
|
260
|
+
import { join as join3 } from "path";
|
|
261
|
+
|
|
262
|
+
// src/core/pr-review/report.ts
|
|
263
|
+
import { readdir, readFile } from "fs/promises";
|
|
264
|
+
import { join as join2 } from "path";
|
|
265
|
+
var RECOMMENDATIONS = {
|
|
266
|
+
APPROVE: "APPROVE",
|
|
267
|
+
APPROVE_WITH_SUGGESTIONS: "APPROVE_WITH_SUGGESTIONS",
|
|
268
|
+
REQUEST_CHANGES: "REQUEST_CHANGES"
|
|
269
|
+
};
|
|
270
|
+
function parsePrReviewResult(output) {
|
|
271
|
+
const block = output.match(/<pr-review-result>([\s\S]*?)<\/pr-review-result>/);
|
|
272
|
+
const scope = block?.[1] ?? output;
|
|
273
|
+
const line = scope.match(/RECOMMENDATION:\s*([^\n\r]+)/i);
|
|
274
|
+
if (line?.[1] === void 0) {
|
|
275
|
+
return {
|
|
276
|
+
ok: false,
|
|
277
|
+
error: block ? "The <pr-review-result> block has no RECOMMENDATION line." : "No <pr-review-result> block and no RECOMMENDATION line in the output."
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const raw = line[1].trim().replace(/[*`]/g, "").replace(/^_+|[_.]+$/g, "").trim();
|
|
281
|
+
const normalized = raw.toUpperCase().replace(/[\s-]+/g, "_");
|
|
282
|
+
const recommendation = RECOMMENDATIONS[normalized];
|
|
283
|
+
if (recommendation === void 0) {
|
|
284
|
+
return {
|
|
285
|
+
ok: false,
|
|
286
|
+
error: `Unknown recommendation '${raw}'. Expected APPROVE, APPROVE_WITH_SUGGESTIONS or REQUEST_CHANGES.`
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return { ok: true, result: { recommendation, blockers: parseBlockers(scope) } };
|
|
290
|
+
}
|
|
291
|
+
function parseBlockers(scope) {
|
|
292
|
+
const match = scope.match(/BLOCKERS:([\s\S]*)/i);
|
|
293
|
+
if (match?.[1] === void 0) return [];
|
|
294
|
+
return match[1].split("\n").map((entry) => entry.trim()).filter((entry) => /^[-*]\s+/.test(entry)).map((entry) => entry.replace(/^[-*]\s+/, "").trim()).filter((entry) => entry !== "" && !/^(none|nenhum|n\/a)\.?$/i.test(entry));
|
|
295
|
+
}
|
|
296
|
+
var REPORT_SECTIONS = [
|
|
297
|
+
"Executive summary",
|
|
298
|
+
"Strengths",
|
|
299
|
+
"Issues found",
|
|
300
|
+
"Suggested improvements",
|
|
301
|
+
"Architectural observations",
|
|
302
|
+
"Risks identified",
|
|
303
|
+
"Required before merge",
|
|
304
|
+
"Final recommendation"
|
|
305
|
+
];
|
|
306
|
+
var SECTION_ALIASES = {
|
|
307
|
+
"Executive summary": ["executive summary", "summary", "overview", "resumo executivo", "resumo"],
|
|
308
|
+
Strengths: ["strengths", "positives", "highlights", "what went well", "pontos positivos"],
|
|
309
|
+
"Issues found": ["issues found", "issues", "problems", "findings", "problemas encontrados"],
|
|
310
|
+
"Suggested improvements": [
|
|
311
|
+
"suggested improvements",
|
|
312
|
+
"suggestions",
|
|
313
|
+
"improvements",
|
|
314
|
+
"sugestoes de melhoria",
|
|
315
|
+
"sugestoes"
|
|
316
|
+
],
|
|
317
|
+
"Architectural observations": [
|
|
318
|
+
"architectural observations",
|
|
319
|
+
"architecture",
|
|
320
|
+
"architectural notes",
|
|
321
|
+
"observacoes arquiteturais",
|
|
322
|
+
"arquitetura"
|
|
323
|
+
],
|
|
324
|
+
"Risks identified": ["risks identified", "risks", "riscos identificados", "riscos"],
|
|
325
|
+
"Required before merge": [
|
|
326
|
+
"required before merge",
|
|
327
|
+
"blockers",
|
|
328
|
+
"must fix before merge",
|
|
329
|
+
"itens obrigatorios antes do merge"
|
|
330
|
+
],
|
|
331
|
+
"Final recommendation": [
|
|
332
|
+
"final recommendation",
|
|
333
|
+
"recommendation",
|
|
334
|
+
"verdict",
|
|
335
|
+
"recomendacao final",
|
|
336
|
+
"recomendacao"
|
|
337
|
+
]
|
|
338
|
+
};
|
|
339
|
+
function normalizeHeading(heading) {
|
|
340
|
+
return heading.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/^\d+[.)]\s*/, "").replace(/[*_`#]/g, "").replace(/[.:;!?]+$/, "").replace(/\s+/g, " ").trim();
|
|
341
|
+
}
|
|
342
|
+
function matchSection(heading) {
|
|
343
|
+
const normalized = normalizeHeading(heading);
|
|
344
|
+
for (const section of REPORT_SECTIONS) {
|
|
345
|
+
if (SECTION_ALIASES[section].includes(normalized)) return section;
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
function splitBody(body) {
|
|
350
|
+
const sections = /* @__PURE__ */ new Map();
|
|
351
|
+
const extra = [];
|
|
352
|
+
let current = null;
|
|
353
|
+
let buffer = [];
|
|
354
|
+
const flush = () => {
|
|
355
|
+
const content = buffer.join("\n").trim();
|
|
356
|
+
buffer = [];
|
|
357
|
+
if (content === "") return;
|
|
358
|
+
if (current === null) {
|
|
359
|
+
extra.push(content);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const previous = sections.get(current);
|
|
363
|
+
sections.set(current, previous === void 0 ? content : `${previous}
|
|
364
|
+
|
|
365
|
+
${content}`);
|
|
366
|
+
};
|
|
367
|
+
for (const line of body.split("\n")) {
|
|
368
|
+
const heading = line.match(/^#{1,6}\s+(.*)$/);
|
|
369
|
+
if (heading?.[1] === void 0) {
|
|
370
|
+
buffer.push(line);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
flush();
|
|
374
|
+
const section = matchSection(heading[1]);
|
|
375
|
+
current = section;
|
|
376
|
+
if (section === null) {
|
|
377
|
+
buffer.push(line);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
flush();
|
|
381
|
+
return { sections, extra: extra.join("\n\n").trim() };
|
|
382
|
+
}
|
|
383
|
+
var FINDING_LINE = /^[-*]\s*\[(\w+)\]\s+(.+)$/;
|
|
384
|
+
var FINDING_LOCATION = /^(\S+?)(?::(\d+))?\s+[—–-]\s+(.+)$/;
|
|
385
|
+
var SEVERITY_ALIASES = {
|
|
386
|
+
blocker: "blocker",
|
|
387
|
+
blocking: "blocker",
|
|
388
|
+
critical: "blocker",
|
|
389
|
+
high: "high",
|
|
390
|
+
major: "high",
|
|
391
|
+
medium: "medium",
|
|
392
|
+
moderate: "medium",
|
|
393
|
+
low: "low",
|
|
394
|
+
minor: "low",
|
|
395
|
+
nit: "low"
|
|
396
|
+
};
|
|
397
|
+
function parseFindings(body) {
|
|
398
|
+
const { sections } = splitBody(body);
|
|
399
|
+
const scopes = [sections.get("Issues found"), sections.get("Required before merge")];
|
|
400
|
+
const findings = [];
|
|
401
|
+
for (const scope of scopes) {
|
|
402
|
+
if (scope === void 0) continue;
|
|
403
|
+
for (const line of scope.split("\n")) {
|
|
404
|
+
const item = line.trim().match(FINDING_LINE);
|
|
405
|
+
if (item?.[1] === void 0 || item[2] === void 0) continue;
|
|
406
|
+
const severity = SEVERITY_ALIASES[item[1].toLowerCase()];
|
|
407
|
+
if (severity === void 0) continue;
|
|
408
|
+
const rest = item[2].trim();
|
|
409
|
+
const location = rest.match(FINDING_LOCATION);
|
|
410
|
+
if (location?.[1] === void 0 || location[3] === void 0) {
|
|
411
|
+
findings.push({ severity, file: null, line: null, title: rest });
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
findings.push({
|
|
415
|
+
severity,
|
|
416
|
+
file: location[1],
|
|
417
|
+
line: location[2] === void 0 ? null : Number(location[2]),
|
|
418
|
+
title: location[3].trim()
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return dedupeFindings(findings);
|
|
423
|
+
}
|
|
424
|
+
function dedupeFindings(findings) {
|
|
425
|
+
const seen = /* @__PURE__ */ new Set();
|
|
426
|
+
return findings.filter((finding) => {
|
|
427
|
+
const key = `${finding.file ?? ""}:${finding.line ?? ""}:${finding.title}`;
|
|
428
|
+
if (seen.has(key)) return false;
|
|
429
|
+
seen.add(key);
|
|
430
|
+
return true;
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
function buildReportMarkdown(input) {
|
|
434
|
+
const pr = input.pullRequest;
|
|
435
|
+
const parts = [`# Pull Request Review \u2014 #${pr.number} (round ${input.round})`, ""];
|
|
436
|
+
const heading = pr.url === null ? `#${pr.number}` : `[#${pr.number}](${pr.url})`;
|
|
437
|
+
parts.push(`- **Pull Request:** ${heading}${pr.title === null ? "" : ` \u2014 ${pr.title}`}`);
|
|
438
|
+
if (pr.headBranch !== null) parts.push(`- **Head branch:** ${pr.headBranch}`);
|
|
439
|
+
if (input.headSha != null) parts.push(`- **Head SHA:** ${input.headSha}`);
|
|
440
|
+
parts.push(`- **Round:** ${input.round}`);
|
|
441
|
+
parts.push(`- **Reviewed at:** ${input.at}`);
|
|
442
|
+
parts.push(`- **Recommendation:** ${input.recommendation ?? "UNPARSED"}`);
|
|
443
|
+
parts.push("");
|
|
444
|
+
if (input.parseError != null && input.parseError !== "") {
|
|
445
|
+
parts.push(`> Verdict could not be parsed: ${input.parseError}`, "");
|
|
446
|
+
}
|
|
447
|
+
const { sections, extra } = splitBody(input.body);
|
|
448
|
+
for (const section of REPORT_SECTIONS) {
|
|
449
|
+
parts.push(`## ${section}`, "", sections.get(section) ?? "_Not reported._", "");
|
|
450
|
+
}
|
|
451
|
+
if (input.parseError != null && input.parseError !== "") {
|
|
452
|
+
parts.push("## Raw agent output", "", "```text", input.body.trim(), "```", "");
|
|
453
|
+
} else if (extra !== "") {
|
|
454
|
+
parts.push("## Additional notes", "", extra, "");
|
|
455
|
+
}
|
|
456
|
+
return `${parts.join("\n").trimEnd()}
|
|
457
|
+
`;
|
|
458
|
+
}
|
|
459
|
+
async function prReviewDir(opts) {
|
|
460
|
+
const slug = opts.issue === void 0 || opts.issue === "" ? `pr-${opts.pullRequest}` : opts.issue.replace(/^#/, "");
|
|
461
|
+
return join2(await getIssueDir(slug), "pr-review");
|
|
462
|
+
}
|
|
463
|
+
function reportFileName(pullRequest, round) {
|
|
464
|
+
return `pr-${pullRequest}-round-${round}.md`;
|
|
465
|
+
}
|
|
466
|
+
var REPORT_FILE = /^pr-(\d+)-round-(\d+)\.md$/;
|
|
467
|
+
async function readPrReviewIndex(dir) {
|
|
468
|
+
try {
|
|
469
|
+
const parsed = JSON.parse(await readFile(join2(dir, "index.json"), "utf-8"));
|
|
470
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
471
|
+
const index = parsed;
|
|
472
|
+
if (!Array.isArray(index.rounds) || typeof index.pullRequest !== "object") return null;
|
|
473
|
+
return {
|
|
474
|
+
schemaVersion: 1,
|
|
475
|
+
pullRequest: index.pullRequest,
|
|
476
|
+
rounds: index.rounds
|
|
477
|
+
};
|
|
478
|
+
} catch {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async function roundsOnDisk(dir, pullRequest) {
|
|
483
|
+
try {
|
|
484
|
+
return (await readdir(dir)).map((name) => name.match(REPORT_FILE)).filter((match) => match !== null).filter((match) => Number(match[1]) === pullRequest).map((match) => Number(match[2]));
|
|
485
|
+
} catch {
|
|
486
|
+
return [];
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async function resolveRound(dir, pullRequest, explicit) {
|
|
490
|
+
if (explicit !== void 0) return explicit;
|
|
491
|
+
const index = await readPrReviewIndex(dir);
|
|
492
|
+
const known = [
|
|
493
|
+
...(index?.rounds ?? []).map((entry) => entry.round),
|
|
494
|
+
...await roundsOnDisk(dir, pullRequest)
|
|
495
|
+
];
|
|
496
|
+
return known.length === 0 ? 1 : Math.max(...known) + 1;
|
|
497
|
+
}
|
|
498
|
+
function upsertRound(index, entry) {
|
|
499
|
+
const rounds = index.rounds.filter((existing) => existing.round !== entry.round);
|
|
500
|
+
rounds.push(entry);
|
|
501
|
+
rounds.sort((a, b) => a.round - b.round);
|
|
502
|
+
return { ...index, rounds };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/core/pr-review/publisher.ts
|
|
506
|
+
var LocalReportPublisher = class {
|
|
507
|
+
constructor(dir) {
|
|
508
|
+
this.dir = dir;
|
|
509
|
+
}
|
|
510
|
+
async publish(report) {
|
|
511
|
+
await mkdir(this.dir, { recursive: true });
|
|
512
|
+
const fileName = reportFileName(report.pullRequest.number, report.round);
|
|
513
|
+
await writeFileAtomic(join3(this.dir, fileName), report.markdown);
|
|
514
|
+
const existing = await readPrReviewIndex(this.dir);
|
|
515
|
+
const index = {
|
|
516
|
+
schemaVersion: 1,
|
|
517
|
+
pullRequest: report.pullRequest,
|
|
518
|
+
rounds: existing?.rounds ?? []
|
|
519
|
+
};
|
|
520
|
+
const updated = upsertRound(index, {
|
|
521
|
+
round: report.round,
|
|
522
|
+
at: report.at,
|
|
523
|
+
recommendation: report.recommendation,
|
|
524
|
+
headSha: report.headSha,
|
|
525
|
+
reportPath: fileName,
|
|
526
|
+
findings: report.findings
|
|
527
|
+
});
|
|
528
|
+
await writeFileAtomic(join3(this.dir, "index.json"), `${JSON.stringify(updated, null, 2)}
|
|
529
|
+
`);
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
var PUBLISHERS = {
|
|
533
|
+
local: (dir) => new LocalReportPublisher(dir)
|
|
534
|
+
};
|
|
535
|
+
function createPrReviewPublisher(dir, kind = "local") {
|
|
536
|
+
return PUBLISHERS[kind](dir);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/commands/pr-review.ts
|
|
540
|
+
var FAIL_ON_LEVELS = ["request-changes", "suggestions", "none"];
|
|
541
|
+
var DEFAULT_FAIL_ON = "request-changes";
|
|
542
|
+
var NO_ISSUE = "none";
|
|
543
|
+
var NO_PATH = "(none)";
|
|
544
|
+
function exitCodeFor(recommendation, failOn) {
|
|
545
|
+
if (failOn === "none") {
|
|
546
|
+
return 0;
|
|
547
|
+
}
|
|
548
|
+
if (recommendation === "REQUEST_CHANGES") {
|
|
549
|
+
return 2;
|
|
550
|
+
}
|
|
551
|
+
return failOn === "suggestions" && recommendation === "APPROVE_WITH_SUGGESTIONS" ? 2 : 0;
|
|
552
|
+
}
|
|
553
|
+
function parseFailOn(value) {
|
|
554
|
+
if (value === void 0 || value === "") {
|
|
555
|
+
return DEFAULT_FAIL_ON;
|
|
556
|
+
}
|
|
557
|
+
const normalized = value.trim().toLowerCase();
|
|
558
|
+
return FAIL_ON_LEVELS.find((level) => level === normalized) ?? null;
|
|
559
|
+
}
|
|
560
|
+
async function fetchPullRequestMetadata(number) {
|
|
561
|
+
try {
|
|
562
|
+
const result = await run("gh", [
|
|
563
|
+
"pr",
|
|
564
|
+
"view",
|
|
565
|
+
String(number),
|
|
566
|
+
"--json",
|
|
567
|
+
"title,url,headRefName,headRefOid"
|
|
568
|
+
]);
|
|
569
|
+
if (result.exitCode !== 0) {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
const parsed = JSON.parse(result.stdout);
|
|
573
|
+
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
574
|
+
} catch {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function mergePullRequest(target, meta) {
|
|
579
|
+
return {
|
|
580
|
+
number: target.number,
|
|
581
|
+
url: target.url ?? meta?.url ?? null,
|
|
582
|
+
title: target.title ?? meta?.title ?? null,
|
|
583
|
+
headBranch: target.headBranch ?? meta?.headRefName ?? null
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
async function persistState(input) {
|
|
587
|
+
try {
|
|
588
|
+
const plan = await loadTaskPlan(input.tasksPath);
|
|
589
|
+
plan.pipeline.prReviewCompleted = input.recommendation === "APPROVE" || input.recommendation === "APPROVE_WITH_SUGGESTIONS";
|
|
590
|
+
plan.prReview = {
|
|
591
|
+
// Preserved, never turned on here: opting the pipeline in is `run
|
|
592
|
+
// --pr-review`'s decision, not a side effect of a manual review.
|
|
593
|
+
enabled: plan.prReview?.enabled ?? false,
|
|
594
|
+
pullRequestNumber: input.pullRequestNumber,
|
|
595
|
+
// `--round <n>` rewrites an earlier round, which must not shrink the count.
|
|
596
|
+
rounds: Math.max(plan.prReview?.rounds ?? 0, input.round),
|
|
597
|
+
lastRecommendation: input.recommendation ?? plan.prReview?.lastRecommendation,
|
|
598
|
+
lastReviewedAt: input.at
|
|
599
|
+
};
|
|
600
|
+
await saveTaskPlan(input.tasksPath, plan);
|
|
601
|
+
} catch {
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
async function runPrReview(prArg, opts = {}) {
|
|
605
|
+
const failOn = parseFailOn(opts.failOn);
|
|
606
|
+
if (failOn === null) {
|
|
607
|
+
printError(`Invalid --fail-on value '${opts.failOn}'. Expected ${FAIL_ON_LEVELS.join(", ")}.`);
|
|
608
|
+
return 1;
|
|
609
|
+
}
|
|
610
|
+
const explicitRound = opts.round === void 0 ? void 0 : Number(opts.round);
|
|
611
|
+
if (explicitRound !== void 0 && (!Number.isInteger(explicitRound) || explicitRound < 1)) {
|
|
612
|
+
printError(`Invalid --round value '${opts.round}'. Expected a positive integer.`);
|
|
613
|
+
return 1;
|
|
614
|
+
}
|
|
615
|
+
const issueRef = opts.issue?.replace(/^#/, "").trim();
|
|
616
|
+
const issue = issueRef === void 0 || issueRef === "" ? void 0 : issueRef;
|
|
617
|
+
let target;
|
|
618
|
+
try {
|
|
619
|
+
target = await resolvePullRequest(prArg, { issue, yes: opts.yes });
|
|
620
|
+
} catch (err) {
|
|
621
|
+
if (err instanceof PrDiscoveryError) {
|
|
622
|
+
printError(err.message);
|
|
623
|
+
return err.exitCode;
|
|
624
|
+
}
|
|
625
|
+
printError(
|
|
626
|
+
`Could not determine which Pull Request to review: ${err instanceof Error ? err.message : String(err)}`
|
|
627
|
+
);
|
|
628
|
+
return 1;
|
|
629
|
+
}
|
|
630
|
+
const issueDir = issue === void 0 ? null : await getIssueDir(issue);
|
|
631
|
+
const tasksPath = issueDir === null ? null : join4(issueDir, "tasks.json");
|
|
632
|
+
const prdPath = issueDir === null ? null : join4(issueDir, "prd.md");
|
|
633
|
+
const dir = await prReviewDir({ issue, pullRequest: target.number });
|
|
634
|
+
const round = await resolveRound(dir, target.number, explicitRound);
|
|
635
|
+
const reportPath = join4(dir, reportFileName(target.number, round));
|
|
636
|
+
const template = await loadPrompt("pr-review");
|
|
637
|
+
const prompt = applyPlaceholders(template, {
|
|
638
|
+
__PR_NUMBER__: String(target.number),
|
|
639
|
+
__ISSUE_NUMBER__: issue ?? NO_ISSUE,
|
|
640
|
+
__TASKS_PATH__: tasksPath ?? NO_PATH,
|
|
641
|
+
__PRD_PATH__: prdPath ?? NO_PATH,
|
|
642
|
+
__REPORT_PATH__: reportPath,
|
|
643
|
+
__ROUND__: String(round)
|
|
644
|
+
});
|
|
645
|
+
const result = await runHeadless({
|
|
646
|
+
prompt,
|
|
647
|
+
maxTurns: 40,
|
|
648
|
+
timeout: getGlobalTimeout() ?? 9e5,
|
|
649
|
+
outputFormat: "text",
|
|
650
|
+
allowedTools: ["Bash", "Read", "Glob", "Grep"],
|
|
651
|
+
statusMessage: `Reviewing Pull Request #${target.number} (round ${round})...`
|
|
652
|
+
});
|
|
653
|
+
if (!result.success) {
|
|
654
|
+
printError(`PR review failed: ${result.error}`);
|
|
655
|
+
return 1;
|
|
656
|
+
}
|
|
657
|
+
const parsed = parsePrReviewResult(result.result);
|
|
658
|
+
const recommendation = parsed.ok ? parsed.result.recommendation : null;
|
|
659
|
+
const blockers = parsed.ok ? parsed.result.blockers : [];
|
|
660
|
+
const meta = await fetchPullRequestMetadata(target.number);
|
|
661
|
+
const pullRequest = mergePullRequest(target, meta);
|
|
662
|
+
const at = isoNow();
|
|
663
|
+
const headSha = meta?.headRefOid ?? null;
|
|
664
|
+
const report = {
|
|
665
|
+
pullRequest,
|
|
666
|
+
round,
|
|
667
|
+
at,
|
|
668
|
+
headSha,
|
|
669
|
+
recommendation,
|
|
670
|
+
blockers,
|
|
671
|
+
findings: parseFindings(result.result),
|
|
672
|
+
markdown: buildReportMarkdown({
|
|
673
|
+
pullRequest,
|
|
674
|
+
round,
|
|
675
|
+
at,
|
|
676
|
+
headSha,
|
|
677
|
+
recommendation,
|
|
678
|
+
body: result.result,
|
|
679
|
+
parseError: parsed.ok ? null : parsed.error
|
|
680
|
+
})
|
|
681
|
+
};
|
|
682
|
+
const publisher = opts.publisher ?? createPrReviewPublisher(dir, (await loadPrReviewConfig()).publisher);
|
|
683
|
+
try {
|
|
684
|
+
await publisher.publish(report);
|
|
685
|
+
} catch (err) {
|
|
686
|
+
printError(
|
|
687
|
+
`Could not publish the review report: ${err instanceof Error ? err.message : String(err)}`
|
|
688
|
+
);
|
|
689
|
+
return 1;
|
|
690
|
+
}
|
|
691
|
+
if (tasksPath !== null) {
|
|
692
|
+
await persistState({
|
|
693
|
+
tasksPath,
|
|
694
|
+
pullRequestNumber: target.number,
|
|
695
|
+
round,
|
|
696
|
+
recommendation,
|
|
697
|
+
at
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
printInfo(`Report: ${reportPath}`);
|
|
701
|
+
if (!parsed.ok || recommendation === null) {
|
|
702
|
+
printError(`PR review could not be parsed: ${parsed.ok ? "no recommendation" : parsed.error}`);
|
|
703
|
+
return 1;
|
|
704
|
+
}
|
|
705
|
+
if (recommendation === "REQUEST_CHANGES") {
|
|
706
|
+
printError(`PR review #${target.number}: REQUEST_CHANGES`);
|
|
707
|
+
for (const blocker of blockers) {
|
|
708
|
+
console.log(` - ${blocker}`);
|
|
709
|
+
}
|
|
710
|
+
return exitCodeFor(recommendation, failOn);
|
|
711
|
+
}
|
|
712
|
+
printSuccess(
|
|
713
|
+
recommendation === "APPROVE" ? `PR review #${target.number}: APPROVE` : `PR review #${target.number}: APPROVE_WITH_SUGGESTIONS \u2014 no blockers, improvements suggested`
|
|
714
|
+
);
|
|
715
|
+
return exitCodeFor(recommendation, failOn);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export {
|
|
719
|
+
mostRecent,
|
|
720
|
+
prReviewDir,
|
|
721
|
+
readPrReviewIndex,
|
|
722
|
+
exitCodeFor,
|
|
723
|
+
runPrReview
|
|
724
|
+
};
|
|
725
|
+
//# sourceMappingURL=chunk-EWMSQQL2.js.map
|