issue-flow 0.11.0 → 0.12.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 +1 -1
- package/dist/{analyze-7B5TORNK.js → analyze-OEET7KGE.js} +17 -11
- package/dist/analyze-OEET7KGE.js.map +1 -0
- package/dist/{apply-WUJHDD7A.js → apply-ZIA3C52S.js} +6 -4
- package/dist/{apply-WUJHDD7A.js.map → apply-ZIA3C52S.js.map} +1 -1
- package/dist/{chunk-6N7LZZ5U.js → chunk-3QHK4YQA.js} +204 -107
- package/dist/chunk-3QHK4YQA.js.map +1 -0
- package/dist/chunk-42PJFMAR.js +605 -0
- package/dist/chunk-42PJFMAR.js.map +1 -0
- package/dist/{chunk-PL6CNMVS.js → chunk-4JSOYE4G.js} +105 -19
- package/dist/chunk-4JSOYE4G.js.map +1 -0
- package/dist/chunk-56KS3TFQ.js +703 -0
- package/dist/chunk-56KS3TFQ.js.map +1 -0
- package/dist/{chunk-AAY6KM7N.js → chunk-73ATPIYX.js} +160 -154
- package/dist/chunk-73ATPIYX.js.map +1 -0
- package/dist/{chunk-MKD2D7WE.js → chunk-A4SLHOW7.js} +70 -13
- package/dist/chunk-A4SLHOW7.js.map +1 -0
- package/dist/chunk-BAUPQXPE.js +173 -0
- package/dist/chunk-BAUPQXPE.js.map +1 -0
- package/dist/{chunk-HC2MT7BX.js → chunk-BYZDPR7N.js} +14 -581
- package/dist/chunk-BYZDPR7N.js.map +1 -0
- package/dist/chunk-CD5ULRVC.js +615 -0
- package/dist/chunk-CD5ULRVC.js.map +1 -0
- package/dist/chunk-DIDUVL6O.js +1365 -0
- package/dist/chunk-DIDUVL6O.js.map +1 -0
- package/dist/{chunk-47VIN33V.js → chunk-EZATO3DW.js} +7 -5
- package/dist/{chunk-47VIN33V.js.map → chunk-EZATO3DW.js.map} +1 -1
- package/dist/{chunk-JW7WLSZ6.js → chunk-FADTCQGM.js} +48 -12
- package/dist/chunk-FADTCQGM.js.map +1 -0
- package/dist/{chunk-GANSDJV7.js → chunk-FJP3UQGD.js} +15 -13
- package/dist/chunk-FJP3UQGD.js.map +1 -0
- package/dist/chunk-KMMASOA6.js +134 -0
- package/dist/chunk-KMMASOA6.js.map +1 -0
- package/dist/{chunk-HQYXVIH6.js → chunk-LAFNLLQP.js} +3 -3
- package/dist/chunk-LWRSMTTZ.js +18 -0
- package/dist/chunk-LWRSMTTZ.js.map +1 -0
- package/dist/{chunk-MSPUB635.js → chunk-M5UCXTIT.js} +15 -12
- package/dist/chunk-M5UCXTIT.js.map +1 -0
- package/dist/chunk-M66SJHQQ.js +206 -0
- package/dist/chunk-M66SJHQQ.js.map +1 -0
- package/dist/{chunk-OBLVLIUS.js → chunk-MJULIKQU.js} +42 -6
- package/dist/chunk-MJULIKQU.js.map +1 -0
- package/dist/{chunk-RKTEV6EG.js → chunk-NS6R37II.js} +4 -4
- package/dist/{run-H75MD5BQ.js → chunk-NTMMF5UD.js} +515 -544
- package/dist/chunk-NTMMF5UD.js.map +1 -0
- package/dist/{chunk-UBIQXCVE.js → chunk-QCPGHBSZ.js} +16 -13
- package/dist/chunk-QCPGHBSZ.js.map +1 -0
- package/dist/{chunk-LVKQ2AKT.js → chunk-TSOLWK7Y.js} +8 -6
- package/dist/chunk-TSOLWK7Y.js.map +1 -0
- package/dist/{chunk-VCKX262Z.js → chunk-XQUSIEKA.js} +13 -10
- package/dist/chunk-XQUSIEKA.js.map +1 -0
- package/dist/chunk-Y5ZDJ4PJ.js +76 -0
- package/dist/chunk-Y5ZDJ4PJ.js.map +1 -0
- package/dist/cli.js +156 -26
- package/dist/cli.js.map +1 -1
- package/dist/{config-4THHOXNN.js → config-PRJGWW7J.js} +15 -3
- package/dist/execute-XBNVL753.js +18 -0
- package/dist/{generate-6RA2MFJB.js → generate-EGB4KV2B.js} +18 -11
- package/dist/generate-EGB4KV2B.js.map +1 -0
- package/dist/init-AJHCXJBP.js +10 -0
- package/dist/operations-BYT626KQ.js +365 -0
- package/dist/operations-BYT626KQ.js.map +1 -0
- package/dist/plan-QOZ3RIO6.js +21 -0
- package/dist/{policy-XQW5NUIE.js → policy-6S4GFC76.js} +6 -4
- package/dist/{policy-XQW5NUIE.js.map → policy-6S4GFC76.js.map} +1 -1
- package/dist/pr-EBBHD2ZX.js +26 -0
- package/dist/pr-review-VVMW5PO4.js +20 -0
- package/dist/prd-JQSZF7PR.js +21 -0
- package/dist/resume-PPCNQVVV.js +221 -0
- package/dist/resume-PPCNQVVV.js.map +1 -0
- package/dist/review-3F6MARWH.js +19 -0
- package/dist/run-RXE5GDQO.js +36 -0
- package/dist/run-RXE5GDQO.js.map +1 -0
- package/dist/state-manager-2EDVDBAR.js +34 -0
- package/dist/state-manager-2EDVDBAR.js.map +1 -0
- package/dist/{web-ANQQTTRD.js → web-EF5WIIGG.js} +13 -8
- package/dist/{web-ANQQTTRD.js.map → web-EF5WIIGG.js.map} +1 -1
- package/package.json +1 -1
- package/prompts/pr.md +11 -4
- package/web/public/app.js +12 -0
- package/dist/analyze-7B5TORNK.js.map +0 -1
- package/dist/chunk-3BOHTH4V.js +0 -58
- package/dist/chunk-3BOHTH4V.js.map +0 -1
- package/dist/chunk-6N7LZZ5U.js.map +0 -1
- package/dist/chunk-AAY6KM7N.js.map +0 -1
- package/dist/chunk-GANSDJV7.js.map +0 -1
- package/dist/chunk-HC2MT7BX.js.map +0 -1
- package/dist/chunk-HDV3YDTG.js +0 -52
- package/dist/chunk-HDV3YDTG.js.map +0 -1
- package/dist/chunk-JW7WLSZ6.js.map +0 -1
- package/dist/chunk-LVKQ2AKT.js.map +0 -1
- package/dist/chunk-MKD2D7WE.js.map +0 -1
- package/dist/chunk-MSPUB635.js.map +0 -1
- package/dist/chunk-OBLVLIUS.js.map +0 -1
- package/dist/chunk-PL6CNMVS.js.map +0 -1
- package/dist/chunk-UBIQXCVE.js.map +0 -1
- package/dist/chunk-VCKX262Z.js.map +0 -1
- package/dist/chunk-VQ5VN7DF.js +0 -1018
- package/dist/chunk-VQ5VN7DF.js.map +0 -1
- package/dist/execute-5OEKLM5X.js +0 -15
- package/dist/generate-6RA2MFJB.js.map +0 -1
- package/dist/init-PRMOTQ7N.js +0 -9
- package/dist/plan-KENR6MJ3.js +0 -17
- package/dist/pr-KDGKG4BP.js +0 -21
- package/dist/pr-review-CAMAB7KO.js +0 -17
- package/dist/prd-V3MWDIHK.js +0 -17
- package/dist/review-7LN47SAN.js +0 -15
- package/dist/run-H75MD5BQ.js.map +0 -1
- /package/dist/{chunk-HQYXVIH6.js.map → chunk-LAFNLLQP.js.map} +0 -0
- /package/dist/{chunk-RKTEV6EG.js.map → chunk-NS6R37II.js.map} +0 -0
- /package/dist/{config-4THHOXNN.js.map → config-PRJGWW7J.js.map} +0 -0
- /package/dist/{execute-5OEKLM5X.js.map → execute-XBNVL753.js.map} +0 -0
- /package/dist/{init-PRMOTQ7N.js.map → init-AJHCXJBP.js.map} +0 -0
- /package/dist/{plan-KENR6MJ3.js.map → plan-QOZ3RIO6.js.map} +0 -0
- /package/dist/{pr-KDGKG4BP.js.map → pr-EBBHD2ZX.js.map} +0 -0
- /package/dist/{pr-review-CAMAB7KO.js.map → pr-review-VVMW5PO4.js.map} +0 -0
- /package/dist/{prd-V3MWDIHK.js.map → prd-JQSZF7PR.js.map} +0 -0
- /package/dist/{review-7LN47SAN.js.map → review-3F6MARWH.js.map} +0 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
emptyRelations,
|
|
4
|
+
uniqueIds
|
|
5
|
+
} from "./chunk-KMMASOA6.js";
|
|
6
|
+
import {
|
|
7
|
+
executionPlanSchema
|
|
8
|
+
} from "./chunk-DIDUVL6O.js";
|
|
9
|
+
import {
|
|
10
|
+
MemoryPublisher
|
|
11
|
+
} from "./chunk-BYZDPR7N.js";
|
|
12
|
+
import {
|
|
13
|
+
isoNow,
|
|
14
|
+
loadTaskPlan,
|
|
15
|
+
saveTaskPlan,
|
|
16
|
+
writeFileAtomic
|
|
17
|
+
} from "./chunk-CD5ULRVC.js";
|
|
18
|
+
|
|
19
|
+
// src/core/journal.ts
|
|
20
|
+
import { appendFile, mkdir, rename, stat, unlink } from "fs/promises";
|
|
21
|
+
import { dirname } from "path";
|
|
22
|
+
var DEFAULT_JOURNAL_MAX_BYTES = 10 * 1024 * 1024;
|
|
23
|
+
var JournalPublisher = class extends MemoryPublisher {
|
|
24
|
+
filePath;
|
|
25
|
+
rotatedPath;
|
|
26
|
+
maxFileBytes;
|
|
27
|
+
seq = 0;
|
|
28
|
+
writeChain = Promise.resolve();
|
|
29
|
+
closed = false;
|
|
30
|
+
/** Bytes in the current file. `null` until the first write measures it. */
|
|
31
|
+
bytes = null;
|
|
32
|
+
constructor(filePath, rotatedPath, options = {}) {
|
|
33
|
+
super(options);
|
|
34
|
+
this.filePath = filePath;
|
|
35
|
+
this.rotatedPath = rotatedPath;
|
|
36
|
+
this.maxFileBytes = options.maxFileBytes ?? DEFAULT_JOURNAL_MAX_BYTES;
|
|
37
|
+
}
|
|
38
|
+
afterPublish(event) {
|
|
39
|
+
if (this.closed) return;
|
|
40
|
+
this.seq++;
|
|
41
|
+
const line = `${JSON.stringify({ seq: this.seq, event })}
|
|
42
|
+
`;
|
|
43
|
+
this.enqueue(line);
|
|
44
|
+
}
|
|
45
|
+
enqueue(line) {
|
|
46
|
+
this.writeChain = this.writeChain.then(async () => {
|
|
47
|
+
try {
|
|
48
|
+
await this.appendLine(line);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
this.warnOnce(err);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async appendLine(line) {
|
|
55
|
+
const size = Buffer.byteLength(line);
|
|
56
|
+
if (this.bytes === null) {
|
|
57
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
58
|
+
this.bytes = await currentSize(this.filePath);
|
|
59
|
+
}
|
|
60
|
+
if (this.maxFileBytes > 0 && this.bytes > 0 && this.bytes + size > this.maxFileBytes) {
|
|
61
|
+
await this.rotate();
|
|
62
|
+
}
|
|
63
|
+
await appendFile(this.filePath, line, "utf-8");
|
|
64
|
+
this.bytes += size;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Keep exactly one previous generation.
|
|
68
|
+
*
|
|
69
|
+
* Deliberately not a numbered cascade: two files bound the disk cost at
|
|
70
|
+
* `2 * maxFileBytes`, and a run long enough to fill 10 MB twice has already
|
|
71
|
+
* put everything a human would look for in the current file. A caller that
|
|
72
|
+
* needs full retention sets `maxFileBytes: 0` and rotates on its own terms.
|
|
73
|
+
*/
|
|
74
|
+
async rotate() {
|
|
75
|
+
try {
|
|
76
|
+
await unlink(this.rotatedPath);
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
await rename(this.filePath, this.rotatedPath);
|
|
80
|
+
this.bytes = 0;
|
|
81
|
+
}
|
|
82
|
+
async flush() {
|
|
83
|
+
await this.writeChain;
|
|
84
|
+
}
|
|
85
|
+
async close() {
|
|
86
|
+
await this.flush();
|
|
87
|
+
this.closed = true;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
async function currentSize(path) {
|
|
91
|
+
try {
|
|
92
|
+
return (await stat(path)).size;
|
|
93
|
+
} catch {
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function parseJournal(content) {
|
|
98
|
+
const entries = [];
|
|
99
|
+
for (const line of content.split("\n")) {
|
|
100
|
+
const trimmed = line.trim();
|
|
101
|
+
if (trimmed === "") continue;
|
|
102
|
+
let parsed;
|
|
103
|
+
try {
|
|
104
|
+
parsed = JSON.parse(trimmed);
|
|
105
|
+
} catch {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!isEntry(parsed)) continue;
|
|
109
|
+
entries.push(parsed);
|
|
110
|
+
}
|
|
111
|
+
return entries;
|
|
112
|
+
}
|
|
113
|
+
function isEntry(value) {
|
|
114
|
+
if (typeof value !== "object" || value === null) return false;
|
|
115
|
+
const candidate = value;
|
|
116
|
+
if (typeof candidate.seq !== "number") return false;
|
|
117
|
+
const event = candidate.event;
|
|
118
|
+
return typeof event === "object" && event !== null && typeof event.type === "string";
|
|
119
|
+
}
|
|
120
|
+
var MultiPublisher = class {
|
|
121
|
+
members;
|
|
122
|
+
primary;
|
|
123
|
+
constructor(members) {
|
|
124
|
+
if (members.length === 0) {
|
|
125
|
+
throw new Error("MultiPublisher needs at least one member");
|
|
126
|
+
}
|
|
127
|
+
this.members = members;
|
|
128
|
+
this.primary = members[0];
|
|
129
|
+
}
|
|
130
|
+
publish(event) {
|
|
131
|
+
for (const member of this.members) {
|
|
132
|
+
member.publish(event);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
snapshot() {
|
|
136
|
+
return this.primary.snapshot();
|
|
137
|
+
}
|
|
138
|
+
version() {
|
|
139
|
+
return this.primary.version();
|
|
140
|
+
}
|
|
141
|
+
async flush() {
|
|
142
|
+
await Promise.all(this.members.map((member) => member.flush()));
|
|
143
|
+
}
|
|
144
|
+
async close() {
|
|
145
|
+
await Promise.all(this.members.map((member) => member.close()));
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// src/core/pipeline.ts
|
|
150
|
+
var PIPELINE_PHASES = ["init", "prd", "plan", "execute", "review", "pr"];
|
|
151
|
+
var PIPELINE_PHASES_WITH_PR_REVIEW = [...PIPELINE_PHASES, "pr-review"];
|
|
152
|
+
var PIPELINE_PHASES_NO_BRANCH = PIPELINE_PHASES.filter(
|
|
153
|
+
(p) => p !== "pr"
|
|
154
|
+
);
|
|
155
|
+
var PHASE_TO_FIELD = {
|
|
156
|
+
init: null,
|
|
157
|
+
prd: "prdCompleted",
|
|
158
|
+
plan: "jsonCompleted",
|
|
159
|
+
execute: "executionCompleted",
|
|
160
|
+
review: "reviewCompleted",
|
|
161
|
+
pr: "prCreated",
|
|
162
|
+
"pr-review": "prReviewCompleted"
|
|
163
|
+
};
|
|
164
|
+
var PipelineManager = class {
|
|
165
|
+
tasksJsonPath;
|
|
166
|
+
plan;
|
|
167
|
+
activePhases;
|
|
168
|
+
constructor(plan, tasksJsonPath, activePhases) {
|
|
169
|
+
this.plan = plan;
|
|
170
|
+
this.tasksJsonPath = tasksJsonPath;
|
|
171
|
+
this.activePhases = activePhases ?? PIPELINE_PHASES;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Reload state from disk.
|
|
175
|
+
*/
|
|
176
|
+
async reload() {
|
|
177
|
+
this.plan = await loadTaskPlan(this.tasksJsonPath);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Check if a phase is complete.
|
|
181
|
+
*/
|
|
182
|
+
isPhaseComplete(phase) {
|
|
183
|
+
const field = PHASE_TO_FIELD[phase];
|
|
184
|
+
if (field === null) return true;
|
|
185
|
+
return this.plan.pipeline?.[field] ?? false;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get the first incomplete phase in the pipeline.
|
|
189
|
+
* Returns null if all phases are complete.
|
|
190
|
+
*/
|
|
191
|
+
getNextPhase() {
|
|
192
|
+
for (const phase of this.activePhases) {
|
|
193
|
+
if (!this.isPhaseComplete(phase)) {
|
|
194
|
+
return phase;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Get the current phase (alias for getNextPhase).
|
|
201
|
+
*/
|
|
202
|
+
getCurrentPhase() {
|
|
203
|
+
return this.getNextPhase();
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Check whether we can resume from a specific phase.
|
|
207
|
+
* All prerequisite phases must be complete.
|
|
208
|
+
*/
|
|
209
|
+
canResume(fromPhase) {
|
|
210
|
+
const idx = this.activePhases.indexOf(fromPhase);
|
|
211
|
+
if (idx < 0) return false;
|
|
212
|
+
for (let i = 0; i < idx; i++) {
|
|
213
|
+
if (!this.isPhaseComplete(this.activePhases[i])) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Mark a phase as complete and persist to disk.
|
|
221
|
+
*/
|
|
222
|
+
async markPhaseComplete(phase) {
|
|
223
|
+
const field = PHASE_TO_FIELD[phase];
|
|
224
|
+
if (field === null) return;
|
|
225
|
+
this.plan = {
|
|
226
|
+
...this.plan,
|
|
227
|
+
pipeline: {
|
|
228
|
+
...this.plan.pipeline ?? {
|
|
229
|
+
prdCompleted: false,
|
|
230
|
+
jsonCompleted: false,
|
|
231
|
+
executionCompleted: false,
|
|
232
|
+
reviewCompleted: false,
|
|
233
|
+
prCreated: false
|
|
234
|
+
},
|
|
235
|
+
[field]: true
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
await saveTaskPlan(this.tasksJsonPath, this.plan);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// src/execution/plan.ts
|
|
243
|
+
import { mkdir as mkdir2, readFile } from "fs/promises";
|
|
244
|
+
import { dirname as dirname2 } from "path";
|
|
245
|
+
import { ZodError } from "zod";
|
|
246
|
+
|
|
247
|
+
// src/issues/graph.ts
|
|
248
|
+
var DEFAULT_MAX_NODES = 25;
|
|
249
|
+
var DEFAULT_MAX_DEPTH = 3;
|
|
250
|
+
function dependencyEdges(graph) {
|
|
251
|
+
const edges = [];
|
|
252
|
+
const seen = /* @__PURE__ */ new Set();
|
|
253
|
+
const add = (from, to) => {
|
|
254
|
+
if (from === to || !graph.nodes.has(from) || !graph.nodes.has(to)) return;
|
|
255
|
+
const key = `${from}->${to}`;
|
|
256
|
+
if (seen.has(key)) return;
|
|
257
|
+
seen.add(key);
|
|
258
|
+
edges.push({ from, to });
|
|
259
|
+
};
|
|
260
|
+
for (const node of graph.nodes.values()) {
|
|
261
|
+
for (const blocker of node.relations.blockedBy) {
|
|
262
|
+
add(blocker, node.id);
|
|
263
|
+
}
|
|
264
|
+
for (const blocked of node.relations.blocking) {
|
|
265
|
+
add(node.id, blocked);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return edges;
|
|
269
|
+
}
|
|
270
|
+
function findCycles(graph) {
|
|
271
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
272
|
+
for (const { from, to } of dependencyEdges(graph)) {
|
|
273
|
+
const list = adjacency.get(from);
|
|
274
|
+
if (list === void 0) {
|
|
275
|
+
adjacency.set(from, [to]);
|
|
276
|
+
} else {
|
|
277
|
+
list.push(to);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const cycles = [];
|
|
281
|
+
const reported = /* @__PURE__ */ new Set();
|
|
282
|
+
const state = /* @__PURE__ */ new Map();
|
|
283
|
+
const stack = [];
|
|
284
|
+
const record = (cycle) => {
|
|
285
|
+
let pivot = 0;
|
|
286
|
+
for (let i = 1; i < cycle.length; i++) {
|
|
287
|
+
if (compareIds(cycle[i], cycle[pivot]) < 0) pivot = i;
|
|
288
|
+
}
|
|
289
|
+
const normalized = [...cycle.slice(pivot), ...cycle.slice(0, pivot)];
|
|
290
|
+
const key = normalized.join("->");
|
|
291
|
+
if (reported.has(key)) return;
|
|
292
|
+
reported.add(key);
|
|
293
|
+
cycles.push(normalized);
|
|
294
|
+
};
|
|
295
|
+
const visit = (id) => {
|
|
296
|
+
state.set(id, "visiting");
|
|
297
|
+
stack.push(id);
|
|
298
|
+
for (const next of adjacency.get(id) ?? []) {
|
|
299
|
+
const status = state.get(next);
|
|
300
|
+
if (status === "visiting") {
|
|
301
|
+
record(stack.slice(stack.indexOf(next)));
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (status === void 0) {
|
|
305
|
+
visit(next);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
stack.pop();
|
|
309
|
+
state.set(id, "done");
|
|
310
|
+
};
|
|
311
|
+
for (const id of graph.nodes.keys()) {
|
|
312
|
+
if (!state.has(id)) visit(id);
|
|
313
|
+
}
|
|
314
|
+
return cycles;
|
|
315
|
+
}
|
|
316
|
+
function compareIds(a, b) {
|
|
317
|
+
const na = Number(a);
|
|
318
|
+
const nb = Number(b);
|
|
319
|
+
if (Number.isInteger(na) && Number.isInteger(nb)) {
|
|
320
|
+
return na - nb;
|
|
321
|
+
}
|
|
322
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
323
|
+
}
|
|
324
|
+
function neighbours(relations) {
|
|
325
|
+
return uniqueIds(
|
|
326
|
+
[
|
|
327
|
+
...relations.parent === null ? [] : [relations.parent],
|
|
328
|
+
...relations.children,
|
|
329
|
+
...relations.blockedBy,
|
|
330
|
+
...relations.blocking
|
|
331
|
+
],
|
|
332
|
+
relations.id
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
async function buildDependencyGraph(roots, fetchRelations, options = {}) {
|
|
336
|
+
const maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES;
|
|
337
|
+
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
338
|
+
const warn = options.warn ?? (() => {
|
|
339
|
+
});
|
|
340
|
+
const known = /* @__PURE__ */ new Map();
|
|
341
|
+
for (const issue of options.known ?? []) {
|
|
342
|
+
known.set(issue.id, issue);
|
|
343
|
+
}
|
|
344
|
+
const rootIds = uniqueIds(roots);
|
|
345
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
346
|
+
const queued = new Set(rootIds);
|
|
347
|
+
let truncated = false;
|
|
348
|
+
let frontier = rootIds.map((id) => ({ id, depth: 0 }));
|
|
349
|
+
while (frontier.length > 0) {
|
|
350
|
+
const next = [];
|
|
351
|
+
for (const { id, depth } of frontier) {
|
|
352
|
+
if (nodes.size >= maxNodes) {
|
|
353
|
+
truncated = true;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
let relations = null;
|
|
357
|
+
try {
|
|
358
|
+
relations = await fetchRelations(id);
|
|
359
|
+
} catch (err) {
|
|
360
|
+
warn(
|
|
361
|
+
`Could not read the relations of issue '${id}': ${err instanceof Error ? err.message : String(err)}`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
let issue = known.get(id) ?? null;
|
|
365
|
+
if (issue === null && options.fetchIssue !== void 0) {
|
|
366
|
+
try {
|
|
367
|
+
issue = await options.fetchIssue(id);
|
|
368
|
+
} catch (err) {
|
|
369
|
+
warn(`Could not read issue '${id}': ${err instanceof Error ? err.message : String(err)}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const resolved = relations ?? emptyRelations(id);
|
|
373
|
+
nodes.set(id, { id, relations: resolved, depth, issue, root: rootIds.includes(id) });
|
|
374
|
+
if (depth >= maxDepth) {
|
|
375
|
+
if (neighbours(resolved).some((neighbour) => !queued.has(neighbour))) {
|
|
376
|
+
truncated = true;
|
|
377
|
+
}
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
for (const neighbour of neighbours(resolved)) {
|
|
381
|
+
if (queued.has(neighbour)) continue;
|
|
382
|
+
queued.add(neighbour);
|
|
383
|
+
next.push({ id: neighbour, depth: depth + 1 });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (nodes.size >= maxNodes && next.length > 0) {
|
|
387
|
+
truncated = true;
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
frontier = next;
|
|
391
|
+
}
|
|
392
|
+
const graph = {
|
|
393
|
+
roots: rootIds,
|
|
394
|
+
nodes,
|
|
395
|
+
cycles: [],
|
|
396
|
+
truncated,
|
|
397
|
+
limits: { maxNodes, maxDepth }
|
|
398
|
+
};
|
|
399
|
+
linkReferencedBy(graph);
|
|
400
|
+
graph.cycles = findCycles(graph);
|
|
401
|
+
return graph;
|
|
402
|
+
}
|
|
403
|
+
function linkReferencedBy(graph) {
|
|
404
|
+
for (const node of graph.nodes.values()) {
|
|
405
|
+
for (const cited of node.relations.references) {
|
|
406
|
+
const target = graph.nodes.get(cited);
|
|
407
|
+
if (target === void 0 || target.id === node.id) continue;
|
|
408
|
+
target.relations.referencedBy = uniqueIds(
|
|
409
|
+
[...target.relations.referencedBy, node.id],
|
|
410
|
+
target.id
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/execution/order.ts
|
|
417
|
+
var PRIORITY_LABELS = {
|
|
418
|
+
high: "high",
|
|
419
|
+
medium: "medium",
|
|
420
|
+
low: "low"
|
|
421
|
+
};
|
|
422
|
+
var PRIORITY_RANK = { high: 0, medium: 1, low: 2 };
|
|
423
|
+
var NO_PRIORITY_RANK = 3;
|
|
424
|
+
function priorityOf(labels) {
|
|
425
|
+
for (const label of labels) {
|
|
426
|
+
const normalized = label.trim().toLowerCase();
|
|
427
|
+
const direct = PRIORITY_LABELS[normalized];
|
|
428
|
+
if (direct !== void 0) return direct;
|
|
429
|
+
const qualified = /^(?:priority|prio|p)\s*[:/-]\s*(high|medium|low)$/.exec(normalized);
|
|
430
|
+
const matched = qualified?.[1];
|
|
431
|
+
if (matched !== void 0) return PRIORITY_LABELS[matched];
|
|
432
|
+
}
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
function hierarchyDepth(id, parentOf, selected) {
|
|
436
|
+
let depth = 0;
|
|
437
|
+
let current = parentOf.get(id) ?? null;
|
|
438
|
+
const seen = /* @__PURE__ */ new Set([id]);
|
|
439
|
+
while (current !== null && selected.has(current) && !seen.has(current)) {
|
|
440
|
+
depth++;
|
|
441
|
+
seen.add(current);
|
|
442
|
+
current = parentOf.get(current) ?? null;
|
|
443
|
+
}
|
|
444
|
+
return depth;
|
|
445
|
+
}
|
|
446
|
+
function computeExecutionOrder(graph, options = {}) {
|
|
447
|
+
const selected = new Set(
|
|
448
|
+
(options.include ?? [...graph.nodes.keys()]).filter((id) => graph.nodes.has(id))
|
|
449
|
+
);
|
|
450
|
+
const parentOf = /* @__PURE__ */ new Map();
|
|
451
|
+
const priority = /* @__PURE__ */ new Map();
|
|
452
|
+
for (const id of selected) {
|
|
453
|
+
const node = graph.nodes.get(id);
|
|
454
|
+
parentOf.set(id, node?.relations.parent ?? null);
|
|
455
|
+
priority.set(id, priorityOf(node?.issue?.labels ?? []));
|
|
456
|
+
}
|
|
457
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
458
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
459
|
+
for (const id of selected) {
|
|
460
|
+
incoming.set(id, /* @__PURE__ */ new Set());
|
|
461
|
+
outgoing.set(id, []);
|
|
462
|
+
}
|
|
463
|
+
for (const { from, to } of dependencyEdges(graph)) {
|
|
464
|
+
if (!selected.has(from) || !selected.has(to) || from === to) continue;
|
|
465
|
+
incoming.get(to)?.add(from);
|
|
466
|
+
outgoing.get(from)?.push(to);
|
|
467
|
+
}
|
|
468
|
+
const depth = /* @__PURE__ */ new Map();
|
|
469
|
+
for (const id of selected) {
|
|
470
|
+
depth.set(id, hierarchyDepth(id, parentOf, selected));
|
|
471
|
+
}
|
|
472
|
+
const rank = (id) => {
|
|
473
|
+
const value = priority.get(id) ?? null;
|
|
474
|
+
return value === null ? NO_PRIORITY_RANK : PRIORITY_RANK[value];
|
|
475
|
+
};
|
|
476
|
+
const better = (a, b) => {
|
|
477
|
+
const byHierarchy = (depth.get(a) ?? 0) - (depth.get(b) ?? 0);
|
|
478
|
+
if (byHierarchy !== 0) return byHierarchy;
|
|
479
|
+
const byPriority = rank(a) - rank(b);
|
|
480
|
+
if (byPriority !== 0) return byPriority;
|
|
481
|
+
return compareIds(a, b);
|
|
482
|
+
};
|
|
483
|
+
const ready = [...selected].filter((id) => (incoming.get(id)?.size ?? 0) === 0);
|
|
484
|
+
const order = [];
|
|
485
|
+
while (ready.length > 0) {
|
|
486
|
+
ready.sort(better);
|
|
487
|
+
const next = ready.shift();
|
|
488
|
+
order.push(next);
|
|
489
|
+
for (const dependent of outgoing.get(next) ?? []) {
|
|
490
|
+
const pending = incoming.get(dependent);
|
|
491
|
+
if (pending === void 0) continue;
|
|
492
|
+
pending.delete(next);
|
|
493
|
+
if (pending.size === 0) {
|
|
494
|
+
ready.push(dependent);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (order.length !== selected.size) {
|
|
499
|
+
const cycles = graph.cycles.length > 0 ? graph.cycles : findCycles(graph);
|
|
500
|
+
const relevant = cycles.filter((cycle) => cycle.every((id) => selected.has(id)));
|
|
501
|
+
return { ok: false, cycles: relevant.length > 0 ? relevant : cycles };
|
|
502
|
+
}
|
|
503
|
+
return { ok: true, order };
|
|
504
|
+
}
|
|
505
|
+
function describeCycles(cycles) {
|
|
506
|
+
return cycles.map((cycle) => [...cycle, cycle[0]].map((id) => `#${id}`).join(" \u2192 ")).join("; ");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/execution/plan.ts
|
|
510
|
+
function describeIssue(issue, id) {
|
|
511
|
+
const parsed = Number.parseInt(id, 10);
|
|
512
|
+
return {
|
|
513
|
+
number: issue?.number ?? (Number.isNaN(parsed) ? null : parsed),
|
|
514
|
+
title: issue?.title ?? "",
|
|
515
|
+
url: issue?.remoteRef ?? null,
|
|
516
|
+
source: issue?.source ?? "github"
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function buildExecutionPlan(input) {
|
|
520
|
+
const now = input.now ?? isoNow;
|
|
521
|
+
const at = now();
|
|
522
|
+
const scheduled = new Set(input.order);
|
|
523
|
+
const requested = new Set(input.requested);
|
|
524
|
+
const issues = input.order.map((id, index) => {
|
|
525
|
+
const node = input.graph.nodes.get(id);
|
|
526
|
+
const relations = node?.relations;
|
|
527
|
+
const described = describeIssue(node?.issue ?? null, id);
|
|
528
|
+
return {
|
|
529
|
+
id,
|
|
530
|
+
number: described.number,
|
|
531
|
+
title: described.title,
|
|
532
|
+
url: described.url,
|
|
533
|
+
source: described.source,
|
|
534
|
+
position: index + 1,
|
|
535
|
+
status: "pending",
|
|
536
|
+
origin: requested.has(id) ? "requested" : "discovered",
|
|
537
|
+
// Only the constraints that survive inside the queue: a blocker left out
|
|
538
|
+
// of it is reported in `excluded`, not enforced as a dependency.
|
|
539
|
+
dependsOn: (relations?.blockedBy ?? []).filter((blocker) => scheduled.has(blocker)),
|
|
540
|
+
parent: relations?.parent ?? null,
|
|
541
|
+
priority: priorityOf(node?.issue?.labels ?? []),
|
|
542
|
+
heuristic: (relations?.heuristic.length ?? 0) > 0,
|
|
543
|
+
failedPhase: null,
|
|
544
|
+
lastError: null,
|
|
545
|
+
attempts: 0,
|
|
546
|
+
blockedReason: null,
|
|
547
|
+
startedAt: null,
|
|
548
|
+
completedAt: null
|
|
549
|
+
};
|
|
550
|
+
});
|
|
551
|
+
const excluded = [];
|
|
552
|
+
for (const node of input.graph.nodes.values()) {
|
|
553
|
+
if (scheduled.has(node.id)) continue;
|
|
554
|
+
const described = describeIssue(node.issue, node.id);
|
|
555
|
+
excluded.push({
|
|
556
|
+
id: node.id,
|
|
557
|
+
number: described.number,
|
|
558
|
+
title: described.title,
|
|
559
|
+
url: described.url,
|
|
560
|
+
reason: "Discovered in the hierarchy but not selected for this run"
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
return {
|
|
564
|
+
schemaVersion: 1,
|
|
565
|
+
id: input.requested[0] ?? input.order[0],
|
|
566
|
+
project: input.projectId,
|
|
567
|
+
requested: [...input.requested],
|
|
568
|
+
branchName: null,
|
|
569
|
+
noBranch: input.noBranch ?? false,
|
|
570
|
+
prReview: input.prReview ?? false,
|
|
571
|
+
status: "pending",
|
|
572
|
+
createdAt: at,
|
|
573
|
+
updatedAt: at,
|
|
574
|
+
truncated: input.graph.truncated,
|
|
575
|
+
issues,
|
|
576
|
+
excluded
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
async function loadExecutionPlan(path) {
|
|
580
|
+
const raw = JSON.parse(await readFile(path, "utf-8"));
|
|
581
|
+
try {
|
|
582
|
+
return executionPlanSchema.parse(raw);
|
|
583
|
+
} catch (err) {
|
|
584
|
+
if (err instanceof ZodError) {
|
|
585
|
+
const issues = err.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
586
|
+
throw new Error(`Invalid execution-plan.json at ${path}:
|
|
587
|
+
${issues}`);
|
|
588
|
+
}
|
|
589
|
+
throw err;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
async function saveExecutionPlan(path, plan, now = isoNow) {
|
|
593
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
594
|
+
const stamped = { ...plan, updatedAt: now() };
|
|
595
|
+
await writeFileAtomic(path, `${JSON.stringify(stamped, null, 2)}
|
|
596
|
+
`);
|
|
597
|
+
}
|
|
598
|
+
function dependenciesSatisfied(plan, entry) {
|
|
599
|
+
return entry.dependsOn.every((id) => {
|
|
600
|
+
const dependency = plan.issues.find((candidate) => candidate.id === id);
|
|
601
|
+
return dependency === void 0 || dependency.status === "completed";
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
function nextQueueIssue(plan, options = {}) {
|
|
605
|
+
const exclude = options.exclude ?? /* @__PURE__ */ new Set();
|
|
606
|
+
const eligible = (status) => (entry) => entry.status === status && !exclude.has(entry.id) && dependenciesSatisfied(plan, entry);
|
|
607
|
+
return plan.issues.find(eligible("in_progress")) ?? plan.issues.find(eligible("failed")) ?? plan.issues.find(eligible("pending")) ?? plan.issues.find(eligible("skipped")) ?? null;
|
|
608
|
+
}
|
|
609
|
+
function queueStatus(plan) {
|
|
610
|
+
if (plan.issues.some((entry) => entry.status === "failed" || entry.status === "blocked")) {
|
|
611
|
+
return "failed";
|
|
612
|
+
}
|
|
613
|
+
if (plan.issues.every((entry) => entry.status === "completed")) return "completed";
|
|
614
|
+
if (plan.issues.some((entry) => entry.status !== "pending")) return "in_progress";
|
|
615
|
+
return "pending";
|
|
616
|
+
}
|
|
617
|
+
function isQueueComplete(plan) {
|
|
618
|
+
return plan.issues.every((entry) => entry.status === "completed");
|
|
619
|
+
}
|
|
620
|
+
function updateIssue(plan, id, patch) {
|
|
621
|
+
const issues = plan.issues.map((entry) => entry.id === id ? { ...entry, ...patch } : entry);
|
|
622
|
+
const next = { ...plan, issues };
|
|
623
|
+
return { ...next, status: queueStatus(next) };
|
|
624
|
+
}
|
|
625
|
+
function markQueueIssueInProgress(plan, id, now = isoNow) {
|
|
626
|
+
const current = plan.issues.find((entry) => entry.id === id);
|
|
627
|
+
return updateIssue(plan, id, {
|
|
628
|
+
status: "in_progress",
|
|
629
|
+
// Preserved across a resume: the queue reports when the work on this Issue
|
|
630
|
+
// really began, not when it was last picked up.
|
|
631
|
+
startedAt: current?.startedAt ?? now(),
|
|
632
|
+
failedPhase: null,
|
|
633
|
+
lastError: null
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
function markQueueIssueCompleted(plan, id, now = isoNow) {
|
|
637
|
+
return updateIssue(plan, id, {
|
|
638
|
+
status: "completed",
|
|
639
|
+
completedAt: now(),
|
|
640
|
+
failedPhase: null,
|
|
641
|
+
lastError: null
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
function markQueueIssueFailed(plan, id, failure) {
|
|
645
|
+
const current = plan.issues.find((entry) => entry.id === id);
|
|
646
|
+
return updateIssue(plan, id, {
|
|
647
|
+
status: "failed",
|
|
648
|
+
failedPhase: failure.phase,
|
|
649
|
+
lastError: failure.error,
|
|
650
|
+
attempts: (current?.attempts ?? 0) + 1
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
function markQueueIssueSkipped(plan, id, failure) {
|
|
654
|
+
const current = plan.issues.find((entry) => entry.id === id);
|
|
655
|
+
return updateIssue(plan, id, {
|
|
656
|
+
status: "skipped",
|
|
657
|
+
failedPhase: failure.phase,
|
|
658
|
+
lastError: failure.error,
|
|
659
|
+
attempts: (current?.attempts ?? 0) + 1
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
function markQueueIssueBlocked(plan, id, reason, failure) {
|
|
663
|
+
const current = plan.issues.find((entry) => entry.id === id);
|
|
664
|
+
return updateIssue(plan, id, {
|
|
665
|
+
status: "blocked",
|
|
666
|
+
blockedReason: reason,
|
|
667
|
+
failedPhase: failure.phase,
|
|
668
|
+
lastError: failure.error,
|
|
669
|
+
attempts: (current?.attempts ?? 0) + 1
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
function setQueueBranch(plan, branchName) {
|
|
673
|
+
return { ...plan, branchName };
|
|
674
|
+
}
|
|
675
|
+
function setQueuePullRequest(plan, pullRequest) {
|
|
676
|
+
return { ...plan, pullRequest };
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export {
|
|
680
|
+
JournalPublisher,
|
|
681
|
+
parseJournal,
|
|
682
|
+
MultiPublisher,
|
|
683
|
+
PIPELINE_PHASES,
|
|
684
|
+
PIPELINE_PHASES_WITH_PR_REVIEW,
|
|
685
|
+
PIPELINE_PHASES_NO_BRANCH,
|
|
686
|
+
PipelineManager,
|
|
687
|
+
buildDependencyGraph,
|
|
688
|
+
computeExecutionOrder,
|
|
689
|
+
describeCycles,
|
|
690
|
+
buildExecutionPlan,
|
|
691
|
+
loadExecutionPlan,
|
|
692
|
+
saveExecutionPlan,
|
|
693
|
+
nextQueueIssue,
|
|
694
|
+
isQueueComplete,
|
|
695
|
+
markQueueIssueInProgress,
|
|
696
|
+
markQueueIssueCompleted,
|
|
697
|
+
markQueueIssueFailed,
|
|
698
|
+
markQueueIssueSkipped,
|
|
699
|
+
markQueueIssueBlocked,
|
|
700
|
+
setQueueBranch,
|
|
701
|
+
setQueuePullRequest
|
|
702
|
+
};
|
|
703
|
+
//# sourceMappingURL=chunk-56KS3TFQ.js.map
|