issue-flow 0.3.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 +226 -0
- package/dist/analyze-YYQQP64A.js +12 -0
- package/dist/analyze-YYQQP64A.js.map +1 -0
- package/dist/chunk-64FU2L2T.js +180 -0
- package/dist/chunk-64FU2L2T.js.map +1 -0
- package/dist/chunk-7APADNBV.js +68 -0
- package/dist/chunk-7APADNBV.js.map +1 -0
- package/dist/chunk-7FRXZ2XA.js +78 -0
- package/dist/chunk-7FRXZ2XA.js.map +1 -0
- package/dist/chunk-D4WBCVB4.js +85 -0
- package/dist/chunk-D4WBCVB4.js.map +1 -0
- package/dist/chunk-G36DIQ4J.js +80 -0
- package/dist/chunk-G36DIQ4J.js.map +1 -0
- package/dist/chunk-JEUHCE3V.js +123 -0
- package/dist/chunk-JEUHCE3V.js.map +1 -0
- package/dist/chunk-L7RIGFP6.js +92 -0
- package/dist/chunk-L7RIGFP6.js.map +1 -0
- package/dist/chunk-OZVHOVDT.js +43 -0
- package/dist/chunk-OZVHOVDT.js.map +1 -0
- package/dist/chunk-PINC2LST.js +82 -0
- package/dist/chunk-PINC2LST.js.map +1 -0
- package/dist/chunk-V356G3JS.js +611 -0
- package/dist/chunk-V356G3JS.js.map +1 -0
- package/dist/chunk-ZOX7M2C2.js +85 -0
- package/dist/chunk-ZOX7M2C2.js.map +1 -0
- package/dist/cli.js +76 -0
- package/dist/cli.js.map +1 -0
- package/dist/execute-UF3IY2MG.js +11 -0
- package/dist/execute-UF3IY2MG.js.map +1 -0
- package/dist/generate-WMYJEPI4.js +48 -0
- package/dist/generate-WMYJEPI4.js.map +1 -0
- package/dist/init-QES2HLP5.js +9 -0
- package/dist/init-QES2HLP5.js.map +1 -0
- package/dist/plan-BKSUDN34.js +12 -0
- package/dist/plan-BKSUDN34.js.map +1 -0
- package/dist/pr-E2ASXBWE.js +12 -0
- package/dist/pr-E2ASXBWE.js.map +1 -0
- package/dist/prd-WYW35SGL.js +12 -0
- package/dist/prd-WYW35SGL.js.map +1 -0
- package/dist/review-YI2WPNC4.js +12 -0
- package/dist/review-YI2WPNC4.js.map +1 -0
- package/dist/run-J62FIB5O.js +288 -0
- package/dist/run-J62FIB5O.js.map +1 -0
- package/package.json +58 -0
- package/prompts/analyze.md +28 -0
- package/prompts/execute.md +124 -0
- package/prompts/generate.md +19 -0
- package/prompts/plan.md +49 -0
- package/prompts/pr.md +19 -0
- package/prompts/prd.md +30 -0
- package/prompts/review.md +27 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
allStoriesPass,
|
|
4
|
+
clearLastError,
|
|
5
|
+
initializeState,
|
|
6
|
+
isoNow,
|
|
7
|
+
loadTaskPlan,
|
|
8
|
+
markIssueCompleted,
|
|
9
|
+
markIssueInProgress,
|
|
10
|
+
saveTaskPlan,
|
|
11
|
+
setLastError,
|
|
12
|
+
trimErrorMessage
|
|
13
|
+
} from "./chunk-64FU2L2T.js";
|
|
14
|
+
import {
|
|
15
|
+
applyPlaceholders,
|
|
16
|
+
loadPrompt
|
|
17
|
+
} from "./chunk-OZVHOVDT.js";
|
|
18
|
+
import {
|
|
19
|
+
getIcons,
|
|
20
|
+
getTermWidth,
|
|
21
|
+
printError,
|
|
22
|
+
printInfo,
|
|
23
|
+
printRetry,
|
|
24
|
+
printSuccess,
|
|
25
|
+
printWarning
|
|
26
|
+
} from "./chunk-L7RIGFP6.js";
|
|
27
|
+
|
|
28
|
+
// src/config.ts
|
|
29
|
+
import { platform } from "os";
|
|
30
|
+
import { join } from "path";
|
|
31
|
+
|
|
32
|
+
// src/utils/shell.ts
|
|
33
|
+
import { execa } from "execa";
|
|
34
|
+
async function run(command, args = [], options) {
|
|
35
|
+
const result = await execa(command, args, {
|
|
36
|
+
reject: false,
|
|
37
|
+
...options
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
stdout: result.stdout?.toString() ?? "",
|
|
41
|
+
stderr: result.stderr?.toString() ?? "",
|
|
42
|
+
exitCode: result.exitCode ?? 1
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/utils/git.ts
|
|
47
|
+
async function getProjectRoot() {
|
|
48
|
+
const result = await run("git", ["rev-parse", "--show-toplevel"]);
|
|
49
|
+
if (result.exitCode !== 0) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
"Not inside a git repository. Please run issue-flow from within a git project."
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return result.stdout.trim();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/config.ts
|
|
58
|
+
var DEFAULTS = {
|
|
59
|
+
retryLimit: 10,
|
|
60
|
+
retryForever: false,
|
|
61
|
+
backoffBaseSeconds: 30,
|
|
62
|
+
backoffMaxSeconds: 900
|
|
63
|
+
};
|
|
64
|
+
function createConfig(options) {
|
|
65
|
+
return {
|
|
66
|
+
issueNumber: options.issueNumber,
|
|
67
|
+
maxIterations: options.maxIterations,
|
|
68
|
+
retryLimit: options.retryLimit ?? DEFAULTS.retryLimit,
|
|
69
|
+
retryForever: options.retryForever ?? DEFAULTS.retryForever,
|
|
70
|
+
backoffBaseSeconds: options.backoffBaseSeconds ?? DEFAULTS.backoffBaseSeconds,
|
|
71
|
+
backoffMaxSeconds: options.backoffMaxSeconds ?? DEFAULTS.backoffMaxSeconds
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async function resolvePaths(config, scriptDir) {
|
|
75
|
+
const projectRoot = await getProjectRoot();
|
|
76
|
+
if (config.issueNumber) {
|
|
77
|
+
const issueDir = join(projectRoot, "issues", config.issueNumber);
|
|
78
|
+
return {
|
|
79
|
+
prdFile: join(issueDir, "tasks.json"),
|
|
80
|
+
progressFile: join(issueDir, "progress.txt"),
|
|
81
|
+
archiveDir: join(issueDir, "archive"),
|
|
82
|
+
lastBranchFile: join(issueDir, ".last-branch"),
|
|
83
|
+
projectRoot
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const base = scriptDir ?? projectRoot;
|
|
87
|
+
return {
|
|
88
|
+
prdFile: join(base, "prd.json"),
|
|
89
|
+
progressFile: join(base, "progress.txt"),
|
|
90
|
+
archiveDir: join(base, "archive"),
|
|
91
|
+
lastBranchFile: join(base, ".last-branch"),
|
|
92
|
+
projectRoot
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function getInstallHint(pkg) {
|
|
96
|
+
const os = platform();
|
|
97
|
+
if (os === "darwin") {
|
|
98
|
+
return `brew install ${pkg}`;
|
|
99
|
+
}
|
|
100
|
+
if (os === "linux") {
|
|
101
|
+
return `apt install ${pkg} (or your distro's package manager)`;
|
|
102
|
+
}
|
|
103
|
+
if (os === "win32") {
|
|
104
|
+
return `winget install ${pkg} (or choco install ${pkg})`;
|
|
105
|
+
}
|
|
106
|
+
return `install ${pkg} using your system package manager`;
|
|
107
|
+
}
|
|
108
|
+
async function validateDependencies() {
|
|
109
|
+
const errors = [];
|
|
110
|
+
const gitResult = await run("git", ["--version"]);
|
|
111
|
+
if (gitResult.exitCode !== 0) {
|
|
112
|
+
errors.push(` - git (install with: ${getInstallHint("git")})`);
|
|
113
|
+
}
|
|
114
|
+
const claudeResult = await run("claude", ["--version"]);
|
|
115
|
+
if (claudeResult.exitCode !== 0) {
|
|
116
|
+
errors.push(" - claude (install with: npm install -g @anthropic-ai/claude-code)");
|
|
117
|
+
}
|
|
118
|
+
return errors;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/core/engine.ts
|
|
122
|
+
import { existsSync } from "fs";
|
|
123
|
+
import { cp, mkdir, readFile, writeFile } from "fs/promises";
|
|
124
|
+
import { join as join2 } from "path";
|
|
125
|
+
|
|
126
|
+
// src/ui/progress.ts
|
|
127
|
+
import chalk from "chalk";
|
|
128
|
+
function useColor() {
|
|
129
|
+
if (process.env.NO_COLOR === "1") return false;
|
|
130
|
+
if (!process.stdout.isTTY) return false;
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
function useUnicode() {
|
|
134
|
+
if (process.env.NO_COLOR === "1") return false;
|
|
135
|
+
if (!process.stdout.isTTY) return false;
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
function printProgressBar(passed, total) {
|
|
139
|
+
const barWidth = 20;
|
|
140
|
+
let pct = 0;
|
|
141
|
+
let filled = 0;
|
|
142
|
+
if (total > 0) {
|
|
143
|
+
pct = Math.floor(passed * 100 / total);
|
|
144
|
+
filled = Math.floor(passed * barWidth / total);
|
|
145
|
+
}
|
|
146
|
+
const empty = barWidth - filled;
|
|
147
|
+
const fillChar = useUnicode() ? "\u2588" : "#";
|
|
148
|
+
const emptyChar = useUnicode() ? "\u2591" : "-";
|
|
149
|
+
const bar = fillChar.repeat(filled) + emptyChar.repeat(empty);
|
|
150
|
+
const text = `${passed}/${total} (${pct}%)`;
|
|
151
|
+
if (useColor()) {
|
|
152
|
+
return `${chalk.green(bar)} ${text}`;
|
|
153
|
+
}
|
|
154
|
+
return `${bar} ${text}`;
|
|
155
|
+
}
|
|
156
|
+
function printIterationHeader(iteration, maxIter, stories) {
|
|
157
|
+
const icons = getIcons();
|
|
158
|
+
const colored = useColor();
|
|
159
|
+
const iterLabel = maxIter ? `Iteration ${iteration} of ${maxIter}` : `Iteration ${iteration}`;
|
|
160
|
+
const total = stories.length;
|
|
161
|
+
const passed = stories.filter((s) => s.passes).length;
|
|
162
|
+
console.log("");
|
|
163
|
+
if (colored) {
|
|
164
|
+
console.log(chalk.blue(`\u2501\u2501\u2501 ${icons.start} ${iterLabel} \u2501\u2501\u2501`));
|
|
165
|
+
} else {
|
|
166
|
+
console.log(`--- ${icons.start} ${iterLabel} ---`);
|
|
167
|
+
}
|
|
168
|
+
console.log("");
|
|
169
|
+
let foundFirstPending = false;
|
|
170
|
+
for (const story of stories) {
|
|
171
|
+
let icon;
|
|
172
|
+
let colorFn;
|
|
173
|
+
if (story.passes) {
|
|
174
|
+
icon = icons.success;
|
|
175
|
+
colorFn = colored ? chalk.green : (s) => s;
|
|
176
|
+
} else if (!foundFirstPending) {
|
|
177
|
+
icon = icons.pending;
|
|
178
|
+
colorFn = colored ? chalk.yellow : (s) => s;
|
|
179
|
+
foundFirstPending = true;
|
|
180
|
+
} else {
|
|
181
|
+
icon = icons.notReached;
|
|
182
|
+
colorFn = colored ? chalk.gray : (s) => s;
|
|
183
|
+
}
|
|
184
|
+
console.log(colorFn(` ${icon} ${story.id}: ${story.title}`));
|
|
185
|
+
}
|
|
186
|
+
console.log("");
|
|
187
|
+
console.log(` ${printProgressBar(passed, total)}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/ui/summary.ts
|
|
191
|
+
import chalk2 from "chalk";
|
|
192
|
+
function useColor2() {
|
|
193
|
+
if (process.env.NO_COLOR != null && process.env.NO_COLOR !== "") return false;
|
|
194
|
+
if (!process.stdout.isTTY) return false;
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
function useUnicode2() {
|
|
198
|
+
if (process.env.NO_COLOR != null && process.env.NO_COLOR !== "") return false;
|
|
199
|
+
if (!process.stdout.isTTY) return false;
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
function fitLine(text, width) {
|
|
203
|
+
if (text.length > width) {
|
|
204
|
+
return text.substring(0, width);
|
|
205
|
+
}
|
|
206
|
+
return text.padEnd(width);
|
|
207
|
+
}
|
|
208
|
+
function printBox(lines) {
|
|
209
|
+
const colored = useColor2();
|
|
210
|
+
const unicode = useUnicode2();
|
|
211
|
+
const termWidth = getTermWidth();
|
|
212
|
+
let maxContentWidth = 0;
|
|
213
|
+
for (const line of lines) {
|
|
214
|
+
if (line !== "---" && line.length > maxContentWidth) {
|
|
215
|
+
maxContentWidth = line.length;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const available = termWidth - 4;
|
|
219
|
+
if (maxContentWidth > available) {
|
|
220
|
+
maxContentWidth = available;
|
|
221
|
+
}
|
|
222
|
+
if (maxContentWidth < 20) {
|
|
223
|
+
maxContentWidth = 20;
|
|
224
|
+
}
|
|
225
|
+
const tl = unicode ? "\u256D" : "+";
|
|
226
|
+
const tr = unicode ? "\u256E" : "+";
|
|
227
|
+
const bl = unicode ? "\u2570" : "+";
|
|
228
|
+
const br = unicode ? "\u256F" : "+";
|
|
229
|
+
const h = unicode ? "\u2500" : "-";
|
|
230
|
+
const v = unicode ? "\u2502" : "|";
|
|
231
|
+
const sepL = unicode ? "\u251C" : "+";
|
|
232
|
+
const sepR = unicode ? "\u2524" : "+";
|
|
233
|
+
const hrule = h.repeat(maxContentWidth + 2);
|
|
234
|
+
const blue = colored ? chalk2.blue : (s) => s;
|
|
235
|
+
const _reset = (s) => s;
|
|
236
|
+
console.log(blue(`${tl}${hrule}${tr}`));
|
|
237
|
+
for (const line of lines) {
|
|
238
|
+
if (line === "---") {
|
|
239
|
+
console.log(blue(`${sepL}${hrule}${sepR}`));
|
|
240
|
+
} else {
|
|
241
|
+
const fitted = fitLine(line, maxContentWidth);
|
|
242
|
+
console.log(`${blue(v)} ${fitted} ${blue(v)}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
console.log(blue(`${bl}${hrule}${br}`));
|
|
246
|
+
}
|
|
247
|
+
function printStartupHeader(config, plan) {
|
|
248
|
+
const icons = getIcons();
|
|
249
|
+
const storiesTotal = plan.userStories.length;
|
|
250
|
+
const storiesPassing = plan.userStories.filter((s) => s.passes).length;
|
|
251
|
+
const branchName = plan.branchName ?? "N/A";
|
|
252
|
+
const issueLabel = config.issueNumber ? `Issue #${config.issueNumber}` : "Standalone mode";
|
|
253
|
+
const maxIterLabel = config.maxIterations !== void 0 ? String(config.maxIterations) : "unlimited";
|
|
254
|
+
const retryLabel = config.retryForever ? "unlimited retries" : `${config.retryLimit} consecutive retries`;
|
|
255
|
+
printBox([
|
|
256
|
+
`${icons.start} Issue Flow`,
|
|
257
|
+
"---",
|
|
258
|
+
`Issue: ${issueLabel}`,
|
|
259
|
+
`Branch: ${branchName}`,
|
|
260
|
+
`Stories: ${storiesPassing}/${storiesTotal} passing`,
|
|
261
|
+
`Iterations: ${maxIterLabel}`,
|
|
262
|
+
`Retries: ${retryLabel}`
|
|
263
|
+
]);
|
|
264
|
+
}
|
|
265
|
+
function formatDuration(totalSeconds) {
|
|
266
|
+
const mins = Math.floor(totalSeconds / 60);
|
|
267
|
+
const secs = totalSeconds % 60;
|
|
268
|
+
if (mins > 0) {
|
|
269
|
+
return `${mins}m ${secs}s`;
|
|
270
|
+
}
|
|
271
|
+
return `${secs}s`;
|
|
272
|
+
}
|
|
273
|
+
function printSummaryBox(status, iterations, totalRetries, elapsedSeconds, plan, extraInfo) {
|
|
274
|
+
const icons = getIcons();
|
|
275
|
+
const storiesTotal = plan.userStories.length;
|
|
276
|
+
const storiesPassing = plan.userStories.filter((s) => s.passes).length;
|
|
277
|
+
const duration = formatDuration(elapsedSeconds);
|
|
278
|
+
let statusIcon;
|
|
279
|
+
let statusLabel;
|
|
280
|
+
switch (status) {
|
|
281
|
+
case "success":
|
|
282
|
+
statusIcon = icons.success;
|
|
283
|
+
statusLabel = "Completed";
|
|
284
|
+
break;
|
|
285
|
+
case "incomplete":
|
|
286
|
+
statusIcon = icons.warn;
|
|
287
|
+
statusLabel = "Incomplete";
|
|
288
|
+
break;
|
|
289
|
+
case "failed":
|
|
290
|
+
statusIcon = icons.fail;
|
|
291
|
+
statusLabel = "Failed";
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
const boxLines = [
|
|
295
|
+
`${icons.end} Issue Flow Summary`,
|
|
296
|
+
"---",
|
|
297
|
+
`Status: ${statusIcon} ${statusLabel}`,
|
|
298
|
+
`Stories: ${storiesPassing}/${storiesTotal} passing`,
|
|
299
|
+
`Iterations: ${iterations}`,
|
|
300
|
+
`Duration: ${duration}`,
|
|
301
|
+
`Retries: ${totalRetries}`
|
|
302
|
+
];
|
|
303
|
+
if (extraInfo) {
|
|
304
|
+
boxLines.push("---");
|
|
305
|
+
boxLines.push(extraInfo);
|
|
306
|
+
}
|
|
307
|
+
console.log("");
|
|
308
|
+
printBox(boxLines);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/utils/retry.ts
|
|
312
|
+
var TRANSIENT_PATTERNS = [
|
|
313
|
+
"timed out",
|
|
314
|
+
"timeout",
|
|
315
|
+
"connection reset",
|
|
316
|
+
"connection refused",
|
|
317
|
+
"connection aborted",
|
|
318
|
+
"network error",
|
|
319
|
+
"network unavailable",
|
|
320
|
+
"temporary failure",
|
|
321
|
+
"temporarily unavailable",
|
|
322
|
+
"service unavailable",
|
|
323
|
+
"overloaded",
|
|
324
|
+
"rate limit",
|
|
325
|
+
"too many requests",
|
|
326
|
+
"bad gateway",
|
|
327
|
+
"gateway timeout",
|
|
328
|
+
"internal server error",
|
|
329
|
+
"http 429",
|
|
330
|
+
"http 500",
|
|
331
|
+
"http 502",
|
|
332
|
+
"http 503",
|
|
333
|
+
"http 504",
|
|
334
|
+
"econnreset",
|
|
335
|
+
"econnrefused",
|
|
336
|
+
"enotfound",
|
|
337
|
+
"etimedout",
|
|
338
|
+
"socket hang up"
|
|
339
|
+
];
|
|
340
|
+
function isTransientFailure(exitCode, output) {
|
|
341
|
+
if (exitCode === 75) {
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
const lowered = output.toLowerCase();
|
|
345
|
+
return TRANSIENT_PATTERNS.some((pattern) => lowered.includes(pattern));
|
|
346
|
+
}
|
|
347
|
+
function retryDelaySeconds(attempt, baseSeconds = 30, maxSeconds = 900) {
|
|
348
|
+
const delay = baseSeconds * 2 ** (attempt - 1);
|
|
349
|
+
return Math.min(delay, maxSeconds);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/core/executor.ts
|
|
353
|
+
import { execa as execa2 } from "execa";
|
|
354
|
+
async function executeClaude(prompt) {
|
|
355
|
+
const result = await execa2("claude", ["--dangerously-skip-permissions", "--print"], {
|
|
356
|
+
input: prompt,
|
|
357
|
+
reject: false,
|
|
358
|
+
timeout: 0,
|
|
359
|
+
// No timeout — let the engine handle iteration limits
|
|
360
|
+
stripFinalNewline: false
|
|
361
|
+
});
|
|
362
|
+
const stdout = result.stdout?.toString() ?? "";
|
|
363
|
+
const stderr = result.stderr?.toString() ?? "";
|
|
364
|
+
const output = stdout + (stderr ? `
|
|
365
|
+
${stderr}` : "");
|
|
366
|
+
if (output.trim()) {
|
|
367
|
+
process.stderr.write(`${output}
|
|
368
|
+
`);
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
exitCode: result.exitCode ?? 1,
|
|
372
|
+
output
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/core/engine.ts
|
|
377
|
+
function sleep(seconds) {
|
|
378
|
+
return new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
|
|
379
|
+
}
|
|
380
|
+
async function ensureProgressFile(progressFile) {
|
|
381
|
+
if (!existsSync(progressFile)) {
|
|
382
|
+
const content = `# Issue Flow Progress Log
|
|
383
|
+
Started: ${(/* @__PURE__ */ new Date()).toString()}
|
|
384
|
+
---
|
|
385
|
+
`;
|
|
386
|
+
await writeFile(progressFile, content, "utf-8");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async function archiveIfBranchChanged(plan, paths) {
|
|
390
|
+
const { lastBranchFile, archiveDir, prdFile, progressFile } = paths;
|
|
391
|
+
if (!existsSync(lastBranchFile)) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const currentBranch = plan.branchName ?? "";
|
|
395
|
+
let lastBranch = "";
|
|
396
|
+
try {
|
|
397
|
+
lastBranch = (await readFile(lastBranchFile, "utf-8")).trim();
|
|
398
|
+
} catch {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (currentBranch && lastBranch && currentBranch !== lastBranch) {
|
|
402
|
+
const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
403
|
+
const folderName = lastBranch.replace(/^issue\//, "").replace(/[<>:"|?*\\]/g, "_");
|
|
404
|
+
const archiveFolder = join2(archiveDir, `${dateStr}-${folderName}`);
|
|
405
|
+
printInfo(`Archiving previous run: ${lastBranch}`);
|
|
406
|
+
await mkdir(archiveFolder, { recursive: true });
|
|
407
|
+
if (existsSync(prdFile)) {
|
|
408
|
+
await cp(prdFile, join2(archiveFolder, "tasks.json"));
|
|
409
|
+
}
|
|
410
|
+
if (existsSync(progressFile)) {
|
|
411
|
+
await cp(progressFile, join2(archiveFolder, "progress.txt"));
|
|
412
|
+
}
|
|
413
|
+
printInfo(` Archived to: ${archiveFolder}`);
|
|
414
|
+
await writeFile(
|
|
415
|
+
progressFile,
|
|
416
|
+
`# Issue Flow Progress Log
|
|
417
|
+
Started: ${(/* @__PURE__ */ new Date()).toString()}
|
|
418
|
+
---
|
|
419
|
+
`,
|
|
420
|
+
"utf-8"
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
async function trackBranch(plan, lastBranchFile) {
|
|
425
|
+
const branch = plan.branchName ?? "";
|
|
426
|
+
if (branch) {
|
|
427
|
+
await writeFile(lastBranchFile, `${branch}
|
|
428
|
+
`, "utf-8");
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async function runEngine(config, paths) {
|
|
432
|
+
if (!existsSync(paths.prdFile)) {
|
|
433
|
+
printError(`PRD file not found at ${paths.prdFile}`);
|
|
434
|
+
if (config.issueNumber) {
|
|
435
|
+
console.log(`Have you run the resolve-issue skill for issue #${config.issueNumber} first?`);
|
|
436
|
+
}
|
|
437
|
+
return 1;
|
|
438
|
+
}
|
|
439
|
+
let plan = await loadTaskPlan(paths.prdFile);
|
|
440
|
+
plan = initializeState(plan);
|
|
441
|
+
await saveTaskPlan(paths.prdFile, plan);
|
|
442
|
+
if (plan.issueStatus === "completed" && allStoriesPass(plan)) {
|
|
443
|
+
console.log(`Issue already marked complete in ${paths.prdFile}`);
|
|
444
|
+
return 0;
|
|
445
|
+
}
|
|
446
|
+
if (plan.issueStatus === "completed" && !allStoriesPass(plan)) {
|
|
447
|
+
printWarning(
|
|
448
|
+
"Issue marked completed but some stories are still pending. Resetting to in_progress."
|
|
449
|
+
);
|
|
450
|
+
plan = markIssueInProgress(plan);
|
|
451
|
+
plan = setLastError(
|
|
452
|
+
plan,
|
|
453
|
+
"invalid_completion_state",
|
|
454
|
+
"tasks.json claimed the issue was completed before every story had passes=true."
|
|
455
|
+
);
|
|
456
|
+
await saveTaskPlan(paths.prdFile, plan);
|
|
457
|
+
}
|
|
458
|
+
if (allStoriesPass(plan)) {
|
|
459
|
+
console.log("All user stories already pass. Marking issue as completed.");
|
|
460
|
+
plan = markIssueCompleted(plan);
|
|
461
|
+
await saveTaskPlan(paths.prdFile, plan);
|
|
462
|
+
return 0;
|
|
463
|
+
}
|
|
464
|
+
await archiveIfBranchChanged(plan, paths);
|
|
465
|
+
await trackBranch(plan, paths.lastBranchFile);
|
|
466
|
+
await ensureProgressFile(paths.progressFile);
|
|
467
|
+
const promptTemplate = await loadPrompt("execute");
|
|
468
|
+
printStartupHeader(config, plan);
|
|
469
|
+
const startTime = Date.now();
|
|
470
|
+
let i = 0;
|
|
471
|
+
let retryCount = 0;
|
|
472
|
+
let totalRetryCount = 0;
|
|
473
|
+
while (true) {
|
|
474
|
+
if (config.maxIterations !== void 0 && i >= config.maxIterations) {
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
i++;
|
|
478
|
+
plan = await loadTaskPlan(paths.prdFile);
|
|
479
|
+
printIterationHeader(i, config.maxIterations, plan.userStories);
|
|
480
|
+
const prompt = applyPlaceholders(promptTemplate, {
|
|
481
|
+
__PRD_FILE__: paths.prdFile,
|
|
482
|
+
__PROGRESS_FILE__: paths.progressFile
|
|
483
|
+
});
|
|
484
|
+
const iterationStartedAt = isoNow();
|
|
485
|
+
plan = markIssueInProgress(plan, iterationStartedAt);
|
|
486
|
+
await saveTaskPlan(paths.prdFile, plan);
|
|
487
|
+
const result = await executeClaude(prompt);
|
|
488
|
+
if (result.exitCode !== 0) {
|
|
489
|
+
const errorMessage = trimErrorMessage(result.output);
|
|
490
|
+
if (isTransientFailure(result.exitCode, result.output)) {
|
|
491
|
+
retryCount++;
|
|
492
|
+
totalRetryCount++;
|
|
493
|
+
plan = await loadTaskPlan(paths.prdFile);
|
|
494
|
+
plan = setLastError(plan, "transient_claude_failure", errorMessage);
|
|
495
|
+
await saveTaskPlan(paths.prdFile, plan);
|
|
496
|
+
if (!config.retryForever && retryCount > config.retryLimit) {
|
|
497
|
+
const elapsed3 = Math.floor((Date.now() - startTime) / 1e3);
|
|
498
|
+
plan = await loadTaskPlan(paths.prdFile);
|
|
499
|
+
printSummaryBox(
|
|
500
|
+
"failed",
|
|
501
|
+
i,
|
|
502
|
+
totalRetryCount,
|
|
503
|
+
elapsed3,
|
|
504
|
+
plan,
|
|
505
|
+
`Exceeded retry limit (${config.retryLimit}) on transient errors`
|
|
506
|
+
);
|
|
507
|
+
return result.exitCode;
|
|
508
|
+
}
|
|
509
|
+
const delaySeconds = retryDelaySeconds(
|
|
510
|
+
retryCount,
|
|
511
|
+
config.backoffBaseSeconds,
|
|
512
|
+
config.backoffMaxSeconds
|
|
513
|
+
);
|
|
514
|
+
console.log("");
|
|
515
|
+
printRetry(
|
|
516
|
+
`Transient Claude failure on iteration ${i} (attempt ${retryCount}). Retrying in ${delaySeconds}s.`
|
|
517
|
+
);
|
|
518
|
+
i--;
|
|
519
|
+
await sleep(delaySeconds);
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
plan = await loadTaskPlan(paths.prdFile);
|
|
523
|
+
plan = setLastError(plan, "fatal_claude_failure", errorMessage);
|
|
524
|
+
await saveTaskPlan(paths.prdFile, plan);
|
|
525
|
+
const elapsed2 = Math.floor((Date.now() - startTime) / 1e3);
|
|
526
|
+
printSummaryBox(
|
|
527
|
+
"failed",
|
|
528
|
+
i,
|
|
529
|
+
totalRetryCount,
|
|
530
|
+
elapsed2,
|
|
531
|
+
plan,
|
|
532
|
+
`Claude CLI failed with exit code ${result.exitCode}`
|
|
533
|
+
);
|
|
534
|
+
return result.exitCode;
|
|
535
|
+
}
|
|
536
|
+
retryCount = 0;
|
|
537
|
+
plan = await loadTaskPlan(paths.prdFile);
|
|
538
|
+
plan = clearLastError(plan, iterationStartedAt);
|
|
539
|
+
await saveTaskPlan(paths.prdFile, plan);
|
|
540
|
+
if (result.output.includes("<promise>COMPLETE</promise>")) {
|
|
541
|
+
plan = await loadTaskPlan(paths.prdFile);
|
|
542
|
+
if (allStoriesPass(plan)) {
|
|
543
|
+
plan = markIssueCompleted(plan);
|
|
544
|
+
await saveTaskPlan(paths.prdFile, plan);
|
|
545
|
+
const elapsed2 = Math.floor((Date.now() - startTime) / 1e3);
|
|
546
|
+
printSummaryBox("success", i, totalRetryCount, elapsed2, plan);
|
|
547
|
+
return 0;
|
|
548
|
+
}
|
|
549
|
+
plan = setLastError(
|
|
550
|
+
plan,
|
|
551
|
+
"invalid_completion_signal",
|
|
552
|
+
"Claude returned <promise>COMPLETE</promise> before every story had passes=true."
|
|
553
|
+
);
|
|
554
|
+
await saveTaskPlan(paths.prdFile, plan);
|
|
555
|
+
console.log("");
|
|
556
|
+
printWarning(
|
|
557
|
+
"Claude returned a completion signal, but tasks.json still has pending stories. Ignoring completion and continuing."
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
printSuccess(`Iteration ${i} complete. Continuing...`);
|
|
561
|
+
await sleep(2);
|
|
562
|
+
}
|
|
563
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1e3);
|
|
564
|
+
plan = await loadTaskPlan(paths.prdFile);
|
|
565
|
+
printSummaryBox(
|
|
566
|
+
"incomplete",
|
|
567
|
+
config.maxIterations ?? i,
|
|
568
|
+
totalRetryCount,
|
|
569
|
+
elapsed,
|
|
570
|
+
plan,
|
|
571
|
+
"Reached max iterations without completing all tasks."
|
|
572
|
+
);
|
|
573
|
+
return 1;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/commands/execute.ts
|
|
577
|
+
async function runExecute(positionalMaxIter, options) {
|
|
578
|
+
const errors = await validateDependencies();
|
|
579
|
+
if (errors.length > 0) {
|
|
580
|
+
printError("The following required tools are not installed:");
|
|
581
|
+
for (const err of errors) {
|
|
582
|
+
console.log(err);
|
|
583
|
+
}
|
|
584
|
+
return 1;
|
|
585
|
+
}
|
|
586
|
+
const maxIterations = options.maxIterations ?? positionalMaxIter;
|
|
587
|
+
const config = createConfig({
|
|
588
|
+
issueNumber: options.issue,
|
|
589
|
+
maxIterations,
|
|
590
|
+
retryLimit: options.retryLimit,
|
|
591
|
+
retryForever: options.retryForever
|
|
592
|
+
});
|
|
593
|
+
const paths = await resolvePaths(config);
|
|
594
|
+
const exitCode = await runEngine(config, paths);
|
|
595
|
+
if (exitCode === 0 && config.issueNumber) {
|
|
596
|
+
try {
|
|
597
|
+
const plan = await loadTaskPlan(paths.prdFile);
|
|
598
|
+
if (allStoriesPass(plan)) {
|
|
599
|
+
plan.pipeline.executionCompleted = true;
|
|
600
|
+
await saveTaskPlan(paths.prdFile, plan);
|
|
601
|
+
}
|
|
602
|
+
} catch {
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return exitCode;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export {
|
|
609
|
+
runExecute
|
|
610
|
+
};
|
|
611
|
+
//# sourceMappingURL=chunk-V356G3JS.js.map
|