issue-flow 0.8.0 → 0.9.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/README.md +16 -0
- package/dist/{analyze-RBNKXWL5.js → analyze-VSWAEVN3.js} +6 -6
- package/dist/{chunk-BH7DYADV.js → chunk-57MH6RYJ.js} +97 -7
- package/dist/chunk-57MH6RYJ.js.map +1 -0
- package/dist/{chunk-J4SICTAN.js → chunk-66ZGYXTH.js} +53 -9
- package/dist/chunk-66ZGYXTH.js.map +1 -0
- package/dist/{chunk-TGP6WK65.js → chunk-DP43WJY4.js} +75 -11
- package/dist/chunk-DP43WJY4.js.map +1 -0
- package/dist/{chunk-QO2MWMAZ.js → chunk-DTNM3LTO.js} +33 -10
- package/dist/chunk-DTNM3LTO.js.map +1 -0
- package/dist/{chunk-JWZKV3CL.js → chunk-JGKPSWOA.js} +2 -2
- package/dist/{chunk-HSGTZ3CT.js → chunk-KXAG24DD.js} +219 -3
- package/dist/chunk-KXAG24DD.js.map +1 -0
- package/dist/{chunk-J3PJBZXJ.js → chunk-M37YZYDO.js} +3 -3
- package/dist/{chunk-VPDWTZ3F.js → chunk-M72ZEPWD.js} +2 -2
- package/dist/{chunk-BASRRXAM.js → chunk-MTJQOOMG.js} +105 -4
- package/dist/chunk-MTJQOOMG.js.map +1 -0
- package/dist/{chunk-RQW5W4XC.js → chunk-NB6WKTBL.js} +2 -2
- package/dist/chunk-TJMZWRS6.js +265 -0
- package/dist/chunk-TJMZWRS6.js.map +1 -0
- package/dist/{chunk-VSF2CR3G.js → chunk-TLSIK656.js} +6 -6
- package/dist/{chunk-F52S6ZGF.js → chunk-VNKSMWDN.js} +17 -3
- package/dist/chunk-VNKSMWDN.js.map +1 -0
- package/dist/{chunk-36ECROTC.js → chunk-WHEU55ZP.js} +7 -7
- package/dist/{chunk-A4M6AKIK.js → chunk-X7XBF5TS.js} +6 -6
- package/dist/cli.js +91 -29
- package/dist/cli.js.map +1 -1
- package/dist/{config-R7O5YC65.js → config-J5DW6HSY.js} +3 -3
- package/dist/execute-Z2DKGVLR.js +14 -0
- package/dist/{generate-TGJQLGMV.js → generate-ERDPOTKX.js} +6 -6
- package/dist/init-UY6ZCIFT.js +9 -0
- package/dist/plan-REMAMCX4.js +16 -0
- package/dist/pr-KERP52TI.js +20 -0
- package/dist/pr-review-IU4N3TEK.js +16 -0
- package/dist/prd-SHVQW6RM.js +16 -0
- package/dist/review-L5F77QAC.js +14 -0
- package/dist/run-BRBFHVUO.js +1860 -0
- package/dist/run-BRBFHVUO.js.map +1 -0
- package/dist/{web-JVPK2PIF.js → web-WQISLO66.js} +4 -4
- package/package.json +1 -1
- package/prompts/execute.md +2 -2
- package/prompts/plan.md +10 -1
- package/prompts/pr.md +9 -4
- package/web/CLAUDE.md +22 -8
- package/web/public/app.css +178 -0
- package/web/public/app.js +336 -17
- package/web/public/index.html +24 -0
- package/dist/chunk-BASRRXAM.js.map +0 -1
- package/dist/chunk-BH7DYADV.js.map +0 -1
- package/dist/chunk-F52S6ZGF.js.map +0 -1
- package/dist/chunk-HSGTZ3CT.js.map +0 -1
- package/dist/chunk-J4SICTAN.js.map +0 -1
- package/dist/chunk-MK43N4X3.js +0 -123
- package/dist/chunk-MK43N4X3.js.map +0 -1
- package/dist/chunk-QO2MWMAZ.js.map +0 -1
- package/dist/chunk-TGP6WK65.js.map +0 -1
- package/dist/execute-LU3HWA54.js +0 -14
- package/dist/init-WP7Z2LFQ.js +0 -9
- package/dist/plan-2MWMCKRY.js +0 -16
- package/dist/pr-XVQDEQ7B.js +0 -16
- package/dist/pr-review-VO6ZXAFM.js +0 -16
- package/dist/prd-FPZAHQEO.js +0 -16
- package/dist/review-AHALLMXO.js +0 -14
- package/dist/run-2CXVR3VZ.js +0 -733
- package/dist/run-2CXVR3VZ.js.map +0 -1
- /package/dist/{analyze-RBNKXWL5.js.map → analyze-VSWAEVN3.js.map} +0 -0
- /package/dist/{chunk-JWZKV3CL.js.map → chunk-JGKPSWOA.js.map} +0 -0
- /package/dist/{chunk-J3PJBZXJ.js.map → chunk-M37YZYDO.js.map} +0 -0
- /package/dist/{chunk-VPDWTZ3F.js.map → chunk-M72ZEPWD.js.map} +0 -0
- /package/dist/{chunk-RQW5W4XC.js.map → chunk-NB6WKTBL.js.map} +0 -0
- /package/dist/{chunk-VSF2CR3G.js.map → chunk-TLSIK656.js.map} +0 -0
- /package/dist/{chunk-36ECROTC.js.map → chunk-WHEU55ZP.js.map} +0 -0
- /package/dist/{chunk-A4M6AKIK.js.map → chunk-X7XBF5TS.js.map} +0 -0
- /package/dist/{config-R7O5YC65.js.map → config-J5DW6HSY.js.map} +0 -0
- /package/dist/{execute-LU3HWA54.js.map → execute-Z2DKGVLR.js.map} +0 -0
- /package/dist/{generate-TGJQLGMV.js.map → generate-ERDPOTKX.js.map} +0 -0
- /package/dist/{init-WP7Z2LFQ.js.map → init-UY6ZCIFT.js.map} +0 -0
- /package/dist/{plan-2MWMCKRY.js.map → plan-REMAMCX4.js.map} +0 -0
- /package/dist/{pr-XVQDEQ7B.js.map → pr-KERP52TI.js.map} +0 -0
- /package/dist/{pr-review-VO6ZXAFM.js.map → pr-review-IU4N3TEK.js.map} +0 -0
- /package/dist/{prd-FPZAHQEO.js.map → prd-SHVQW6RM.js.map} +0 -0
- /package/dist/{review-AHALLMXO.js.map → review-L5F77QAC.js.map} +0 -0
- /package/dist/{web-JVPK2PIF.js.map → web-WQISLO66.js.map} +0 -0
|
@@ -0,0 +1,1860 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
runPrd
|
|
4
|
+
} from "./chunk-WHEU55ZP.js";
|
|
5
|
+
import {
|
|
6
|
+
runReview
|
|
7
|
+
} from "./chunk-X7XBF5TS.js";
|
|
8
|
+
import {
|
|
9
|
+
ensureWebMonitor
|
|
10
|
+
} from "./chunk-VNKSMWDN.js";
|
|
11
|
+
import {
|
|
12
|
+
runInit
|
|
13
|
+
} from "./chunk-M72ZEPWD.js";
|
|
14
|
+
import {
|
|
15
|
+
printQueueSummary,
|
|
16
|
+
printRunSummary,
|
|
17
|
+
runExecute
|
|
18
|
+
} from "./chunk-DP43WJY4.js";
|
|
19
|
+
import {
|
|
20
|
+
runPlan
|
|
21
|
+
} from "./chunk-TJMZWRS6.js";
|
|
22
|
+
import {
|
|
23
|
+
runPr
|
|
24
|
+
} from "./chunk-66ZGYXTH.js";
|
|
25
|
+
import "./chunk-NB6WKTBL.js";
|
|
26
|
+
import {
|
|
27
|
+
emptyRelations,
|
|
28
|
+
getProvider,
|
|
29
|
+
resolveCommandIssue,
|
|
30
|
+
uniqueIds
|
|
31
|
+
} from "./chunk-KXAG24DD.js";
|
|
32
|
+
import "./chunk-HDV3YDTG.js";
|
|
33
|
+
import {
|
|
34
|
+
mostRecent,
|
|
35
|
+
prReviewDir,
|
|
36
|
+
readPrReviewIndex,
|
|
37
|
+
runPrReview
|
|
38
|
+
} from "./chunk-TLSIK656.js";
|
|
39
|
+
import {
|
|
40
|
+
listPullRequests,
|
|
41
|
+
publishGitState
|
|
42
|
+
} from "./chunk-M37YZYDO.js";
|
|
43
|
+
import "./chunk-JGKPSWOA.js";
|
|
44
|
+
import {
|
|
45
|
+
beginUsageScope,
|
|
46
|
+
getRunUsageTotals
|
|
47
|
+
} from "./chunk-DTNM3LTO.js";
|
|
48
|
+
import "./chunk-SLHZPB3C.js";
|
|
49
|
+
import {
|
|
50
|
+
executionPlanSchema,
|
|
51
|
+
loadIssuesConfig,
|
|
52
|
+
loadWebConfig,
|
|
53
|
+
resolveIssuePaths,
|
|
54
|
+
resolveProjectPaths,
|
|
55
|
+
resolveQueuePaths
|
|
56
|
+
} from "./chunk-MTJQOOMG.js";
|
|
57
|
+
import {
|
|
58
|
+
FilePublisher,
|
|
59
|
+
NullPublisher,
|
|
60
|
+
isVerbose,
|
|
61
|
+
isoNow,
|
|
62
|
+
loadTaskPlan,
|
|
63
|
+
printError,
|
|
64
|
+
printInfo,
|
|
65
|
+
printSuccess,
|
|
66
|
+
printWarning,
|
|
67
|
+
saveTaskPlan,
|
|
68
|
+
setOutputCallback,
|
|
69
|
+
setSessionPublisher,
|
|
70
|
+
setStoryStageCallback,
|
|
71
|
+
setStoryUpdateCallback,
|
|
72
|
+
writeFileAtomic
|
|
73
|
+
} from "./chunk-57MH6RYJ.js";
|
|
74
|
+
|
|
75
|
+
// src/commands/run.ts
|
|
76
|
+
import { randomUUID } from "crypto";
|
|
77
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
78
|
+
import { join } from "path";
|
|
79
|
+
import { execa } from "execa";
|
|
80
|
+
|
|
81
|
+
// src/core/pipeline.ts
|
|
82
|
+
var PIPELINE_PHASES = ["init", "prd", "plan", "execute", "review", "pr"];
|
|
83
|
+
var PIPELINE_PHASES_WITH_PR_REVIEW = [...PIPELINE_PHASES, "pr-review"];
|
|
84
|
+
var PIPELINE_PHASES_NO_BRANCH = PIPELINE_PHASES.filter(
|
|
85
|
+
(p) => p !== "pr"
|
|
86
|
+
);
|
|
87
|
+
var PHASE_TO_FIELD = {
|
|
88
|
+
init: null,
|
|
89
|
+
prd: "prdCompleted",
|
|
90
|
+
plan: "jsonCompleted",
|
|
91
|
+
execute: "executionCompleted",
|
|
92
|
+
review: "reviewCompleted",
|
|
93
|
+
pr: "prCreated",
|
|
94
|
+
"pr-review": "prReviewCompleted"
|
|
95
|
+
};
|
|
96
|
+
var PipelineManager = class {
|
|
97
|
+
tasksJsonPath;
|
|
98
|
+
plan;
|
|
99
|
+
activePhases;
|
|
100
|
+
constructor(plan, tasksJsonPath, activePhases) {
|
|
101
|
+
this.plan = plan;
|
|
102
|
+
this.tasksJsonPath = tasksJsonPath;
|
|
103
|
+
this.activePhases = activePhases ?? PIPELINE_PHASES;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Reload state from disk.
|
|
107
|
+
*/
|
|
108
|
+
async reload() {
|
|
109
|
+
this.plan = await loadTaskPlan(this.tasksJsonPath);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Check if a phase is complete.
|
|
113
|
+
*/
|
|
114
|
+
isPhaseComplete(phase) {
|
|
115
|
+
const field = PHASE_TO_FIELD[phase];
|
|
116
|
+
if (field === null) return true;
|
|
117
|
+
return this.plan.pipeline?.[field] ?? false;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get the first incomplete phase in the pipeline.
|
|
121
|
+
* Returns null if all phases are complete.
|
|
122
|
+
*/
|
|
123
|
+
getNextPhase() {
|
|
124
|
+
for (const phase of this.activePhases) {
|
|
125
|
+
if (!this.isPhaseComplete(phase)) {
|
|
126
|
+
return phase;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get the current phase (alias for getNextPhase).
|
|
133
|
+
*/
|
|
134
|
+
getCurrentPhase() {
|
|
135
|
+
return this.getNextPhase();
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Check whether we can resume from a specific phase.
|
|
139
|
+
* All prerequisite phases must be complete.
|
|
140
|
+
*/
|
|
141
|
+
canResume(fromPhase) {
|
|
142
|
+
const idx = this.activePhases.indexOf(fromPhase);
|
|
143
|
+
if (idx < 0) return false;
|
|
144
|
+
for (let i = 0; i < idx; i++) {
|
|
145
|
+
if (!this.isPhaseComplete(this.activePhases[i])) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Mark a phase as complete and persist to disk.
|
|
153
|
+
*/
|
|
154
|
+
async markPhaseComplete(phase) {
|
|
155
|
+
const field = PHASE_TO_FIELD[phase];
|
|
156
|
+
if (field === null) return;
|
|
157
|
+
this.plan = {
|
|
158
|
+
...this.plan,
|
|
159
|
+
pipeline: {
|
|
160
|
+
...this.plan.pipeline ?? {
|
|
161
|
+
prdCompleted: false,
|
|
162
|
+
jsonCompleted: false,
|
|
163
|
+
executionCompleted: false,
|
|
164
|
+
reviewCompleted: false,
|
|
165
|
+
prCreated: false
|
|
166
|
+
},
|
|
167
|
+
[field]: true
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
await saveTaskPlan(this.tasksJsonPath, this.plan);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// src/execution/plan.ts
|
|
175
|
+
import { mkdir, readFile } from "fs/promises";
|
|
176
|
+
import { dirname } from "path";
|
|
177
|
+
import { ZodError } from "zod";
|
|
178
|
+
|
|
179
|
+
// src/issues/graph.ts
|
|
180
|
+
var DEFAULT_MAX_NODES = 25;
|
|
181
|
+
var DEFAULT_MAX_DEPTH = 3;
|
|
182
|
+
function dependencyEdges(graph) {
|
|
183
|
+
const edges = [];
|
|
184
|
+
const seen = /* @__PURE__ */ new Set();
|
|
185
|
+
const add = (from, to) => {
|
|
186
|
+
if (from === to || !graph.nodes.has(from) || !graph.nodes.has(to)) return;
|
|
187
|
+
const key = `${from}->${to}`;
|
|
188
|
+
if (seen.has(key)) return;
|
|
189
|
+
seen.add(key);
|
|
190
|
+
edges.push({ from, to });
|
|
191
|
+
};
|
|
192
|
+
for (const node of graph.nodes.values()) {
|
|
193
|
+
for (const blocker of node.relations.blockedBy) {
|
|
194
|
+
add(blocker, node.id);
|
|
195
|
+
}
|
|
196
|
+
for (const blocked of node.relations.blocking) {
|
|
197
|
+
add(node.id, blocked);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return edges;
|
|
201
|
+
}
|
|
202
|
+
function findCycles(graph) {
|
|
203
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
204
|
+
for (const { from, to } of dependencyEdges(graph)) {
|
|
205
|
+
const list = adjacency.get(from);
|
|
206
|
+
if (list === void 0) {
|
|
207
|
+
adjacency.set(from, [to]);
|
|
208
|
+
} else {
|
|
209
|
+
list.push(to);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const cycles = [];
|
|
213
|
+
const reported = /* @__PURE__ */ new Set();
|
|
214
|
+
const state = /* @__PURE__ */ new Map();
|
|
215
|
+
const stack = [];
|
|
216
|
+
const record = (cycle) => {
|
|
217
|
+
let pivot = 0;
|
|
218
|
+
for (let i = 1; i < cycle.length; i++) {
|
|
219
|
+
if (compareIds(cycle[i], cycle[pivot]) < 0) pivot = i;
|
|
220
|
+
}
|
|
221
|
+
const normalized = [...cycle.slice(pivot), ...cycle.slice(0, pivot)];
|
|
222
|
+
const key = normalized.join("->");
|
|
223
|
+
if (reported.has(key)) return;
|
|
224
|
+
reported.add(key);
|
|
225
|
+
cycles.push(normalized);
|
|
226
|
+
};
|
|
227
|
+
const visit = (id) => {
|
|
228
|
+
state.set(id, "visiting");
|
|
229
|
+
stack.push(id);
|
|
230
|
+
for (const next of adjacency.get(id) ?? []) {
|
|
231
|
+
const status = state.get(next);
|
|
232
|
+
if (status === "visiting") {
|
|
233
|
+
record(stack.slice(stack.indexOf(next)));
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (status === void 0) {
|
|
237
|
+
visit(next);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
stack.pop();
|
|
241
|
+
state.set(id, "done");
|
|
242
|
+
};
|
|
243
|
+
for (const id of graph.nodes.keys()) {
|
|
244
|
+
if (!state.has(id)) visit(id);
|
|
245
|
+
}
|
|
246
|
+
return cycles;
|
|
247
|
+
}
|
|
248
|
+
function compareIds(a, b) {
|
|
249
|
+
const na = Number(a);
|
|
250
|
+
const nb = Number(b);
|
|
251
|
+
if (Number.isInteger(na) && Number.isInteger(nb)) {
|
|
252
|
+
return na - nb;
|
|
253
|
+
}
|
|
254
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
255
|
+
}
|
|
256
|
+
function neighbours(relations) {
|
|
257
|
+
return uniqueIds(
|
|
258
|
+
[
|
|
259
|
+
...relations.parent === null ? [] : [relations.parent],
|
|
260
|
+
...relations.children,
|
|
261
|
+
...relations.blockedBy,
|
|
262
|
+
...relations.blocking
|
|
263
|
+
],
|
|
264
|
+
relations.id
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
async function buildDependencyGraph(roots, fetchRelations, options = {}) {
|
|
268
|
+
const maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES;
|
|
269
|
+
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
270
|
+
const warn = options.warn ?? (() => {
|
|
271
|
+
});
|
|
272
|
+
const known = /* @__PURE__ */ new Map();
|
|
273
|
+
for (const issue of options.known ?? []) {
|
|
274
|
+
known.set(issue.id, issue);
|
|
275
|
+
}
|
|
276
|
+
const rootIds = uniqueIds(roots);
|
|
277
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
278
|
+
const queued = new Set(rootIds);
|
|
279
|
+
let truncated = false;
|
|
280
|
+
let frontier = rootIds.map((id) => ({ id, depth: 0 }));
|
|
281
|
+
while (frontier.length > 0) {
|
|
282
|
+
const next = [];
|
|
283
|
+
for (const { id, depth } of frontier) {
|
|
284
|
+
if (nodes.size >= maxNodes) {
|
|
285
|
+
truncated = true;
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
let relations = null;
|
|
289
|
+
try {
|
|
290
|
+
relations = await fetchRelations(id);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
warn(
|
|
293
|
+
`Could not read the relations of issue '${id}': ${err instanceof Error ? err.message : String(err)}`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
let issue = known.get(id) ?? null;
|
|
297
|
+
if (issue === null && options.fetchIssue !== void 0) {
|
|
298
|
+
try {
|
|
299
|
+
issue = await options.fetchIssue(id);
|
|
300
|
+
} catch (err) {
|
|
301
|
+
warn(`Could not read issue '${id}': ${err instanceof Error ? err.message : String(err)}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const resolved = relations ?? emptyRelations(id);
|
|
305
|
+
nodes.set(id, { id, relations: resolved, depth, issue, root: rootIds.includes(id) });
|
|
306
|
+
if (depth >= maxDepth) {
|
|
307
|
+
if (neighbours(resolved).some((neighbour) => !queued.has(neighbour))) {
|
|
308
|
+
truncated = true;
|
|
309
|
+
}
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
for (const neighbour of neighbours(resolved)) {
|
|
313
|
+
if (queued.has(neighbour)) continue;
|
|
314
|
+
queued.add(neighbour);
|
|
315
|
+
next.push({ id: neighbour, depth: depth + 1 });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (nodes.size >= maxNodes && next.length > 0) {
|
|
319
|
+
truncated = true;
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
frontier = next;
|
|
323
|
+
}
|
|
324
|
+
const graph = {
|
|
325
|
+
roots: rootIds,
|
|
326
|
+
nodes,
|
|
327
|
+
cycles: [],
|
|
328
|
+
truncated,
|
|
329
|
+
limits: { maxNodes, maxDepth }
|
|
330
|
+
};
|
|
331
|
+
linkReferencedBy(graph);
|
|
332
|
+
graph.cycles = findCycles(graph);
|
|
333
|
+
return graph;
|
|
334
|
+
}
|
|
335
|
+
function linkReferencedBy(graph) {
|
|
336
|
+
for (const node of graph.nodes.values()) {
|
|
337
|
+
for (const cited of node.relations.references) {
|
|
338
|
+
const target = graph.nodes.get(cited);
|
|
339
|
+
if (target === void 0 || target.id === node.id) continue;
|
|
340
|
+
target.relations.referencedBy = uniqueIds(
|
|
341
|
+
[...target.relations.referencedBy, node.id],
|
|
342
|
+
target.id
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/execution/order.ts
|
|
349
|
+
var PRIORITY_LABELS = {
|
|
350
|
+
high: "high",
|
|
351
|
+
medium: "medium",
|
|
352
|
+
low: "low"
|
|
353
|
+
};
|
|
354
|
+
var PRIORITY_RANK = { high: 0, medium: 1, low: 2 };
|
|
355
|
+
var NO_PRIORITY_RANK = 3;
|
|
356
|
+
function priorityOf(labels) {
|
|
357
|
+
for (const label of labels) {
|
|
358
|
+
const normalized = label.trim().toLowerCase();
|
|
359
|
+
const direct = PRIORITY_LABELS[normalized];
|
|
360
|
+
if (direct !== void 0) return direct;
|
|
361
|
+
const qualified = /^(?:priority|prio|p)\s*[:/-]\s*(high|medium|low)$/.exec(normalized);
|
|
362
|
+
const matched = qualified?.[1];
|
|
363
|
+
if (matched !== void 0) return PRIORITY_LABELS[matched];
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
function hierarchyDepth(id, parentOf, selected) {
|
|
368
|
+
let depth = 0;
|
|
369
|
+
let current = parentOf.get(id) ?? null;
|
|
370
|
+
const seen = /* @__PURE__ */ new Set([id]);
|
|
371
|
+
while (current !== null && selected.has(current) && !seen.has(current)) {
|
|
372
|
+
depth++;
|
|
373
|
+
seen.add(current);
|
|
374
|
+
current = parentOf.get(current) ?? null;
|
|
375
|
+
}
|
|
376
|
+
return depth;
|
|
377
|
+
}
|
|
378
|
+
function computeExecutionOrder(graph, options = {}) {
|
|
379
|
+
const selected = new Set(
|
|
380
|
+
(options.include ?? [...graph.nodes.keys()]).filter((id) => graph.nodes.has(id))
|
|
381
|
+
);
|
|
382
|
+
const parentOf = /* @__PURE__ */ new Map();
|
|
383
|
+
const priority = /* @__PURE__ */ new Map();
|
|
384
|
+
for (const id of selected) {
|
|
385
|
+
const node = graph.nodes.get(id);
|
|
386
|
+
parentOf.set(id, node?.relations.parent ?? null);
|
|
387
|
+
priority.set(id, priorityOf(node?.issue?.labels ?? []));
|
|
388
|
+
}
|
|
389
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
390
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
391
|
+
for (const id of selected) {
|
|
392
|
+
incoming.set(id, /* @__PURE__ */ new Set());
|
|
393
|
+
outgoing.set(id, []);
|
|
394
|
+
}
|
|
395
|
+
for (const { from, to } of dependencyEdges(graph)) {
|
|
396
|
+
if (!selected.has(from) || !selected.has(to) || from === to) continue;
|
|
397
|
+
incoming.get(to)?.add(from);
|
|
398
|
+
outgoing.get(from)?.push(to);
|
|
399
|
+
}
|
|
400
|
+
const depth = /* @__PURE__ */ new Map();
|
|
401
|
+
for (const id of selected) {
|
|
402
|
+
depth.set(id, hierarchyDepth(id, parentOf, selected));
|
|
403
|
+
}
|
|
404
|
+
const rank = (id) => {
|
|
405
|
+
const value = priority.get(id) ?? null;
|
|
406
|
+
return value === null ? NO_PRIORITY_RANK : PRIORITY_RANK[value];
|
|
407
|
+
};
|
|
408
|
+
const better = (a, b) => {
|
|
409
|
+
const byHierarchy = (depth.get(a) ?? 0) - (depth.get(b) ?? 0);
|
|
410
|
+
if (byHierarchy !== 0) return byHierarchy;
|
|
411
|
+
const byPriority = rank(a) - rank(b);
|
|
412
|
+
if (byPriority !== 0) return byPriority;
|
|
413
|
+
return compareIds(a, b);
|
|
414
|
+
};
|
|
415
|
+
const ready = [...selected].filter((id) => (incoming.get(id)?.size ?? 0) === 0);
|
|
416
|
+
const order = [];
|
|
417
|
+
while (ready.length > 0) {
|
|
418
|
+
ready.sort(better);
|
|
419
|
+
const next = ready.shift();
|
|
420
|
+
order.push(next);
|
|
421
|
+
for (const dependent of outgoing.get(next) ?? []) {
|
|
422
|
+
const pending = incoming.get(dependent);
|
|
423
|
+
if (pending === void 0) continue;
|
|
424
|
+
pending.delete(next);
|
|
425
|
+
if (pending.size === 0) {
|
|
426
|
+
ready.push(dependent);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (order.length !== selected.size) {
|
|
431
|
+
const cycles = graph.cycles.length > 0 ? graph.cycles : findCycles(graph);
|
|
432
|
+
const relevant = cycles.filter((cycle) => cycle.every((id) => selected.has(id)));
|
|
433
|
+
return { ok: false, cycles: relevant.length > 0 ? relevant : cycles };
|
|
434
|
+
}
|
|
435
|
+
return { ok: true, order };
|
|
436
|
+
}
|
|
437
|
+
function describeCycles(cycles) {
|
|
438
|
+
return cycles.map((cycle) => [...cycle, cycle[0]].map((id) => `#${id}`).join(" \u2192 ")).join("; ");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/execution/plan.ts
|
|
442
|
+
function describeIssue(issue, id) {
|
|
443
|
+
const parsed = Number.parseInt(id, 10);
|
|
444
|
+
return {
|
|
445
|
+
number: issue?.number ?? (Number.isNaN(parsed) ? null : parsed),
|
|
446
|
+
title: issue?.title ?? "",
|
|
447
|
+
url: issue?.remoteRef ?? null,
|
|
448
|
+
source: issue?.source ?? "github"
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function buildExecutionPlan(input) {
|
|
452
|
+
const now = input.now ?? isoNow;
|
|
453
|
+
const at = now();
|
|
454
|
+
const scheduled = new Set(input.order);
|
|
455
|
+
const requested = new Set(input.requested);
|
|
456
|
+
const issues = input.order.map((id, index) => {
|
|
457
|
+
const node = input.graph.nodes.get(id);
|
|
458
|
+
const relations = node?.relations;
|
|
459
|
+
const described = describeIssue(node?.issue ?? null, id);
|
|
460
|
+
return {
|
|
461
|
+
id,
|
|
462
|
+
number: described.number,
|
|
463
|
+
title: described.title,
|
|
464
|
+
url: described.url,
|
|
465
|
+
source: described.source,
|
|
466
|
+
position: index + 1,
|
|
467
|
+
status: "pending",
|
|
468
|
+
origin: requested.has(id) ? "requested" : "discovered",
|
|
469
|
+
// Only the constraints that survive inside the queue: a blocker left out
|
|
470
|
+
// of it is reported in `excluded`, not enforced as a dependency.
|
|
471
|
+
dependsOn: (relations?.blockedBy ?? []).filter((blocker) => scheduled.has(blocker)),
|
|
472
|
+
parent: relations?.parent ?? null,
|
|
473
|
+
priority: priorityOf(node?.issue?.labels ?? []),
|
|
474
|
+
heuristic: (relations?.heuristic.length ?? 0) > 0,
|
|
475
|
+
failedPhase: null,
|
|
476
|
+
lastError: null,
|
|
477
|
+
startedAt: null,
|
|
478
|
+
completedAt: null
|
|
479
|
+
};
|
|
480
|
+
});
|
|
481
|
+
const excluded = [];
|
|
482
|
+
for (const node of input.graph.nodes.values()) {
|
|
483
|
+
if (scheduled.has(node.id)) continue;
|
|
484
|
+
const described = describeIssue(node.issue, node.id);
|
|
485
|
+
excluded.push({
|
|
486
|
+
id: node.id,
|
|
487
|
+
number: described.number,
|
|
488
|
+
title: described.title,
|
|
489
|
+
url: described.url,
|
|
490
|
+
reason: "Discovered in the hierarchy but not selected for this run"
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
schemaVersion: 1,
|
|
495
|
+
id: input.requested[0] ?? input.order[0],
|
|
496
|
+
project: input.projectId,
|
|
497
|
+
requested: [...input.requested],
|
|
498
|
+
branchName: null,
|
|
499
|
+
noBranch: input.noBranch ?? false,
|
|
500
|
+
prReview: input.prReview ?? false,
|
|
501
|
+
status: "pending",
|
|
502
|
+
createdAt: at,
|
|
503
|
+
updatedAt: at,
|
|
504
|
+
truncated: input.graph.truncated,
|
|
505
|
+
issues,
|
|
506
|
+
excluded
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
async function loadExecutionPlan(path) {
|
|
510
|
+
const raw = JSON.parse(await readFile(path, "utf-8"));
|
|
511
|
+
try {
|
|
512
|
+
return executionPlanSchema.parse(raw);
|
|
513
|
+
} catch (err) {
|
|
514
|
+
if (err instanceof ZodError) {
|
|
515
|
+
const issues = err.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
516
|
+
throw new Error(`Invalid execution-plan.json at ${path}:
|
|
517
|
+
${issues}`);
|
|
518
|
+
}
|
|
519
|
+
throw err;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
async function saveExecutionPlan(path, plan, now = isoNow) {
|
|
523
|
+
await mkdir(dirname(path), { recursive: true });
|
|
524
|
+
const stamped = { ...plan, updatedAt: now() };
|
|
525
|
+
await writeFileAtomic(path, `${JSON.stringify(stamped, null, 2)}
|
|
526
|
+
`);
|
|
527
|
+
}
|
|
528
|
+
function nextQueueIssue(plan) {
|
|
529
|
+
return plan.issues.find((entry) => entry.status === "in_progress") ?? plan.issues.find((entry) => entry.status === "failed") ?? plan.issues.find((entry) => entry.status === "pending") ?? null;
|
|
530
|
+
}
|
|
531
|
+
function queueStatus(plan) {
|
|
532
|
+
if (plan.issues.some((entry) => entry.status === "failed")) return "failed";
|
|
533
|
+
if (plan.issues.every((entry) => entry.status === "completed")) return "completed";
|
|
534
|
+
if (plan.issues.some((entry) => entry.status !== "pending")) return "in_progress";
|
|
535
|
+
return "pending";
|
|
536
|
+
}
|
|
537
|
+
function isQueueComplete(plan) {
|
|
538
|
+
return plan.issues.every((entry) => entry.status === "completed");
|
|
539
|
+
}
|
|
540
|
+
function updateIssue(plan, id, patch) {
|
|
541
|
+
const issues = plan.issues.map((entry) => entry.id === id ? { ...entry, ...patch } : entry);
|
|
542
|
+
const next = { ...plan, issues };
|
|
543
|
+
return { ...next, status: queueStatus(next) };
|
|
544
|
+
}
|
|
545
|
+
function markQueueIssueInProgress(plan, id, now = isoNow) {
|
|
546
|
+
const current = plan.issues.find((entry) => entry.id === id);
|
|
547
|
+
return updateIssue(plan, id, {
|
|
548
|
+
status: "in_progress",
|
|
549
|
+
// Preserved across a resume: the queue reports when the work on this Issue
|
|
550
|
+
// really began, not when it was last picked up.
|
|
551
|
+
startedAt: current?.startedAt ?? now(),
|
|
552
|
+
failedPhase: null,
|
|
553
|
+
lastError: null
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
function markQueueIssueCompleted(plan, id, now = isoNow) {
|
|
557
|
+
return updateIssue(plan, id, {
|
|
558
|
+
status: "completed",
|
|
559
|
+
completedAt: now(),
|
|
560
|
+
failedPhase: null,
|
|
561
|
+
lastError: null
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
function markQueueIssueFailed(plan, id, failure2) {
|
|
565
|
+
return updateIssue(plan, id, {
|
|
566
|
+
status: "failed",
|
|
567
|
+
failedPhase: failure2.phase,
|
|
568
|
+
lastError: failure2.error
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
function setQueueBranch(plan, branchName) {
|
|
572
|
+
return { ...plan, branchName };
|
|
573
|
+
}
|
|
574
|
+
function setQueuePullRequest(plan, pullRequest) {
|
|
575
|
+
return { ...plan, pullRequest };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/execution/confirm.ts
|
|
579
|
+
import { createInterface } from "readline";
|
|
580
|
+
var MAX_PROMPT_ATTEMPTS = 3;
|
|
581
|
+
var QueueConfirmationError = class extends Error {
|
|
582
|
+
exitCode;
|
|
583
|
+
constructor(message, exitCode = 1) {
|
|
584
|
+
super(message);
|
|
585
|
+
this.name = "QueueConfirmationError";
|
|
586
|
+
this.exitCode = exitCode;
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
function isInteractiveByDefault() {
|
|
590
|
+
const ci = process.env.CI;
|
|
591
|
+
const inCi = ci !== void 0 && ci !== "" && ci !== "0" && ci.toLowerCase() !== "false";
|
|
592
|
+
return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) && !inCi;
|
|
593
|
+
}
|
|
594
|
+
function issueLabel(entry) {
|
|
595
|
+
return entry.number === null ? entry.id : `#${entry.number}`;
|
|
596
|
+
}
|
|
597
|
+
function reasonFor(entry) {
|
|
598
|
+
const parts = [];
|
|
599
|
+
if (entry.origin === "requested") {
|
|
600
|
+
parts.push("requested");
|
|
601
|
+
}
|
|
602
|
+
if (entry.dependsOn.length > 0) {
|
|
603
|
+
parts.push(`after ${entry.dependsOn.map((id) => `#${id}`).join(", ")}`);
|
|
604
|
+
}
|
|
605
|
+
if (entry.parent !== null) {
|
|
606
|
+
parts.push(`sub-issue of #${entry.parent}`);
|
|
607
|
+
}
|
|
608
|
+
if (entry.priority !== null) {
|
|
609
|
+
parts.push(entry.priority);
|
|
610
|
+
}
|
|
611
|
+
return parts.join(", ");
|
|
612
|
+
}
|
|
613
|
+
function buildQueueSummaryLines(plan) {
|
|
614
|
+
const primary = plan.issues.find((entry) => entry.id === plan.id) ?? plan.issues[0];
|
|
615
|
+
const lines = [];
|
|
616
|
+
if (primary !== void 0) {
|
|
617
|
+
lines.push(` Main issue: ${issueLabel(primary)}${primary.title ? ` ${primary.title}` : ""}`);
|
|
618
|
+
}
|
|
619
|
+
lines.push(` Total issues: ${plan.issues.length}`);
|
|
620
|
+
lines.push(" Suggested order:");
|
|
621
|
+
for (const entry of plan.issues) {
|
|
622
|
+
const reason = reasonFor(entry);
|
|
623
|
+
const flag = entry.heuristic ? " ~" : "";
|
|
624
|
+
const title = entry.title === "" ? "" : ` ${entry.title}`;
|
|
625
|
+
lines.push(
|
|
626
|
+
` ${entry.position}. ${issueLabel(entry)}${title}${reason === "" ? "" : ` (${reason})`}${flag}`
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
if (plan.issues.some((entry) => entry.heuristic)) {
|
|
630
|
+
lines.push(" ~ relation found only in the issue text, which may be a false positive");
|
|
631
|
+
}
|
|
632
|
+
if (plan.truncated) {
|
|
633
|
+
lines.push(" Note: discovery hit its limit \u2014 the hierarchy may be larger than shown");
|
|
634
|
+
}
|
|
635
|
+
return lines;
|
|
636
|
+
}
|
|
637
|
+
async function confirmQueue(plan, requestedOnlyCount, options = {}) {
|
|
638
|
+
const info = options.info ?? printInfo;
|
|
639
|
+
const warn = options.warn ?? printWarning;
|
|
640
|
+
info(`Issue ${plan.requested.map((id) => `#${id}`).join(", ")} is part of a larger structure:`);
|
|
641
|
+
for (const line of buildQueueSummaryLines(plan)) {
|
|
642
|
+
(options.stdout ?? process.stdout).write(`${line}
|
|
643
|
+
`);
|
|
644
|
+
}
|
|
645
|
+
if (options.only === true) {
|
|
646
|
+
info(`--only: running just the ${requestedOnlyCount} issue(s) you asked for.`);
|
|
647
|
+
return "requested";
|
|
648
|
+
}
|
|
649
|
+
if (options.yes === true) {
|
|
650
|
+
info(`--yes: running the whole hierarchy (${plan.issues.length} issues).`);
|
|
651
|
+
return "all";
|
|
652
|
+
}
|
|
653
|
+
const interactive = options.interactive ?? isInteractiveByDefault();
|
|
654
|
+
if (!interactive && options.singleRequest === true) {
|
|
655
|
+
warn(
|
|
656
|
+
`A larger structure was found, but the terminal is not interactive: running just issue ${plan.requested.map((id) => `#${id}`).join(", ")}. Re-run with --yes to execute the whole hierarchy.`
|
|
657
|
+
);
|
|
658
|
+
return "requested";
|
|
659
|
+
}
|
|
660
|
+
if (!interactive) {
|
|
661
|
+
throw new QueueConfirmationError(
|
|
662
|
+
"This run involves more than one issue and the terminal is not interactive. Re-run with --yes to execute the whole hierarchy, or --only to execute just the issues you informed."
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
const query = `Which scope should run? [1] Only the issues informed (${requestedOnlyCount}) [2] The whole hierarchy (${plan.issues.length}) [3] Cancel: `;
|
|
666
|
+
return await ask(query, options.stdin ?? process.stdin, options.stdout ?? process.stdout, warn);
|
|
667
|
+
}
|
|
668
|
+
async function ask(query, stdin, stdout, warn) {
|
|
669
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
670
|
+
const queued = [];
|
|
671
|
+
let pending = null;
|
|
672
|
+
let closed = false;
|
|
673
|
+
rl.on("line", (line) => {
|
|
674
|
+
if (pending !== null) {
|
|
675
|
+
const resolve = pending;
|
|
676
|
+
pending = null;
|
|
677
|
+
resolve(line);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
queued.push(line);
|
|
681
|
+
});
|
|
682
|
+
rl.on("close", () => {
|
|
683
|
+
closed = true;
|
|
684
|
+
if (pending !== null) {
|
|
685
|
+
const resolve = pending;
|
|
686
|
+
pending = null;
|
|
687
|
+
resolve(null);
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
const read = () => new Promise((resolve) => {
|
|
691
|
+
const buffered = queued.shift();
|
|
692
|
+
if (buffered !== void 0) {
|
|
693
|
+
resolve(buffered);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (closed) {
|
|
697
|
+
resolve(null);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
stdout.write(query);
|
|
701
|
+
pending = resolve;
|
|
702
|
+
});
|
|
703
|
+
try {
|
|
704
|
+
for (let attempt = 0; attempt < MAX_PROMPT_ATTEMPTS; attempt++) {
|
|
705
|
+
const answer = await read();
|
|
706
|
+
if (answer === null) {
|
|
707
|
+
return "cancel";
|
|
708
|
+
}
|
|
709
|
+
const choice = answer.trim();
|
|
710
|
+
if (choice === "1") return "requested";
|
|
711
|
+
if (choice === "" || choice === "2") return "all";
|
|
712
|
+
if (choice === "3") return "cancel";
|
|
713
|
+
warn(`Invalid choice: '${choice}'. Enter 1, 2 or 3.`);
|
|
714
|
+
}
|
|
715
|
+
return "cancel";
|
|
716
|
+
} finally {
|
|
717
|
+
rl.close();
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/execution/discovery.ts
|
|
722
|
+
function supportsRelations(source) {
|
|
723
|
+
try {
|
|
724
|
+
return typeof getProvider(source).fetchRelations === "function";
|
|
725
|
+
} catch {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
async function discoverIssueGraph(roots, options = {}) {
|
|
730
|
+
const source = options.source ?? [...options.known ?? []][0]?.source ?? "github";
|
|
731
|
+
let fetchRelations = null;
|
|
732
|
+
let fetchIssue = null;
|
|
733
|
+
try {
|
|
734
|
+
const provider = getProvider(source);
|
|
735
|
+
const relations = provider.fetchRelations?.bind(provider);
|
|
736
|
+
if (relations !== void 0) {
|
|
737
|
+
fetchRelations = (id) => relations(id);
|
|
738
|
+
fetchIssue = (id) => provider.get(id);
|
|
739
|
+
}
|
|
740
|
+
} catch {
|
|
741
|
+
}
|
|
742
|
+
if (fetchRelations === null) {
|
|
743
|
+
return buildDependencyGraph(roots, async () => null, { ...options, fetchIssue: void 0 });
|
|
744
|
+
}
|
|
745
|
+
return buildDependencyGraph(roots, fetchRelations, {
|
|
746
|
+
...options,
|
|
747
|
+
fetchIssue: options.fetchIssue ?? fetchIssue ?? void 0
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/execution/queue.ts
|
|
752
|
+
async function loadResumableQueue(planFile, requested) {
|
|
753
|
+
let plan;
|
|
754
|
+
try {
|
|
755
|
+
plan = await loadExecutionPlan(planFile);
|
|
756
|
+
} catch {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
const sameRequest = plan.requested.length === requested.length && plan.requested.every((id, index) => id === requested[index]);
|
|
760
|
+
return sameRequest ? plan : null;
|
|
761
|
+
}
|
|
762
|
+
async function planQueue(input) {
|
|
763
|
+
const info = input.info ?? printInfo;
|
|
764
|
+
const warn = input.warn ?? printWarning;
|
|
765
|
+
const single = input.requested.length === 1;
|
|
766
|
+
const resumable = await loadResumableQueue(input.planFile, input.requested);
|
|
767
|
+
if (resumable !== null && isQueueComplete(resumable)) {
|
|
768
|
+
const pr = resumable.pullRequest?.url;
|
|
769
|
+
info(
|
|
770
|
+
`The execution queue of issue #${resumable.id} is already complete (${resumable.issues.length} issues)${pr === void 0 ? "" : ` \u2014 ${pr}`}. Nothing to run.`
|
|
771
|
+
);
|
|
772
|
+
return { kind: "stop", code: 0 };
|
|
773
|
+
}
|
|
774
|
+
if (resumable !== null) {
|
|
775
|
+
const done = resumable.issues.filter((entry) => entry.status === "completed").length;
|
|
776
|
+
info(
|
|
777
|
+
`Resuming the execution queue of issue #${resumable.id}: ${done}/${resumable.issues.length} issues already completed.`
|
|
778
|
+
);
|
|
779
|
+
return { kind: "queue", plan: resumable, resumed: true };
|
|
780
|
+
}
|
|
781
|
+
if (single && input.confirm?.only === true) {
|
|
782
|
+
return { kind: "single" };
|
|
783
|
+
}
|
|
784
|
+
if (single && !supportsRelations(input.source)) {
|
|
785
|
+
return { kind: "single" };
|
|
786
|
+
}
|
|
787
|
+
const graph = await discoverIssueGraph(input.requested, {
|
|
788
|
+
source: input.source,
|
|
789
|
+
known: input.known,
|
|
790
|
+
warn
|
|
791
|
+
});
|
|
792
|
+
if (single && graph.nodes.size <= 1) {
|
|
793
|
+
return { kind: "single" };
|
|
794
|
+
}
|
|
795
|
+
const suggested = computeExecutionOrder(graph);
|
|
796
|
+
if (!suggested.ok) {
|
|
797
|
+
if (single) {
|
|
798
|
+
warn(
|
|
799
|
+
`Dependency cycle between issues: ${describeCycles(suggested.cycles)}. Running just the issue you informed; fix the dependencies on GitHub to run the hierarchy.`
|
|
800
|
+
);
|
|
801
|
+
return { kind: "single" };
|
|
802
|
+
}
|
|
803
|
+
warn(
|
|
804
|
+
`Dependency cycle between issues: ${describeCycles(suggested.cycles)}. Fix the dependencies on GitHub, or re-run with --only to execute just the issues you informed.`
|
|
805
|
+
);
|
|
806
|
+
return { kind: "stop", code: 1 };
|
|
807
|
+
}
|
|
808
|
+
const requestedOrder = computeExecutionOrder(graph, { include: input.requested });
|
|
809
|
+
const requestedCount = requestedOrder.ok ? requestedOrder.order.length : input.requested.length;
|
|
810
|
+
const suggestedPlan = buildExecutionPlan({
|
|
811
|
+
projectId: input.projectId,
|
|
812
|
+
requested: input.requested,
|
|
813
|
+
graph,
|
|
814
|
+
order: suggested.order,
|
|
815
|
+
noBranch: input.noBranch,
|
|
816
|
+
prReview: input.prReview
|
|
817
|
+
});
|
|
818
|
+
let choice;
|
|
819
|
+
try {
|
|
820
|
+
choice = await confirmQueue(suggestedPlan, requestedCount, {
|
|
821
|
+
...input.confirm ?? {},
|
|
822
|
+
singleRequest: single
|
|
823
|
+
});
|
|
824
|
+
} catch (err) {
|
|
825
|
+
if (err instanceof QueueConfirmationError) {
|
|
826
|
+
warn(err.message);
|
|
827
|
+
return { kind: "stop", code: err.exitCode };
|
|
828
|
+
}
|
|
829
|
+
throw err;
|
|
830
|
+
}
|
|
831
|
+
if (choice === "cancel") {
|
|
832
|
+
info("Cancelled: nothing was executed.");
|
|
833
|
+
return { kind: "stop", code: 1 };
|
|
834
|
+
}
|
|
835
|
+
let plan = suggestedPlan;
|
|
836
|
+
if (choice === "requested") {
|
|
837
|
+
if (!requestedOrder.ok) {
|
|
838
|
+
warn(
|
|
839
|
+
`Dependency cycle between the issues you informed: ${describeCycles(requestedOrder.cycles)}.`
|
|
840
|
+
);
|
|
841
|
+
return { kind: "stop", code: 1 };
|
|
842
|
+
}
|
|
843
|
+
if (requestedOrder.order.length <= 1) {
|
|
844
|
+
if (input.requested.length > 1) {
|
|
845
|
+
const missing = input.requested.filter((id) => !requestedOrder.order.includes(id));
|
|
846
|
+
warn(
|
|
847
|
+
`Only issue #${requestedOrder.order[0] ?? "\u2014"} of the ones you informed could be read; ${missing.map((id) => `#${id}`).join(", ")} did not make it into the dependency graph. Nothing was executed \u2014 check the identifiers and try again.`
|
|
848
|
+
);
|
|
849
|
+
return { kind: "stop", code: 1 };
|
|
850
|
+
}
|
|
851
|
+
return { kind: "single" };
|
|
852
|
+
}
|
|
853
|
+
plan = buildExecutionPlan({
|
|
854
|
+
projectId: input.projectId,
|
|
855
|
+
requested: input.requested,
|
|
856
|
+
graph,
|
|
857
|
+
order: requestedOrder.order,
|
|
858
|
+
noBranch: input.noBranch,
|
|
859
|
+
prReview: input.prReview
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
await saveExecutionPlan(input.planFile, plan);
|
|
863
|
+
return { kind: "queue", plan, resumed: false };
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/issues/args.ts
|
|
867
|
+
var IssueArgumentError = class extends Error {
|
|
868
|
+
constructor(message) {
|
|
869
|
+
super(message);
|
|
870
|
+
this.name = "IssueArgumentError";
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
var INVALID_ID = /[/\\\s]/;
|
|
874
|
+
function parseIssueArguments(values) {
|
|
875
|
+
const ids = [];
|
|
876
|
+
const seen = /* @__PURE__ */ new Set();
|
|
877
|
+
for (const value of values) {
|
|
878
|
+
for (const raw of value.split(",")) {
|
|
879
|
+
const id = raw.trim().replace(/^#/, "").trim();
|
|
880
|
+
if (id === "") {
|
|
881
|
+
if (raw.trim() !== "" || value.includes(",")) {
|
|
882
|
+
throw new IssueArgumentError(
|
|
883
|
+
`Invalid issue list '${value}': it has an empty entry. Use '42,43' or '42 43'.`
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
if (INVALID_ID.test(id) || id === "." || id === "..") {
|
|
889
|
+
throw new IssueArgumentError(`Invalid issue identifier '${raw.trim()}'.`);
|
|
890
|
+
}
|
|
891
|
+
if (seen.has(id)) continue;
|
|
892
|
+
seen.add(id);
|
|
893
|
+
ids.push(id);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if (ids.length === 0) {
|
|
897
|
+
throw new IssueArgumentError("No issue was informed. Pass at least one issue number.");
|
|
898
|
+
}
|
|
899
|
+
return ids;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// src/ui/pipeline-renderer.ts
|
|
903
|
+
import { Listr, PRESET_TIMER, PRESET_TIMESTAMP } from "listr2";
|
|
904
|
+
var PHASE_LABELS = {
|
|
905
|
+
prd: "PRD",
|
|
906
|
+
plan: "Plan",
|
|
907
|
+
execute: "Execute",
|
|
908
|
+
review: "Review",
|
|
909
|
+
pr: "PR",
|
|
910
|
+
"pr-review": "PR Review"
|
|
911
|
+
};
|
|
912
|
+
function phaseLabel(phase) {
|
|
913
|
+
return PHASE_LABELS[phase] ?? phase.charAt(0).toUpperCase() + phase.slice(1);
|
|
914
|
+
}
|
|
915
|
+
function selectRenderer(verbose) {
|
|
916
|
+
const isTTY = !!process.stdout.isTTY;
|
|
917
|
+
const isCI = !!process.env.CI;
|
|
918
|
+
if (!isTTY || isCI) {
|
|
919
|
+
return "simple";
|
|
920
|
+
}
|
|
921
|
+
return verbose ? "verbose" : "default";
|
|
922
|
+
}
|
|
923
|
+
function storySubtaskTitle(story, executing) {
|
|
924
|
+
const base = `${story.id}: ${story.title}`;
|
|
925
|
+
return executing ? `${base} \u2192 Executando...` : base;
|
|
926
|
+
}
|
|
927
|
+
function createActiveStoryTracker(stories, storyTasks) {
|
|
928
|
+
let activeStoryId;
|
|
929
|
+
const retitle = (storyId, executing) => {
|
|
930
|
+
const task = storyTasks.get(storyId);
|
|
931
|
+
if (!task) return;
|
|
932
|
+
task.title = storySubtaskTitle(
|
|
933
|
+
stories.find((s) => s.id === storyId) ?? { id: storyId, title: "" },
|
|
934
|
+
executing
|
|
935
|
+
);
|
|
936
|
+
};
|
|
937
|
+
return {
|
|
938
|
+
setActive(storyId) {
|
|
939
|
+
if (activeStoryId !== void 0 && activeStoryId !== storyId) {
|
|
940
|
+
retitle(activeStoryId, false);
|
|
941
|
+
}
|
|
942
|
+
activeStoryId = storyId;
|
|
943
|
+
if (storyId !== void 0) retitle(storyId, true);
|
|
944
|
+
},
|
|
945
|
+
clear() {
|
|
946
|
+
if (activeStoryId === void 0) return;
|
|
947
|
+
retitle(activeStoryId, false);
|
|
948
|
+
activeStoryId = void 0;
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
function buildExecutePhaseTask(runner, tasksPath, verbose) {
|
|
953
|
+
return async (_ctx, task) => {
|
|
954
|
+
let stories;
|
|
955
|
+
try {
|
|
956
|
+
const plan = await loadTaskPlan(tasksPath);
|
|
957
|
+
stories = [...plan.userStories].sort((a, b) => a.priority - b.priority);
|
|
958
|
+
} catch {
|
|
959
|
+
setOutputCallback((line) => {
|
|
960
|
+
task.output = line;
|
|
961
|
+
});
|
|
962
|
+
try {
|
|
963
|
+
await runner();
|
|
964
|
+
} finally {
|
|
965
|
+
setOutputCallback(void 0);
|
|
966
|
+
}
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
const totalStories = stories.length;
|
|
970
|
+
const initialPassed = stories.filter((s) => s.passes).length;
|
|
971
|
+
task.title = `Execute (${initialPassed}/${totalStories} stories passing)`;
|
|
972
|
+
const resolvers = /* @__PURE__ */ new Map();
|
|
973
|
+
setStoryUpdateCallback((updatedStories) => {
|
|
974
|
+
const passed = updatedStories.filter((s) => s.passes).length;
|
|
975
|
+
task.title = `Execute (${passed}/${totalStories} stories passing)`;
|
|
976
|
+
for (const s of updatedStories) {
|
|
977
|
+
if (s.passes && resolvers.has(s.id)) {
|
|
978
|
+
resolvers.get(s.id).resolve();
|
|
979
|
+
resolvers.delete(s.id);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
const storyTasks = /* @__PURE__ */ new Map();
|
|
984
|
+
const tracker = createActiveStoryTracker(stories, storyTasks);
|
|
985
|
+
setStoryStageCallback((storyId) => {
|
|
986
|
+
tracker.setActive(storyId);
|
|
987
|
+
});
|
|
988
|
+
const subtaskDefs = stories.map((story) => ({
|
|
989
|
+
title: storySubtaskTitle(story, false),
|
|
990
|
+
skip: story.passes ? "already passing" : false,
|
|
991
|
+
task: async (_c, subTask) => {
|
|
992
|
+
storyTasks.set(story.id, subTask);
|
|
993
|
+
await new Promise((resolve, reject) => {
|
|
994
|
+
resolvers.set(story.id, { resolve, reject });
|
|
995
|
+
});
|
|
996
|
+
},
|
|
997
|
+
rendererOptions: {
|
|
998
|
+
timer: PRESET_TIMER
|
|
999
|
+
}
|
|
1000
|
+
}));
|
|
1001
|
+
const engineSubtask = {
|
|
1002
|
+
title: "Running engine",
|
|
1003
|
+
task: async (_c, engineTask) => {
|
|
1004
|
+
setOutputCallback((line) => {
|
|
1005
|
+
engineTask.output = line;
|
|
1006
|
+
});
|
|
1007
|
+
try {
|
|
1008
|
+
await runner();
|
|
1009
|
+
} finally {
|
|
1010
|
+
setOutputCallback(void 0);
|
|
1011
|
+
setStoryUpdateCallback(void 0);
|
|
1012
|
+
setStoryStageCallback(void 0);
|
|
1013
|
+
tracker.clear();
|
|
1014
|
+
for (const [, { resolve }] of resolvers) {
|
|
1015
|
+
resolve();
|
|
1016
|
+
}
|
|
1017
|
+
resolvers.clear();
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
1020
|
+
rendererOptions: {
|
|
1021
|
+
timer: PRESET_TIMER,
|
|
1022
|
+
outputBar: verbose ? Infinity : false,
|
|
1023
|
+
persistentOutput: false,
|
|
1024
|
+
bottomBar: verbose ? Infinity : 3
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
return task.newListr([engineSubtask, ...subtaskDefs], {
|
|
1028
|
+
concurrent: true,
|
|
1029
|
+
exitOnError: true,
|
|
1030
|
+
rendererOptions: {
|
|
1031
|
+
timer: PRESET_TIMER,
|
|
1032
|
+
collapseSkips: false
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
async function runPipelineWithRenderer(options) {
|
|
1038
|
+
const { phases, startIndex, verbose, runners, tasksPath } = options;
|
|
1039
|
+
const overallStart = Date.now();
|
|
1040
|
+
let currentPhase;
|
|
1041
|
+
const renderer = selectRenderer(verbose);
|
|
1042
|
+
const isSimple = renderer === "simple";
|
|
1043
|
+
const tasks = new Listr(
|
|
1044
|
+
phases.map((phase, index) => {
|
|
1045
|
+
const label = phaseLabel(phase);
|
|
1046
|
+
const isExecutePhase = phase === "execute" && tasksPath;
|
|
1047
|
+
return {
|
|
1048
|
+
title: label,
|
|
1049
|
+
skip: index < startIndex ? "skipped" : false,
|
|
1050
|
+
task: isExecutePhase ? async (_ctx, task) => {
|
|
1051
|
+
currentPhase = phase;
|
|
1052
|
+
const runner = runners[phase];
|
|
1053
|
+
if (!runner) {
|
|
1054
|
+
throw new Error(`No runner defined for phase: ${phase}`);
|
|
1055
|
+
}
|
|
1056
|
+
return buildExecutePhaseTask(runner, tasksPath, verbose)(_ctx, task);
|
|
1057
|
+
} : async (_ctx, task) => {
|
|
1058
|
+
currentPhase = phase;
|
|
1059
|
+
const runner = runners[phase];
|
|
1060
|
+
if (!runner) {
|
|
1061
|
+
throw new Error(`No runner defined for phase: ${phase}`);
|
|
1062
|
+
}
|
|
1063
|
+
setOutputCallback((line) => {
|
|
1064
|
+
task.output = line;
|
|
1065
|
+
});
|
|
1066
|
+
try {
|
|
1067
|
+
await runner();
|
|
1068
|
+
} finally {
|
|
1069
|
+
setOutputCallback(void 0);
|
|
1070
|
+
}
|
|
1071
|
+
},
|
|
1072
|
+
rendererOptions: {
|
|
1073
|
+
timer: PRESET_TIMER,
|
|
1074
|
+
outputBar: verbose ? Infinity : false,
|
|
1075
|
+
persistentOutput: false
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
}),
|
|
1079
|
+
{
|
|
1080
|
+
renderer,
|
|
1081
|
+
rendererOptions: {
|
|
1082
|
+
timer: PRESET_TIMER,
|
|
1083
|
+
collapseSkips: false,
|
|
1084
|
+
...isSimple ? { timestamp: PRESET_TIMESTAMP } : {}
|
|
1085
|
+
},
|
|
1086
|
+
exitOnError: true
|
|
1087
|
+
}
|
|
1088
|
+
);
|
|
1089
|
+
let failedPhase;
|
|
1090
|
+
try {
|
|
1091
|
+
await tasks.run();
|
|
1092
|
+
} catch {
|
|
1093
|
+
failedPhase = currentPhase ?? phases[startIndex];
|
|
1094
|
+
} finally {
|
|
1095
|
+
setOutputCallback(void 0);
|
|
1096
|
+
setStoryUpdateCallback(void 0);
|
|
1097
|
+
setStoryStageCallback(void 0);
|
|
1098
|
+
}
|
|
1099
|
+
const overallElapsedSeconds = Math.floor((Date.now() - overallStart) / 1e3);
|
|
1100
|
+
return {
|
|
1101
|
+
success: !failedPhase,
|
|
1102
|
+
failedPhase,
|
|
1103
|
+
overallElapsedSeconds
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// src/commands/run.ts
|
|
1108
|
+
function toIssueNumber(id) {
|
|
1109
|
+
const parsed = Number.parseInt(id, 10);
|
|
1110
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
1111
|
+
}
|
|
1112
|
+
function publishStorySeed(publisher, stories, at) {
|
|
1113
|
+
if (stories.length === 0) return false;
|
|
1114
|
+
publisher.publish({ type: "stories:update", at, stories: [...stories] });
|
|
1115
|
+
return true;
|
|
1116
|
+
}
|
|
1117
|
+
function publishIssueDetails(publisher, issue, at) {
|
|
1118
|
+
publisher.publish({
|
|
1119
|
+
type: "issue:update",
|
|
1120
|
+
at,
|
|
1121
|
+
number: issue.number,
|
|
1122
|
+
// Left undefined (rather than null) when the origin has no remote, so the
|
|
1123
|
+
// reducer keeps whatever URL session:start already published.
|
|
1124
|
+
url: issue.remoteRef ?? void 0,
|
|
1125
|
+
title: issue.title,
|
|
1126
|
+
description: issue.body,
|
|
1127
|
+
labels: issue.labels,
|
|
1128
|
+
state: issue.state
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
var RUNNABLE_PHASES = ["prd", "plan", "execute", "review", "pr"];
|
|
1132
|
+
var RUNNABLE_PHASES_NO_BRANCH = ["prd", "plan", "execute", "review"];
|
|
1133
|
+
var RUNNABLE_PHASES_WITH_PR_REVIEW = [...RUNNABLE_PHASES, "pr-review"];
|
|
1134
|
+
var QUEUE_PR_PHASES = ["init", "pr"];
|
|
1135
|
+
var QUEUE_PR_PHASES_WITH_REVIEW = [
|
|
1136
|
+
"init",
|
|
1137
|
+
"pr",
|
|
1138
|
+
"pr-review"
|
|
1139
|
+
];
|
|
1140
|
+
var RUNNABLE_QUEUE_PR_PHASES = ["pr"];
|
|
1141
|
+
var RUNNABLE_QUEUE_PR_PHASES_WITH_REVIEW = ["pr", "pr-review"];
|
|
1142
|
+
async function readPrReviewOutcome(issue, tasksPath, requestedChanges) {
|
|
1143
|
+
const outcome = { requestedChanges, recommendation: null, reportPath: null };
|
|
1144
|
+
try {
|
|
1145
|
+
const plan = await loadTaskPlan(tasksPath);
|
|
1146
|
+
outcome.recommendation = plan.prReview?.lastRecommendation ?? null;
|
|
1147
|
+
const pullRequest = plan.prReview?.pullRequestNumber;
|
|
1148
|
+
if (pullRequest !== void 0) {
|
|
1149
|
+
const dir = await prReviewDir({ issue, pullRequest });
|
|
1150
|
+
const index = await readPrReviewIndex(dir);
|
|
1151
|
+
const last = index?.rounds.reduce(
|
|
1152
|
+
(latest, entry) => latest === null || entry.round > latest.round ? entry : latest,
|
|
1153
|
+
null
|
|
1154
|
+
);
|
|
1155
|
+
if (last) {
|
|
1156
|
+
outcome.reportPath = join(dir, last.reportPath);
|
|
1157
|
+
outcome.recommendation ??= last.recommendation;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
} catch {
|
|
1161
|
+
}
|
|
1162
|
+
return outcome;
|
|
1163
|
+
}
|
|
1164
|
+
async function runPipeline(issue, mode, from, noBranch, prReview, options = {}) {
|
|
1165
|
+
let requested;
|
|
1166
|
+
try {
|
|
1167
|
+
requested = parseIssueArguments(typeof issue === "string" ? [issue] : issue);
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
1170
|
+
return 1;
|
|
1171
|
+
}
|
|
1172
|
+
const first = await runIssueSession(requested[0], mode, {
|
|
1173
|
+
from,
|
|
1174
|
+
noBranch,
|
|
1175
|
+
prReview,
|
|
1176
|
+
requested,
|
|
1177
|
+
runOptions: options
|
|
1178
|
+
});
|
|
1179
|
+
if (first.queue === void 0) {
|
|
1180
|
+
return first.code;
|
|
1181
|
+
}
|
|
1182
|
+
return runQueue(
|
|
1183
|
+
first.queue.plan,
|
|
1184
|
+
{ mode, from, noBranch, prReview, runOptions: options },
|
|
1185
|
+
first.queue.resolved
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
async function runIssueSession(issueNumber, mode, input) {
|
|
1189
|
+
const paths = await resolveIssuePaths(issueNumber);
|
|
1190
|
+
const webConfig = await loadWebConfig();
|
|
1191
|
+
let publisher = new NullPublisher();
|
|
1192
|
+
if (webConfig.enabled) {
|
|
1193
|
+
await mkdir2(paths.issueDir, { recursive: true });
|
|
1194
|
+
publisher = new FilePublisher(paths.sessionFile, {
|
|
1195
|
+
logLimit: webConfig.logLimit,
|
|
1196
|
+
includeLogs: webConfig.includeLogs
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
setSessionPublisher(publisher);
|
|
1200
|
+
if (webConfig.enabled) {
|
|
1201
|
+
await ensureWebMonitor({
|
|
1202
|
+
publisher,
|
|
1203
|
+
port: webConfig.port,
|
|
1204
|
+
host: webConfig.host,
|
|
1205
|
+
refreshSeconds: webConfig.refreshSeconds
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
let result = {
|
|
1209
|
+
code: 1,
|
|
1210
|
+
failedPhase: null,
|
|
1211
|
+
branchName: null,
|
|
1212
|
+
storyCount: 0,
|
|
1213
|
+
elapsedSeconds: 0
|
|
1214
|
+
};
|
|
1215
|
+
try {
|
|
1216
|
+
result = await runPipelinePhases(issueNumber, paths, mode, publisher, input);
|
|
1217
|
+
return result;
|
|
1218
|
+
} finally {
|
|
1219
|
+
if (result.queue === void 0) {
|
|
1220
|
+
publisher.publish({
|
|
1221
|
+
type: "session:end",
|
|
1222
|
+
at: isoNow(),
|
|
1223
|
+
status: result.code === 0 ? "completed" : "failed"
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
await publisher.close();
|
|
1227
|
+
setSessionPublisher(void 0);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
|
|
1231
|
+
const { from, noBranch, prReview, queue } = input;
|
|
1232
|
+
const continueNumbering = input.runOptions?.continueNumbering;
|
|
1233
|
+
const startUs = input.runOptions?.startUs;
|
|
1234
|
+
const tasksPath = paths.tasksFile;
|
|
1235
|
+
const sessionId = randomUUID();
|
|
1236
|
+
let publishedIssueNumber = toIssueNumber(issueNumber);
|
|
1237
|
+
const publishSessionStart = (phases, at, info) => {
|
|
1238
|
+
publisher.publish({
|
|
1239
|
+
type: "session:start",
|
|
1240
|
+
at,
|
|
1241
|
+
sessionId,
|
|
1242
|
+
issueNumber: publishedIssueNumber,
|
|
1243
|
+
issueUrl: info?.issueUrl,
|
|
1244
|
+
branch: info?.branch,
|
|
1245
|
+
phases: [...phases],
|
|
1246
|
+
environment: { node: process.version, platform: process.platform }
|
|
1247
|
+
});
|
|
1248
|
+
};
|
|
1249
|
+
printInfo(`Starting pipeline for issue #${issueNumber} (mode: ${mode})`);
|
|
1250
|
+
const issuesConfig = await loadIssuesConfig();
|
|
1251
|
+
const sessionStartedAt = isoNow();
|
|
1252
|
+
if (queue?.preChecked !== true) {
|
|
1253
|
+
printInfo("Running prerequisite checks...");
|
|
1254
|
+
const initCode = await runInit(issuesConfig.preferredProvider);
|
|
1255
|
+
if (initCode !== 0) {
|
|
1256
|
+
publishSessionStart(PIPELINE_PHASES, sessionStartedAt);
|
|
1257
|
+
publisher.publish({ type: "phase:start", at: sessionStartedAt, phase: "init" });
|
|
1258
|
+
publisher.publish({
|
|
1259
|
+
type: "phase:end",
|
|
1260
|
+
at: isoNow(),
|
|
1261
|
+
phase: "init",
|
|
1262
|
+
success: false,
|
|
1263
|
+
error: "Prerequisites not met"
|
|
1264
|
+
});
|
|
1265
|
+
printError("Prerequisites not met. Fix the issues above and try again.");
|
|
1266
|
+
return failure(1);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
const resolution = await resolveCommandIssue(issueNumber, queue?.resolved, {
|
|
1270
|
+
config: issuesConfig
|
|
1271
|
+
});
|
|
1272
|
+
if (!resolution.ok) {
|
|
1273
|
+
return failure(resolution.code);
|
|
1274
|
+
}
|
|
1275
|
+
const resolvedIssue = resolution.resolved;
|
|
1276
|
+
publishedIssueNumber = resolvedIssue.issue.number;
|
|
1277
|
+
if (queue === void 0) {
|
|
1278
|
+
const decision = await decideQueue({
|
|
1279
|
+
requested: input.requested ?? [issueNumber],
|
|
1280
|
+
resolved: resolvedIssue,
|
|
1281
|
+
noBranch: noBranch ?? false,
|
|
1282
|
+
prReview: prReview ?? false,
|
|
1283
|
+
runOptions: input.runOptions ?? {}
|
|
1284
|
+
});
|
|
1285
|
+
if (decision.kind === "stop") {
|
|
1286
|
+
return failure(decision.code);
|
|
1287
|
+
}
|
|
1288
|
+
if (decision.kind === "queue") {
|
|
1289
|
+
return {
|
|
1290
|
+
...failure(0),
|
|
1291
|
+
queue: { plan: decision.plan, resumed: decision.resumed, resolved: resolvedIssue }
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
let effectiveNoBranch = noBranch ?? false;
|
|
1296
|
+
let effectivePrReview = prReview ?? false;
|
|
1297
|
+
let planIssueUrl;
|
|
1298
|
+
let planBranch;
|
|
1299
|
+
let planStories = [];
|
|
1300
|
+
try {
|
|
1301
|
+
const existingPlan = await loadTaskPlan(tasksPath);
|
|
1302
|
+
const persistedNoBranch = existingPlan.noBranch ?? false;
|
|
1303
|
+
planIssueUrl = existingPlan.issueUrl || void 0;
|
|
1304
|
+
planBranch = existingPlan.branchName || void 0;
|
|
1305
|
+
planStories = existingPlan.userStories;
|
|
1306
|
+
effectivePrReview = prReview ?? existingPlan.prReview?.enabled ?? false;
|
|
1307
|
+
if (noBranch !== void 0 && noBranch !== persistedNoBranch) {
|
|
1308
|
+
if (persistedNoBranch) {
|
|
1309
|
+
printWarning(
|
|
1310
|
+
"This pipeline was started with --no-branch. Ignoring current flag; using persisted mode."
|
|
1311
|
+
);
|
|
1312
|
+
} else {
|
|
1313
|
+
printWarning(
|
|
1314
|
+
"This pipeline was started without --no-branch. Ignoring current flag; using persisted mode."
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
effectiveNoBranch = persistedNoBranch;
|
|
1319
|
+
} catch {
|
|
1320
|
+
}
|
|
1321
|
+
if (effectiveNoBranch && effectivePrReview) {
|
|
1322
|
+
printWarning(
|
|
1323
|
+
"This pipeline runs with --no-branch and opens no PR. Skipping the pr-review phase."
|
|
1324
|
+
);
|
|
1325
|
+
effectivePrReview = false;
|
|
1326
|
+
}
|
|
1327
|
+
if (effectivePrReview) {
|
|
1328
|
+
try {
|
|
1329
|
+
const plan = await loadTaskPlan(tasksPath);
|
|
1330
|
+
if (plan.prReview?.enabled !== true) {
|
|
1331
|
+
plan.prReview = { ...plan.prReview, enabled: true, rounds: plan.prReview?.rounds ?? 0 };
|
|
1332
|
+
await saveTaskPlan(tasksPath, plan);
|
|
1333
|
+
}
|
|
1334
|
+
} catch {
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
const inQueue = queue !== void 0;
|
|
1338
|
+
const finalPr = queue?.finalPr;
|
|
1339
|
+
const activePhases = finalPr ? effectivePrReview ? QUEUE_PR_PHASES_WITH_REVIEW : QUEUE_PR_PHASES : inQueue ? PIPELINE_PHASES_NO_BRANCH : effectiveNoBranch ? PIPELINE_PHASES_NO_BRANCH : effectivePrReview ? PIPELINE_PHASES_WITH_PR_REVIEW : PIPELINE_PHASES;
|
|
1340
|
+
const phaseOrder = finalPr ? effectivePrReview ? RUNNABLE_QUEUE_PR_PHASES_WITH_REVIEW : RUNNABLE_QUEUE_PR_PHASES : inQueue ? RUNNABLE_PHASES_NO_BRANCH : effectiveNoBranch ? RUNNABLE_PHASES_NO_BRANCH : effectivePrReview ? RUNNABLE_PHASES_WITH_PR_REVIEW : RUNNABLE_PHASES;
|
|
1341
|
+
const queueCommitScope = queue === void 0 ? void 0 : `issue-${issueNumber}`;
|
|
1342
|
+
let producedBranch = null;
|
|
1343
|
+
if (queue !== void 0 && finalPr === void 0 && !effectiveNoBranch) {
|
|
1344
|
+
producedBranch = await adoptQueueBranch(tasksPath, queue.plan.branchName);
|
|
1345
|
+
if (producedBranch !== null) {
|
|
1346
|
+
planBranch = producedBranch;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
publishSessionStart(activePhases, sessionStartedAt, {
|
|
1350
|
+
issueUrl: planIssueUrl ?? resolvedIssue.issue.remoteRef ?? void 0,
|
|
1351
|
+
branch: planBranch
|
|
1352
|
+
});
|
|
1353
|
+
publishIssueDetails(publisher, resolvedIssue.issue, sessionStartedAt);
|
|
1354
|
+
publishStorySeed(publisher, planStories, sessionStartedAt);
|
|
1355
|
+
publisher.publish({ type: "phase:start", at: sessionStartedAt, phase: "init" });
|
|
1356
|
+
publisher.publish({ type: "phase:end", at: isoNow(), phase: "init", success: true });
|
|
1357
|
+
await publishGitState(publisher);
|
|
1358
|
+
let startPhase = "prd";
|
|
1359
|
+
if (from) {
|
|
1360
|
+
if (!activePhases.includes(from)) {
|
|
1361
|
+
const validPhases = activePhases.filter((p) => p !== "init").join(", ");
|
|
1362
|
+
if (effectiveNoBranch && PIPELINE_PHASES.includes(from)) {
|
|
1363
|
+
printError(
|
|
1364
|
+
`The '${from}' phase is not available in --no-branch mode. Valid phases: ${validPhases}`
|
|
1365
|
+
);
|
|
1366
|
+
} else {
|
|
1367
|
+
printError(`Invalid phase: ${from}. Valid phases: ${validPhases}`);
|
|
1368
|
+
}
|
|
1369
|
+
return failure(1);
|
|
1370
|
+
}
|
|
1371
|
+
startPhase = from;
|
|
1372
|
+
} else {
|
|
1373
|
+
try {
|
|
1374
|
+
const plan = await loadTaskPlan(tasksPath);
|
|
1375
|
+
const mgr = new PipelineManager(plan, tasksPath, activePhases);
|
|
1376
|
+
const nextPhase = mgr.getNextPhase();
|
|
1377
|
+
if (nextPhase && nextPhase !== "init") {
|
|
1378
|
+
startPhase = nextPhase;
|
|
1379
|
+
printInfo(`Resuming from phase: ${startPhase}`);
|
|
1380
|
+
}
|
|
1381
|
+
} catch {
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
if (from) {
|
|
1385
|
+
try {
|
|
1386
|
+
const plan = await loadTaskPlan(tasksPath);
|
|
1387
|
+
const mgr = new PipelineManager(plan, tasksPath, activePhases);
|
|
1388
|
+
if (!mgr.canResume(startPhase)) {
|
|
1389
|
+
printError(`Cannot resume from ${startPhase}: prerequisite phases not complete`);
|
|
1390
|
+
return failure(1);
|
|
1391
|
+
}
|
|
1392
|
+
} catch {
|
|
1393
|
+
if (startPhase !== "prd") {
|
|
1394
|
+
printError(`Cannot resume from ${startPhase}: no pipeline state found`);
|
|
1395
|
+
return failure(1);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
const startIdx = Math.max(phaseOrder.indexOf(startPhase), 0);
|
|
1400
|
+
const makeRunner = (fn, phase) => async () => {
|
|
1401
|
+
const code = await fn();
|
|
1402
|
+
if (code !== 0) {
|
|
1403
|
+
throw new Error(`Phase ${phase} failed with exit code ${code}`);
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
const runners = {
|
|
1407
|
+
prd: makeRunner(() => runPrd(issueNumber, resolvedIssue), "prd"),
|
|
1408
|
+
plan: async () => {
|
|
1409
|
+
await makeRunner(
|
|
1410
|
+
() => runPlan(issueNumber, resolvedIssue, { continueFlag: continueNumbering, startUs }),
|
|
1411
|
+
"plan"
|
|
1412
|
+
)();
|
|
1413
|
+
if (effectiveNoBranch || effectivePrReview) {
|
|
1414
|
+
try {
|
|
1415
|
+
const plan = await loadTaskPlan(tasksPath);
|
|
1416
|
+
if (effectiveNoBranch) {
|
|
1417
|
+
plan.noBranch = true;
|
|
1418
|
+
}
|
|
1419
|
+
if (effectivePrReview) {
|
|
1420
|
+
plan.prReview = { ...plan.prReview, enabled: true, rounds: plan.prReview?.rounds ?? 0 };
|
|
1421
|
+
}
|
|
1422
|
+
await saveTaskPlan(tasksPath, plan);
|
|
1423
|
+
} catch {
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (queue !== void 0 && !effectiveNoBranch) {
|
|
1427
|
+
producedBranch = await adoptQueueBranch(tasksPath, queue.plan.branchName);
|
|
1428
|
+
}
|
|
1429
|
+
},
|
|
1430
|
+
execute: makeRunner(
|
|
1431
|
+
() => runExecute(void 0, { issue: issueNumber, commitScope: queueCommitScope }),
|
|
1432
|
+
"execute"
|
|
1433
|
+
),
|
|
1434
|
+
review: async () => {
|
|
1435
|
+
let maxCycles = 3;
|
|
1436
|
+
try {
|
|
1437
|
+
const plan = await loadTaskPlan(tasksPath);
|
|
1438
|
+
maxCycles = plan.maxCorrectionCycles;
|
|
1439
|
+
} catch {
|
|
1440
|
+
}
|
|
1441
|
+
let code = await runReview(issueNumber, resolvedIssue);
|
|
1442
|
+
let cycle = 0;
|
|
1443
|
+
while (code !== 0 && cycle < maxCycles) {
|
|
1444
|
+
cycle++;
|
|
1445
|
+
printWarning(`Review failed. Starting correction cycle ${cycle}/${maxCycles}...`);
|
|
1446
|
+
publisher.publish({ type: "correction:cycle", at: isoNow(), cycle, maxCycles });
|
|
1447
|
+
try {
|
|
1448
|
+
const plan = await loadTaskPlan(tasksPath);
|
|
1449
|
+
plan.correctionCycle = cycle;
|
|
1450
|
+
await saveTaskPlan(tasksPath, plan);
|
|
1451
|
+
} catch {
|
|
1452
|
+
}
|
|
1453
|
+
const execCode = await runExecute(void 0, {
|
|
1454
|
+
issue: issueNumber,
|
|
1455
|
+
commitScope: queueCommitScope
|
|
1456
|
+
});
|
|
1457
|
+
if (execCode !== 0) {
|
|
1458
|
+
throw new Error("Correction execution failed");
|
|
1459
|
+
}
|
|
1460
|
+
code = await runReview(issueNumber, resolvedIssue);
|
|
1461
|
+
}
|
|
1462
|
+
if (code !== 0) {
|
|
1463
|
+
throw new Error(`Review failed after ${maxCycles} correction cycles`);
|
|
1464
|
+
}
|
|
1465
|
+
},
|
|
1466
|
+
pr: makeRunner(
|
|
1467
|
+
// The options argument is omitted outside a queue so a standalone run
|
|
1468
|
+
// calls `runPr` exactly as it always did.
|
|
1469
|
+
() => finalPr === void 0 ? runPr(issueNumber, resolvedIssue) : runPr(issueNumber, resolvedIssue, { queue: finalPr }),
|
|
1470
|
+
"pr"
|
|
1471
|
+
)
|
|
1472
|
+
};
|
|
1473
|
+
const reviewBox = { outcome: null };
|
|
1474
|
+
if (effectivePrReview) {
|
|
1475
|
+
runners["pr-review"] = async () => {
|
|
1476
|
+
const code = await runPrReview(void 0, { issue: issueNumber, yes: true });
|
|
1477
|
+
if (code === 1) {
|
|
1478
|
+
throw new Error("Phase pr-review failed with exit code 1");
|
|
1479
|
+
}
|
|
1480
|
+
reviewBox.outcome = await readPrReviewOutcome(issueNumber, tasksPath, code === 2);
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
const instrumentedRunners = Object.fromEntries(
|
|
1484
|
+
Object.entries(runners).map(([phase, fn]) => [
|
|
1485
|
+
phase,
|
|
1486
|
+
async () => {
|
|
1487
|
+
publisher.publish({ type: "phase:start", at: isoNow(), phase });
|
|
1488
|
+
try {
|
|
1489
|
+
await fn();
|
|
1490
|
+
await publishGitState(publisher);
|
|
1491
|
+
publisher.publish({ type: "phase:end", at: isoNow(), phase, success: true });
|
|
1492
|
+
} catch (err) {
|
|
1493
|
+
await publishGitState(publisher);
|
|
1494
|
+
publisher.publish({
|
|
1495
|
+
type: "phase:end",
|
|
1496
|
+
at: isoNow(),
|
|
1497
|
+
phase,
|
|
1498
|
+
success: false,
|
|
1499
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1500
|
+
});
|
|
1501
|
+
throw err;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
])
|
|
1505
|
+
);
|
|
1506
|
+
const result = await runPipelineWithRenderer({
|
|
1507
|
+
phases: phaseOrder,
|
|
1508
|
+
startIndex: startIdx,
|
|
1509
|
+
verbose: isVerbose(),
|
|
1510
|
+
runners: instrumentedRunners,
|
|
1511
|
+
tasksPath
|
|
1512
|
+
});
|
|
1513
|
+
if (!result.success) {
|
|
1514
|
+
printError(`Phase ${result.failedPhase} failed`);
|
|
1515
|
+
return {
|
|
1516
|
+
...failure(1),
|
|
1517
|
+
failedPhase: result.failedPhase ?? null,
|
|
1518
|
+
branchName: producedBranch,
|
|
1519
|
+
elapsedSeconds: result.overallElapsedSeconds
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
const review = reviewBox.outcome;
|
|
1523
|
+
if (review?.requestedChanges) {
|
|
1524
|
+
console.log("");
|
|
1525
|
+
printWarning("PR review requested changes \u2014 the Pull Request is not ready to merge.");
|
|
1526
|
+
if (review.reportPath !== null) {
|
|
1527
|
+
console.log(` Report: ${review.reportPath}`);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
if (review?.requestedChanges) {
|
|
1531
|
+
printInfo("Issue left open until the review blockers are addressed.");
|
|
1532
|
+
} else if (!inQueue) {
|
|
1533
|
+
await closeIssue(issueNumber, resolvedIssue.source);
|
|
1534
|
+
}
|
|
1535
|
+
let branchName = "unknown";
|
|
1536
|
+
try {
|
|
1537
|
+
const proc = await execa("git", ["branch", "--show-current"], { reject: false });
|
|
1538
|
+
branchName = proc.stdout?.toString().trim() ?? "unknown";
|
|
1539
|
+
} catch {
|
|
1540
|
+
}
|
|
1541
|
+
let storyCount = 0;
|
|
1542
|
+
let planPrUrl = null;
|
|
1543
|
+
try {
|
|
1544
|
+
const plan = await loadTaskPlan(tasksPath);
|
|
1545
|
+
storyCount = plan.userStories.length;
|
|
1546
|
+
planPrUrl = plan.pullRequest?.url ?? null;
|
|
1547
|
+
plan.lastAttemptAt = isoNow();
|
|
1548
|
+
if (!review?.requestedChanges) {
|
|
1549
|
+
plan.issueStatus = "completed";
|
|
1550
|
+
plan.completedAt = isoNow();
|
|
1551
|
+
}
|
|
1552
|
+
await saveTaskPlan(tasksPath, plan);
|
|
1553
|
+
} catch {
|
|
1554
|
+
}
|
|
1555
|
+
if (inQueue) {
|
|
1556
|
+
if (finalPr === void 0) {
|
|
1557
|
+
printSuccess(`Issue #${issueNumber} completed (${storyCount} stories).`);
|
|
1558
|
+
}
|
|
1559
|
+
return {
|
|
1560
|
+
code: 0,
|
|
1561
|
+
failedPhase: null,
|
|
1562
|
+
branchName: producedBranch ?? (branchName === "unknown" ? null : branchName),
|
|
1563
|
+
storyCount,
|
|
1564
|
+
elapsedSeconds: result.overallElapsedSeconds,
|
|
1565
|
+
review
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
let prUrl = "unknown";
|
|
1569
|
+
if (!effectiveNoBranch && resolvedIssue.source === "github") {
|
|
1570
|
+
if (planPrUrl !== null) {
|
|
1571
|
+
prUrl = planPrUrl;
|
|
1572
|
+
} else if (branchName !== "unknown" && branchName !== "") {
|
|
1573
|
+
try {
|
|
1574
|
+
const latest = mostRecent(await listPullRequests(branchName));
|
|
1575
|
+
if (latest !== null) {
|
|
1576
|
+
prUrl = latest.url;
|
|
1577
|
+
}
|
|
1578
|
+
} catch {
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
printRunSummary({
|
|
1583
|
+
issueNumber,
|
|
1584
|
+
branchName,
|
|
1585
|
+
noBranch: effectiveNoBranch,
|
|
1586
|
+
storyCount,
|
|
1587
|
+
elapsedSeconds: result.overallElapsedSeconds,
|
|
1588
|
+
prUrl,
|
|
1589
|
+
prReview: review,
|
|
1590
|
+
// Process-owned counters: the session snapshot is empty whenever web
|
|
1591
|
+
// monitoring is off, so it cannot be the source of these totals.
|
|
1592
|
+
usage: getRunUsageTotals()
|
|
1593
|
+
});
|
|
1594
|
+
return {
|
|
1595
|
+
code: 0,
|
|
1596
|
+
failedPhase: null,
|
|
1597
|
+
branchName: branchName === "unknown" ? null : branchName,
|
|
1598
|
+
storyCount,
|
|
1599
|
+
elapsedSeconds: result.overallElapsedSeconds
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
async function decideQueue(input) {
|
|
1603
|
+
const primary = input.requested[0];
|
|
1604
|
+
const single = input.requested.length === 1;
|
|
1605
|
+
if (single && input.runOptions.only === true) {
|
|
1606
|
+
return { kind: "single" };
|
|
1607
|
+
}
|
|
1608
|
+
try {
|
|
1609
|
+
const [project, queuePaths] = await Promise.all([
|
|
1610
|
+
resolveProjectPaths(),
|
|
1611
|
+
resolveQueuePaths(primary)
|
|
1612
|
+
]);
|
|
1613
|
+
return await planQueue({
|
|
1614
|
+
requested: input.requested,
|
|
1615
|
+
source: input.resolved.source,
|
|
1616
|
+
known: [input.resolved.issue],
|
|
1617
|
+
projectId: project.projectId,
|
|
1618
|
+
planFile: queuePaths.planFile,
|
|
1619
|
+
noBranch: input.noBranch,
|
|
1620
|
+
prReview: input.prReview,
|
|
1621
|
+
confirm: { yes: input.runOptions.yes, only: input.runOptions.only }
|
|
1622
|
+
});
|
|
1623
|
+
} catch (err) {
|
|
1624
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1625
|
+
if (single) {
|
|
1626
|
+
printWarning(
|
|
1627
|
+
`Could not inspect related issues (${message}). Running issue #${primary} alone.`
|
|
1628
|
+
);
|
|
1629
|
+
return { kind: "single" };
|
|
1630
|
+
}
|
|
1631
|
+
printError(`Could not plan the execution of ${input.requested.length} issues: ${message}`);
|
|
1632
|
+
return { kind: "stop", code: 1 };
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
async function runQueue(initialPlan, options, resolvedPrimary) {
|
|
1636
|
+
const planFile = (await resolveQueuePaths(initialPlan.id)).planFile;
|
|
1637
|
+
let plan = initialPlan;
|
|
1638
|
+
const queueUsage = beginUsageScope();
|
|
1639
|
+
const startedAtMs = Date.now();
|
|
1640
|
+
const summaries = [];
|
|
1641
|
+
let firstOfThisRun = true;
|
|
1642
|
+
try {
|
|
1643
|
+
while (true) {
|
|
1644
|
+
const entry = nextQueueIssue(plan);
|
|
1645
|
+
if (entry === null) break;
|
|
1646
|
+
plan = markQueueIssueInProgress(plan, entry.id);
|
|
1647
|
+
await saveExecutionPlan(planFile, plan);
|
|
1648
|
+
printInfo(
|
|
1649
|
+
`Queue ${entry.position}/${plan.issues.length}: issue #${entry.id}${entry.title === "" ? "" : ` \u2014 ${entry.title}`}`
|
|
1650
|
+
);
|
|
1651
|
+
const issueUsage = beginUsageScope();
|
|
1652
|
+
let result;
|
|
1653
|
+
try {
|
|
1654
|
+
result = await runIssueSession(entry.id, options.mode, {
|
|
1655
|
+
// `--from` addresses the issue the queue is resuming, never the ones
|
|
1656
|
+
// that come after it — those start from their own beginning.
|
|
1657
|
+
from: firstOfThisRun ? options.from : void 0,
|
|
1658
|
+
noBranch: options.noBranch,
|
|
1659
|
+
// `--start-us` belongs to the first issue this invocation runs; the
|
|
1660
|
+
// ones after it continue from the history those plans just wrote.
|
|
1661
|
+
runOptions: {
|
|
1662
|
+
...options.runOptions,
|
|
1663
|
+
startUs: firstOfThisRun ? options.runOptions?.startUs : void 0
|
|
1664
|
+
},
|
|
1665
|
+
queue: {
|
|
1666
|
+
plan,
|
|
1667
|
+
preChecked: true,
|
|
1668
|
+
resolved: entry.id === initialPlan.id ? resolvedPrimary : void 0
|
|
1669
|
+
}
|
|
1670
|
+
});
|
|
1671
|
+
} finally {
|
|
1672
|
+
firstOfThisRun = false;
|
|
1673
|
+
issueUsage.end();
|
|
1674
|
+
}
|
|
1675
|
+
const usage = issueUsage.totals();
|
|
1676
|
+
if (result.code !== 0) {
|
|
1677
|
+
plan = markQueueIssueFailed(plan, entry.id, {
|
|
1678
|
+
phase: result.failedPhase,
|
|
1679
|
+
error: {
|
|
1680
|
+
category: "queue_issue_failed",
|
|
1681
|
+
message: `Issue #${entry.id} failed${result.failedPhase === null ? "" : ` in phase ${result.failedPhase}`}`,
|
|
1682
|
+
at: isoNow()
|
|
1683
|
+
}
|
|
1684
|
+
});
|
|
1685
|
+
await saveExecutionPlan(planFile, plan);
|
|
1686
|
+
printError(
|
|
1687
|
+
`Queue stopped at issue #${entry.id}${result.failedPhase === null ? "" : ` (phase ${result.failedPhase})`}. The branch and every commit made so far were kept.`
|
|
1688
|
+
);
|
|
1689
|
+
printInfo(`Resume with: issue-flow run ${plan.requested.join(",")}`);
|
|
1690
|
+
return result.code;
|
|
1691
|
+
}
|
|
1692
|
+
if (plan.branchName === null && result.branchName !== null) {
|
|
1693
|
+
plan = setQueueBranch(plan, result.branchName);
|
|
1694
|
+
}
|
|
1695
|
+
plan = markQueueIssueCompleted(plan, entry.id);
|
|
1696
|
+
await saveExecutionPlan(planFile, plan);
|
|
1697
|
+
summaries.push({
|
|
1698
|
+
id: entry.id,
|
|
1699
|
+
title: entry.title,
|
|
1700
|
+
storyCount: result.storyCount,
|
|
1701
|
+
elapsedSeconds: result.elapsedSeconds,
|
|
1702
|
+
usage
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
return await finishQueue(
|
|
1706
|
+
plan,
|
|
1707
|
+
planFile,
|
|
1708
|
+
summaries,
|
|
1709
|
+
startedAtMs,
|
|
1710
|
+
queueUsage,
|
|
1711
|
+
options,
|
|
1712
|
+
resolvedPrimary
|
|
1713
|
+
);
|
|
1714
|
+
} finally {
|
|
1715
|
+
queueUsage.end();
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
async function finishQueue(plan, planFile, summaries, startedAtMs, queueUsage, options, resolvedPrimary) {
|
|
1719
|
+
let current = plan;
|
|
1720
|
+
let review = null;
|
|
1721
|
+
const alreadyOpened = current.pullRequest !== void 0 || await primaryPrCreated(current);
|
|
1722
|
+
if (!current.noBranch && options.noBranch !== true && !alreadyOpened) {
|
|
1723
|
+
const outcome = await runIssueSession(current.id, options.mode, {
|
|
1724
|
+
prReview: options.prReview ?? current.prReview,
|
|
1725
|
+
queue: {
|
|
1726
|
+
plan: current,
|
|
1727
|
+
preChecked: true,
|
|
1728
|
+
resolved: resolvedPrimary,
|
|
1729
|
+
finalPr: await buildPrQueueContext(current)
|
|
1730
|
+
}
|
|
1731
|
+
});
|
|
1732
|
+
review = outcome.review ?? null;
|
|
1733
|
+
if (outcome.code !== 0) {
|
|
1734
|
+
printError("The consolidated Pull Request could not be created.");
|
|
1735
|
+
await saveExecutionPlan(planFile, current);
|
|
1736
|
+
return outcome.code;
|
|
1737
|
+
}
|
|
1738
|
+
const pullRequest = await propagatePullRequest(current);
|
|
1739
|
+
if (pullRequest !== null) {
|
|
1740
|
+
current = setQueuePullRequest(current, pullRequest);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
await saveExecutionPlan(planFile, current);
|
|
1744
|
+
if (review?.requestedChanges === true) {
|
|
1745
|
+
printInfo("Issues left open until the review blockers are addressed.");
|
|
1746
|
+
} else {
|
|
1747
|
+
for (const entry of current.issues) {
|
|
1748
|
+
await closeIssue(entry.id, entry.source);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
printQueueSummary({
|
|
1752
|
+
queueId: current.id,
|
|
1753
|
+
branchName: current.branchName,
|
|
1754
|
+
issues: summaries,
|
|
1755
|
+
excluded: current.excluded.map((entry) => ({ id: entry.id, title: entry.title })),
|
|
1756
|
+
elapsedSeconds: Math.round((Date.now() - startedAtMs) / 1e3),
|
|
1757
|
+
prUrl: current.pullRequest?.url ?? null,
|
|
1758
|
+
// Read here, after the consolidated Pull Request (and the optional
|
|
1759
|
+
// pr-review) already ran: reading it before would report a queue total
|
|
1760
|
+
// that leaves out the phase that just spent tokens.
|
|
1761
|
+
usage: queueUsage.totals(),
|
|
1762
|
+
prReview: review
|
|
1763
|
+
});
|
|
1764
|
+
return 0;
|
|
1765
|
+
}
|
|
1766
|
+
async function primaryPrCreated(plan) {
|
|
1767
|
+
try {
|
|
1768
|
+
const paths = await resolveIssuePaths(plan.id);
|
|
1769
|
+
return (await loadTaskPlan(paths.tasksFile)).pipeline.prCreated;
|
|
1770
|
+
} catch {
|
|
1771
|
+
return false;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
async function buildPrQueueContext(plan) {
|
|
1775
|
+
const pending = [];
|
|
1776
|
+
for (const entry of plan.issues) {
|
|
1777
|
+
try {
|
|
1778
|
+
const paths = await resolveIssuePaths(entry.id);
|
|
1779
|
+
const taskPlan = await loadTaskPlan(paths.tasksFile);
|
|
1780
|
+
if (taskPlan.lastReviewFindings !== null && taskPlan.lastReviewFindings !== "") {
|
|
1781
|
+
pending.push(`Issue #${entry.id} has unresolved review findings`);
|
|
1782
|
+
}
|
|
1783
|
+
} catch {
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
return {
|
|
1787
|
+
issues: plan.issues.map((entry) => ({
|
|
1788
|
+
id: entry.id,
|
|
1789
|
+
number: entry.number,
|
|
1790
|
+
title: entry.title,
|
|
1791
|
+
url: entry.url,
|
|
1792
|
+
source: entry.source
|
|
1793
|
+
})),
|
|
1794
|
+
excluded: plan.excluded.map((entry) => ({
|
|
1795
|
+
id: entry.id,
|
|
1796
|
+
number: entry.number,
|
|
1797
|
+
title: entry.title,
|
|
1798
|
+
reason: entry.reason
|
|
1799
|
+
})),
|
|
1800
|
+
pending
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
async function propagatePullRequest(plan) {
|
|
1804
|
+
let pullRequest = null;
|
|
1805
|
+
try {
|
|
1806
|
+
const primaryPaths = await resolveIssuePaths(plan.id);
|
|
1807
|
+
pullRequest = (await loadTaskPlan(primaryPaths.tasksFile)).pullRequest ?? null;
|
|
1808
|
+
} catch {
|
|
1809
|
+
return null;
|
|
1810
|
+
}
|
|
1811
|
+
if (pullRequest === null) return null;
|
|
1812
|
+
for (const entry of plan.issues) {
|
|
1813
|
+
if (entry.id === plan.id) continue;
|
|
1814
|
+
try {
|
|
1815
|
+
const paths = await resolveIssuePaths(entry.id);
|
|
1816
|
+
const taskPlan = await loadTaskPlan(paths.tasksFile);
|
|
1817
|
+
taskPlan.pullRequest = pullRequest;
|
|
1818
|
+
taskPlan.pipeline.prCreated = true;
|
|
1819
|
+
await saveTaskPlan(paths.tasksFile, taskPlan);
|
|
1820
|
+
} catch {
|
|
1821
|
+
printWarning(`Could not record the Pull Request on issue #${entry.id}'s task plan.`);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
return pullRequest;
|
|
1825
|
+
}
|
|
1826
|
+
function failure(code) {
|
|
1827
|
+
return { code, failedPhase: null, branchName: null, storyCount: 0, elapsedSeconds: 0 };
|
|
1828
|
+
}
|
|
1829
|
+
async function closeIssue(issueNumber, source) {
|
|
1830
|
+
try {
|
|
1831
|
+
const provider = getProvider(source);
|
|
1832
|
+
if (provider.close !== void 0) {
|
|
1833
|
+
printInfo("Closing issue...");
|
|
1834
|
+
await provider.close(issueNumber);
|
|
1835
|
+
}
|
|
1836
|
+
} catch {
|
|
1837
|
+
printWarning("Failed to close issue automatically");
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
async function adoptQueueBranch(tasksPath, queueBranch) {
|
|
1841
|
+
try {
|
|
1842
|
+
const plan = await loadTaskPlan(tasksPath);
|
|
1843
|
+
if (queueBranch === null || queueBranch === "") {
|
|
1844
|
+
return plan.branchName === "" ? null : plan.branchName;
|
|
1845
|
+
}
|
|
1846
|
+
if (plan.branchName !== queueBranch) {
|
|
1847
|
+
plan.branchName = queueBranch;
|
|
1848
|
+
await saveTaskPlan(tasksPath, plan);
|
|
1849
|
+
}
|
|
1850
|
+
return queueBranch;
|
|
1851
|
+
} catch {
|
|
1852
|
+
return queueBranch;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
export {
|
|
1856
|
+
publishIssueDetails,
|
|
1857
|
+
publishStorySeed,
|
|
1858
|
+
runPipeline
|
|
1859
|
+
};
|
|
1860
|
+
//# sourceMappingURL=run-BRBFHVUO.js.map
|