issue-flow 0.4.3 → 0.5.1

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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -1
  3. package/dist/{chunk-MEB3B2FI.js → analyze-IJ2SYXVM.js} +7 -10
  4. package/dist/{chunk-MEB3B2FI.js.map → analyze-IJ2SYXVM.js.map} +1 -1
  5. package/dist/{chunk-4OQ2ARLP.js → chunk-2XWXVWVD.js} +10 -2
  6. package/dist/chunk-2XWXVWVD.js.map +1 -0
  7. package/dist/{chunk-MWUIIUAS.js → chunk-AY7UZAGX.js} +8 -10
  8. package/dist/{chunk-MWUIIUAS.js.map → chunk-AY7UZAGX.js.map} +1 -1
  9. package/dist/chunk-FNLXZXWP.js +846 -0
  10. package/dist/chunk-FNLXZXWP.js.map +1 -0
  11. package/dist/{chunk-HYP64MZI.js → chunk-GYR4YHEB.js} +110 -116
  12. package/dist/chunk-GYR4YHEB.js.map +1 -0
  13. package/dist/{chunk-CYWHYPTU.js → chunk-MKQIP7YW.js} +7 -9
  14. package/dist/{chunk-CYWHYPTU.js.map → chunk-MKQIP7YW.js.map} +1 -1
  15. package/dist/{chunk-CSSXTPWE.js → chunk-Q2TQCJDN.js} +2 -2
  16. package/dist/{chunk-2JZCWJEQ.js → chunk-SI4ALORB.js} +7 -21
  17. package/dist/chunk-SI4ALORB.js.map +1 -0
  18. package/dist/{chunk-OZVHOVDT.js → chunk-SLHZPB3C.js} +15 -8
  19. package/dist/chunk-SLHZPB3C.js.map +1 -0
  20. package/dist/chunk-TYOFZ6DD.js +240 -0
  21. package/dist/chunk-TYOFZ6DD.js.map +1 -0
  22. package/dist/{chunk-4PZXIOXD.js → chunk-XJJP4TSK.js} +7 -9
  23. package/dist/{chunk-4PZXIOXD.js.map → chunk-XJJP4TSK.js.map} +1 -1
  24. package/dist/cli.js +56 -19
  25. package/dist/cli.js.map +1 -1
  26. package/dist/execute-DALVYZIH.js +11 -0
  27. package/dist/{generate-HP4CPMJG.js → generate-5OWWNRIB.js} +4 -4
  28. package/dist/init-6AWQI3IE.js +9 -0
  29. package/dist/plan-RQXK2OCP.js +11 -0
  30. package/dist/pr-UU4H7ZZR.js +11 -0
  31. package/dist/prd-WB3QH6QF.js +11 -0
  32. package/dist/review-2WHYJG7M.js +11 -0
  33. package/dist/run-UCSNYSQO.js +756 -0
  34. package/dist/run-UCSNYSQO.js.map +1 -0
  35. package/package.json +13 -3
  36. package/prompts/prd.md +5 -4
  37. package/web/public/app.css +481 -0
  38. package/web/public/app.js +522 -0
  39. package/web/public/index.html +113 -0
  40. package/dist/analyze-3SN7ZAZU.js +0 -12
  41. package/dist/chunk-2JZCWJEQ.js.map +0 -1
  42. package/dist/chunk-4OQ2ARLP.js.map +0 -1
  43. package/dist/chunk-64FU2L2T.js +0 -180
  44. package/dist/chunk-64FU2L2T.js.map +0 -1
  45. package/dist/chunk-HYP64MZI.js.map +0 -1
  46. package/dist/chunk-OZVHOVDT.js.map +0 -1
  47. package/dist/chunk-YAFHVFV5.js +0 -199
  48. package/dist/chunk-YAFHVFV5.js.map +0 -1
  49. package/dist/execute-GIA4IKS5.js +0 -11
  50. package/dist/init-SXFGOCML.js +0 -9
  51. package/dist/plan-HWZRG5GP.js +0 -12
  52. package/dist/pr-TTOFYUCV.js +0 -12
  53. package/dist/prd-ET2QN7GB.js +0 -12
  54. package/dist/review-3QDZCOMT.js +0 -12
  55. package/dist/review-3QDZCOMT.js.map +0 -1
  56. package/dist/run-PKHPULYL.js +0 -451
  57. package/dist/run-PKHPULYL.js.map +0 -1
  58. /package/dist/{chunk-CSSXTPWE.js.map → chunk-Q2TQCJDN.js.map} +0 -0
  59. /package/dist/{analyze-3SN7ZAZU.js.map → execute-DALVYZIH.js.map} +0 -0
  60. /package/dist/{generate-HP4CPMJG.js.map → generate-5OWWNRIB.js.map} +0 -0
  61. /package/dist/{execute-GIA4IKS5.js.map → init-6AWQI3IE.js.map} +0 -0
  62. /package/dist/{init-SXFGOCML.js.map → plan-RQXK2OCP.js.map} +0 -0
  63. /package/dist/{plan-HWZRG5GP.js.map → pr-UU4H7ZZR.js.map} +0 -0
  64. /package/dist/{pr-TTOFYUCV.js.map → prd-WB3QH6QF.js.map} +0 -0
  65. /package/dist/{prd-ET2QN7GB.js.map → review-2WHYJG7M.js.map} +0 -0
@@ -0,0 +1,756 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runInit
4
+ } from "./chunk-Q2TQCJDN.js";
5
+ import {
6
+ publishGitState,
7
+ runExecute
8
+ } from "./chunk-GYR4YHEB.js";
9
+ import {
10
+ loadWebConfig
11
+ } from "./chunk-TYOFZ6DD.js";
12
+ import {
13
+ runPlan
14
+ } from "./chunk-AY7UZAGX.js";
15
+ import {
16
+ runPr
17
+ } from "./chunk-MKQIP7YW.js";
18
+ import {
19
+ runPrd
20
+ } from "./chunk-SI4ALORB.js";
21
+ import {
22
+ runReview
23
+ } from "./chunk-XJJP4TSK.js";
24
+ import "./chunk-2XWXVWVD.js";
25
+ import {
26
+ resolvePackageDir
27
+ } from "./chunk-SLHZPB3C.js";
28
+ import {
29
+ FilePublisher,
30
+ NullPublisher,
31
+ formatDuration,
32
+ isVerbose,
33
+ isoNow,
34
+ loadTaskPlan,
35
+ printError,
36
+ printInfo,
37
+ printSuccess,
38
+ printWarning,
39
+ saveTaskPlan,
40
+ setOutputCallback,
41
+ setSessionPublisher,
42
+ setStoryUpdateCallback
43
+ } from "./chunk-FNLXZXWP.js";
44
+
45
+ // src/commands/run.ts
46
+ import { randomUUID } from "crypto";
47
+ import { join as join2 } from "path";
48
+ import { execa } from "execa";
49
+
50
+ // src/core/pipeline.ts
51
+ var PIPELINE_PHASES = ["init", "prd", "plan", "execute", "review", "pr"];
52
+ var PIPELINE_PHASES_NO_BRANCH = PIPELINE_PHASES.filter(
53
+ (p) => p !== "pr"
54
+ );
55
+ var PHASE_TO_FIELD = {
56
+ init: null,
57
+ prd: "prdCompleted",
58
+ plan: "jsonCompleted",
59
+ execute: "executionCompleted",
60
+ review: "reviewCompleted",
61
+ pr: "prCreated"
62
+ };
63
+ var PipelineManager = class {
64
+ tasksJsonPath;
65
+ plan;
66
+ activePhases;
67
+ constructor(plan, tasksJsonPath, activePhases) {
68
+ this.plan = plan;
69
+ this.tasksJsonPath = tasksJsonPath;
70
+ this.activePhases = activePhases ?? PIPELINE_PHASES;
71
+ }
72
+ /**
73
+ * Reload state from disk.
74
+ */
75
+ async reload() {
76
+ this.plan = await loadTaskPlan(this.tasksJsonPath);
77
+ }
78
+ /**
79
+ * Check if a phase is complete.
80
+ */
81
+ isPhaseComplete(phase) {
82
+ const field = PHASE_TO_FIELD[phase];
83
+ if (field === null) return true;
84
+ return this.plan.pipeline?.[field] ?? false;
85
+ }
86
+ /**
87
+ * Get the first incomplete phase in the pipeline.
88
+ * Returns null if all phases are complete.
89
+ */
90
+ getNextPhase() {
91
+ for (const phase of this.activePhases) {
92
+ if (!this.isPhaseComplete(phase)) {
93
+ return phase;
94
+ }
95
+ }
96
+ return null;
97
+ }
98
+ /**
99
+ * Get the current phase (alias for getNextPhase).
100
+ */
101
+ getCurrentPhase() {
102
+ return this.getNextPhase();
103
+ }
104
+ /**
105
+ * Check whether we can resume from a specific phase.
106
+ * All prerequisite phases must be complete.
107
+ */
108
+ canResume(fromPhase) {
109
+ const idx = this.activePhases.indexOf(fromPhase);
110
+ if (idx < 0) return false;
111
+ for (let i = 0; i < idx; i++) {
112
+ if (!this.isPhaseComplete(this.activePhases[i])) {
113
+ return false;
114
+ }
115
+ }
116
+ return true;
117
+ }
118
+ /**
119
+ * Mark a phase as complete and persist to disk.
120
+ */
121
+ async markPhaseComplete(phase) {
122
+ const field = PHASE_TO_FIELD[phase];
123
+ if (field === null) return;
124
+ this.plan = {
125
+ ...this.plan,
126
+ pipeline: {
127
+ ...this.plan.pipeline ?? {
128
+ prdCompleted: false,
129
+ jsonCompleted: false,
130
+ executionCompleted: false,
131
+ reviewCompleted: false,
132
+ prCreated: false
133
+ },
134
+ [field]: true
135
+ }
136
+ };
137
+ await saveTaskPlan(this.tasksJsonPath, this.plan);
138
+ }
139
+ };
140
+
141
+ // src/ui/pipeline-renderer.ts
142
+ import { Listr, PRESET_TIMER, PRESET_TIMESTAMP } from "listr2";
143
+ var PHASE_LABELS = {
144
+ prd: "PRD",
145
+ plan: "Plan",
146
+ execute: "Execute",
147
+ review: "Review",
148
+ pr: "PR"
149
+ };
150
+ function selectRenderer(verbose) {
151
+ const isTTY = !!process.stdout.isTTY;
152
+ const isCI = !!process.env.CI;
153
+ if (!isTTY || isCI) {
154
+ return "simple";
155
+ }
156
+ return verbose ? "verbose" : "default";
157
+ }
158
+ function buildExecutePhaseTask(runner, tasksPath, verbose) {
159
+ return async (_ctx, task) => {
160
+ let stories;
161
+ try {
162
+ const plan = await loadTaskPlan(tasksPath);
163
+ stories = [...plan.userStories].sort((a, b) => a.priority - b.priority);
164
+ } catch {
165
+ setOutputCallback((line) => {
166
+ task.output = line;
167
+ });
168
+ try {
169
+ await runner();
170
+ } finally {
171
+ setOutputCallback(void 0);
172
+ }
173
+ return;
174
+ }
175
+ const totalStories = stories.length;
176
+ const initialPassed = stories.filter((s) => s.passes).length;
177
+ task.title = `Execute (${initialPassed}/${totalStories} stories passing)`;
178
+ const resolvers = /* @__PURE__ */ new Map();
179
+ setStoryUpdateCallback((updatedStories) => {
180
+ const passed = updatedStories.filter((s) => s.passes).length;
181
+ task.title = `Execute (${passed}/${totalStories} stories passing)`;
182
+ for (const s of updatedStories) {
183
+ if (s.passes && resolvers.has(s.id)) {
184
+ resolvers.get(s.id).resolve();
185
+ resolvers.delete(s.id);
186
+ }
187
+ }
188
+ });
189
+ const subtaskDefs = stories.map((story) => ({
190
+ title: `${story.id}: ${story.title}`,
191
+ skip: story.passes ? "already passing" : false,
192
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
193
+ task: async () => {
194
+ await new Promise((resolve, reject) => {
195
+ resolvers.set(story.id, { resolve, reject });
196
+ });
197
+ },
198
+ rendererOptions: {
199
+ timer: PRESET_TIMER
200
+ }
201
+ }));
202
+ const engineSubtask = {
203
+ title: "Running engine",
204
+ task: async (_c, engineTask) => {
205
+ setOutputCallback((line) => {
206
+ engineTask.output = line;
207
+ });
208
+ try {
209
+ await runner();
210
+ } finally {
211
+ setOutputCallback(void 0);
212
+ setStoryUpdateCallback(void 0);
213
+ for (const [, { resolve }] of resolvers) {
214
+ resolve();
215
+ }
216
+ resolvers.clear();
217
+ }
218
+ },
219
+ rendererOptions: {
220
+ timer: PRESET_TIMER,
221
+ outputBar: verbose ? Infinity : false,
222
+ persistentOutput: false,
223
+ bottomBar: verbose ? Infinity : 3
224
+ }
225
+ };
226
+ return task.newListr([engineSubtask, ...subtaskDefs], {
227
+ concurrent: true,
228
+ exitOnError: false,
229
+ rendererOptions: {
230
+ timer: PRESET_TIMER,
231
+ collapseSkips: false
232
+ }
233
+ });
234
+ };
235
+ }
236
+ async function runPipelineWithRenderer(options) {
237
+ const { phases, startIndex, verbose, runners, tasksPath } = options;
238
+ const overallStart = Date.now();
239
+ let currentPhase;
240
+ const renderer = selectRenderer(verbose);
241
+ const isSimple = renderer === "simple";
242
+ const tasks = new Listr(
243
+ phases.map((phase, index) => {
244
+ const label = PHASE_LABELS[phase] ?? phase.charAt(0).toUpperCase() + phase.slice(1);
245
+ const isExecutePhase = phase === "execute" && tasksPath;
246
+ return {
247
+ title: label,
248
+ skip: index < startIndex ? "skipped" : false,
249
+ task: isExecutePhase ? async (_ctx, task) => {
250
+ currentPhase = phase;
251
+ const runner = runners[phase];
252
+ if (!runner) {
253
+ throw new Error(`No runner defined for phase: ${phase}`);
254
+ }
255
+ return buildExecutePhaseTask(runner, tasksPath, verbose)(_ctx, task);
256
+ } : async (_ctx, task) => {
257
+ currentPhase = phase;
258
+ const runner = runners[phase];
259
+ if (!runner) {
260
+ throw new Error(`No runner defined for phase: ${phase}`);
261
+ }
262
+ setOutputCallback((line) => {
263
+ task.output = line;
264
+ });
265
+ try {
266
+ await runner();
267
+ } finally {
268
+ setOutputCallback(void 0);
269
+ }
270
+ },
271
+ rendererOptions: {
272
+ timer: PRESET_TIMER,
273
+ outputBar: verbose ? Infinity : false,
274
+ persistentOutput: false
275
+ }
276
+ };
277
+ }),
278
+ {
279
+ renderer,
280
+ rendererOptions: {
281
+ timer: PRESET_TIMER,
282
+ collapseSkips: false,
283
+ ...isSimple ? { timestamp: PRESET_TIMESTAMP } : {}
284
+ },
285
+ exitOnError: true
286
+ }
287
+ );
288
+ let failedPhase;
289
+ try {
290
+ await tasks.run();
291
+ } catch {
292
+ failedPhase = currentPhase ?? phases[startIndex];
293
+ } finally {
294
+ setOutputCallback(void 0);
295
+ setStoryUpdateCallback(void 0);
296
+ }
297
+ const overallElapsedSeconds = Math.floor((Date.now() - overallStart) / 1e3);
298
+ return {
299
+ success: !failedPhase,
300
+ failedPhase,
301
+ overallElapsedSeconds
302
+ };
303
+ }
304
+
305
+ // src/web/server.ts
306
+ import { readFile } from "fs/promises";
307
+ import { createServer } from "http";
308
+ import { createRequire } from "module";
309
+ import { join } from "path";
310
+ import chalk from "chalk";
311
+ var require2 = createRequire(import.meta.url);
312
+ function readPackageVersion() {
313
+ try {
314
+ return require2("../../package.json").version;
315
+ } catch {
316
+ return "0.0.0";
317
+ }
318
+ }
319
+ var JSON_TYPE = "application/json; charset=utf-8";
320
+ var STATIC_ROUTES = {
321
+ "/": { file: "index.html", contentType: "text/html; charset=utf-8" },
322
+ "/app.css": { file: "app.css", contentType: "text/css; charset=utf-8" },
323
+ "/app.js": { file: "app.js", contentType: "text/javascript; charset=utf-8" }
324
+ };
325
+ async function loadStaticAssets(publicDir) {
326
+ const assets = /* @__PURE__ */ new Map();
327
+ if (publicDir === null) return assets;
328
+ for (const [route, { file, contentType }] of Object.entries(STATIC_ROUTES)) {
329
+ try {
330
+ const body = await readFile(join(publicDir, file), "utf-8");
331
+ assets.set(route, { body, contentType });
332
+ } catch {
333
+ }
334
+ }
335
+ return assets;
336
+ }
337
+ function baseHeaders(res) {
338
+ res.setHeader("Cache-Control", "no-store");
339
+ res.setHeader("X-Content-Type-Options", "nosniff");
340
+ res.setHeader("Referrer-Policy", "no-referrer");
341
+ res.setHeader("X-Robots-Tag", "noindex, nofollow");
342
+ }
343
+ function respond(res, status, contentType, body) {
344
+ res.statusCode = status;
345
+ res.setHeader("Content-Type", contentType);
346
+ res.end(body);
347
+ }
348
+ function respondJson(res, status, payload) {
349
+ respond(res, status, JSON_TYPE, JSON.stringify(payload));
350
+ }
351
+ async function startWebServer(options) {
352
+ const info = options.info ?? printInfo;
353
+ const warn = options.warn ?? printWarning;
354
+ const version = options.version ?? readPackageVersion();
355
+ const startedAtMs = Date.now();
356
+ const assets = await loadStaticAssets(
357
+ options.publicDir ?? resolvePackageDir(join("web", "public"))
358
+ );
359
+ let cached = null;
360
+ const statusPayload = () => {
361
+ const v = options.publisher.version();
362
+ if (cached === null || cached.version !== v) {
363
+ cached = { version: v, body: JSON.stringify(options.publisher.snapshot()), etag: `"${v}"` };
364
+ }
365
+ return cached;
366
+ };
367
+ const handleRequest = (req, res) => {
368
+ baseHeaders(res);
369
+ const path = new URL(req.url ?? "/", "http://localhost").pathname;
370
+ if (req.method !== "GET" && req.method !== "HEAD") {
371
+ respondJson(res, 404, { error: "Not found" });
372
+ return;
373
+ }
374
+ if (path === "/api/status" || path === "/status.json") {
375
+ const { body, etag } = statusPayload();
376
+ res.setHeader("ETag", etag);
377
+ if (req.headers["if-none-match"] === etag) {
378
+ res.statusCode = 304;
379
+ res.end();
380
+ return;
381
+ }
382
+ respond(res, 200, JSON_TYPE, body);
383
+ return;
384
+ }
385
+ if (path === "/api/sessions") {
386
+ const snapshot = options.publisher.snapshot();
387
+ respondJson(res, 200, [
388
+ {
389
+ sessionId: snapshot.sessionId,
390
+ issueNumber: snapshot.issue.number,
391
+ status: snapshot.status,
392
+ startedAt: snapshot.startedAt,
393
+ updatedAt: snapshot.updatedAt,
394
+ statusUrl: "/api/status"
395
+ }
396
+ ]);
397
+ return;
398
+ }
399
+ if (path === "/api/health") {
400
+ respondJson(res, 200, {
401
+ ok: true,
402
+ uptime: Math.round((Date.now() - startedAtMs) / 1e3),
403
+ version,
404
+ refreshSeconds: options.refreshSeconds ?? 5
405
+ });
406
+ return;
407
+ }
408
+ const asset = assets.get(path);
409
+ if (asset) {
410
+ respond(res, 200, asset.contentType, asset.body);
411
+ return;
412
+ }
413
+ respondJson(res, 404, { error: "Not found" });
414
+ };
415
+ const server = createServer((req, res) => {
416
+ try {
417
+ handleRequest(req, res);
418
+ } catch (err) {
419
+ try {
420
+ respondJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
421
+ } catch {
422
+ }
423
+ }
424
+ });
425
+ const listening = await new Promise((resolve) => {
426
+ const onError = (err) => {
427
+ if (err.code === "EADDRINUSE") {
428
+ warn(
429
+ `Web monitor port ${options.port} is already in use. Continuing without the web server.`
430
+ );
431
+ } else {
432
+ warn(`Web monitor failed to start (${err.message}). Continuing without the web server.`);
433
+ }
434
+ resolve(false);
435
+ };
436
+ server.once("error", onError);
437
+ server.listen(options.port, options.host, () => {
438
+ server.removeListener("error", onError);
439
+ resolve(true);
440
+ });
441
+ });
442
+ if (!listening) return null;
443
+ server.unref();
444
+ server.on("error", (err) => {
445
+ warn(`Web monitor server error: ${err.message}`);
446
+ });
447
+ const address = server.address();
448
+ const port = address.port;
449
+ const displayHost = options.host === "0.0.0.0" ? "localhost" : options.host;
450
+ const url = `http://${displayHost}:${port}`;
451
+ if (options.host === "0.0.0.0") {
452
+ warn("Web monitor bound to 0.0.0.0: anyone on your local network can view the session state.");
453
+ }
454
+ info(`Web monitor running at ${chalk.bold.cyan(url)}`);
455
+ let closed = false;
456
+ let closePromise = null;
457
+ const doClose = () => new Promise((resolve) => {
458
+ process.removeListener("SIGINT", onSignal);
459
+ process.removeListener("SIGTERM", onSignal);
460
+ server.close(() => resolve());
461
+ server.closeAllConnections();
462
+ });
463
+ const close = () => {
464
+ if (closed) return closePromise ?? Promise.resolve();
465
+ closed = true;
466
+ closePromise = doClose();
467
+ return closePromise;
468
+ };
469
+ const onSignal = (signal) => {
470
+ void close().finally(() => {
471
+ process.kill(process.pid, signal);
472
+ });
473
+ };
474
+ process.once("SIGINT", onSignal);
475
+ process.once("SIGTERM", onSignal);
476
+ return { server, host: options.host, port, url, close };
477
+ }
478
+
479
+ // src/commands/run.ts
480
+ var RUNNABLE_PHASES = ["prd", "plan", "execute", "review", "pr"];
481
+ var RUNNABLE_PHASES_NO_BRANCH = ["prd", "plan", "execute", "review"];
482
+ async function runPipeline(issue, mode, from, noBranch) {
483
+ const issueNumber = issue.replace(/^#/, "");
484
+ const issueDir = join2("issues", issueNumber);
485
+ const webConfig = await loadWebConfig();
486
+ const publisher = webConfig.enabled ? new FilePublisher(join2(issueDir, "session.json"), {
487
+ logLimit: webConfig.logLimit,
488
+ includeLogs: webConfig.includeLogs
489
+ }) : new NullPublisher();
490
+ setSessionPublisher(publisher);
491
+ let webServer = null;
492
+ if (webConfig.enabled) {
493
+ webServer = await startWebServer({
494
+ publisher,
495
+ port: webConfig.port,
496
+ host: webConfig.host,
497
+ refreshSeconds: webConfig.refreshSeconds
498
+ });
499
+ }
500
+ let exitCode = 1;
501
+ try {
502
+ exitCode = await runPipelinePhases(issueNumber, issueDir, mode, publisher, from, noBranch);
503
+ return exitCode;
504
+ } finally {
505
+ publisher.publish({
506
+ type: "session:end",
507
+ at: isoNow(),
508
+ status: exitCode === 0 ? "completed" : "failed"
509
+ });
510
+ await webServer?.close();
511
+ await publisher.close();
512
+ setSessionPublisher(void 0);
513
+ }
514
+ }
515
+ async function runPipelinePhases(issueNumber, issueDir, mode, publisher, from, noBranch) {
516
+ const tasksPath = join2(issueDir, "tasks.json");
517
+ const sessionId = randomUUID();
518
+ const publishSessionStart = (phases, at, info) => {
519
+ publisher.publish({
520
+ type: "session:start",
521
+ at,
522
+ sessionId,
523
+ issueNumber: Number.parseInt(issueNumber, 10) || 0,
524
+ issueUrl: info?.issueUrl,
525
+ branch: info?.branch,
526
+ phases: [...phases],
527
+ environment: { node: process.version, platform: process.platform }
528
+ });
529
+ };
530
+ printInfo(`Starting pipeline for issue #${issueNumber} (mode: ${mode})`);
531
+ printInfo("Running prerequisite checks...");
532
+ const sessionStartedAt = isoNow();
533
+ const initCode = await runInit();
534
+ if (initCode !== 0) {
535
+ publishSessionStart(PIPELINE_PHASES, sessionStartedAt);
536
+ publisher.publish({ type: "phase:start", at: sessionStartedAt, phase: "init" });
537
+ publisher.publish({
538
+ type: "phase:end",
539
+ at: isoNow(),
540
+ phase: "init",
541
+ success: false,
542
+ error: "Prerequisites not met"
543
+ });
544
+ printError("Prerequisites not met. Fix the issues above and try again.");
545
+ return 1;
546
+ }
547
+ let effectiveNoBranch = noBranch ?? false;
548
+ let planIssueUrl;
549
+ let planBranch;
550
+ try {
551
+ const existingPlan = await loadTaskPlan(tasksPath);
552
+ const persistedNoBranch = existingPlan.noBranch ?? false;
553
+ planIssueUrl = existingPlan.issueUrl || void 0;
554
+ planBranch = existingPlan.branchName || void 0;
555
+ if (noBranch !== void 0 && noBranch !== persistedNoBranch) {
556
+ if (persistedNoBranch) {
557
+ printWarning(
558
+ "This pipeline was started with --no-branch. Ignoring current flag; using persisted mode."
559
+ );
560
+ } else {
561
+ printWarning(
562
+ "This pipeline was started without --no-branch. Ignoring current flag; using persisted mode."
563
+ );
564
+ }
565
+ }
566
+ effectiveNoBranch = persistedNoBranch;
567
+ } catch {
568
+ }
569
+ const activePhases = effectiveNoBranch ? PIPELINE_PHASES_NO_BRANCH : PIPELINE_PHASES;
570
+ const phaseOrder = effectiveNoBranch ? RUNNABLE_PHASES_NO_BRANCH : RUNNABLE_PHASES;
571
+ publishSessionStart(activePhases, sessionStartedAt, {
572
+ issueUrl: planIssueUrl,
573
+ branch: planBranch
574
+ });
575
+ publisher.publish({ type: "phase:start", at: sessionStartedAt, phase: "init" });
576
+ publisher.publish({ type: "phase:end", at: isoNow(), phase: "init", success: true });
577
+ await publishGitState(publisher);
578
+ let startPhase = "prd";
579
+ if (from) {
580
+ if (!activePhases.includes(from)) {
581
+ const validPhases = activePhases.filter((p) => p !== "init").join(", ");
582
+ if (effectiveNoBranch && PIPELINE_PHASES.includes(from)) {
583
+ printError(
584
+ `The '${from}' phase is not available in --no-branch mode. Valid phases: ${validPhases}`
585
+ );
586
+ } else {
587
+ printError(`Invalid phase: ${from}. Valid phases: ${validPhases}`);
588
+ }
589
+ return 1;
590
+ }
591
+ startPhase = from;
592
+ } else {
593
+ try {
594
+ const plan = await loadTaskPlan(tasksPath);
595
+ const mgr = new PipelineManager(plan, tasksPath, activePhases);
596
+ const nextPhase = mgr.getNextPhase();
597
+ if (nextPhase && nextPhase !== "init") {
598
+ startPhase = nextPhase;
599
+ printInfo(`Resuming from phase: ${startPhase}`);
600
+ }
601
+ } catch {
602
+ }
603
+ }
604
+ if (from) {
605
+ try {
606
+ const plan = await loadTaskPlan(tasksPath);
607
+ const mgr = new PipelineManager(plan, tasksPath, activePhases);
608
+ if (!mgr.canResume(startPhase)) {
609
+ printError(`Cannot resume from ${startPhase}: prerequisite phases not complete`);
610
+ return 1;
611
+ }
612
+ } catch {
613
+ if (startPhase !== "prd") {
614
+ printError(`Cannot resume from ${startPhase}: no pipeline state found`);
615
+ return 1;
616
+ }
617
+ }
618
+ }
619
+ const startIdx = phaseOrder.indexOf(startPhase);
620
+ const makeRunner = (fn, phase) => async () => {
621
+ const code = await fn();
622
+ if (code !== 0) {
623
+ throw new Error(`Phase ${phase} failed with exit code ${code}`);
624
+ }
625
+ };
626
+ const runners = {
627
+ prd: makeRunner(() => runPrd(issueNumber), "prd"),
628
+ plan: async () => {
629
+ await makeRunner(() => runPlan(issueNumber), "plan")();
630
+ if (effectiveNoBranch) {
631
+ try {
632
+ const plan = await loadTaskPlan(tasksPath);
633
+ plan.noBranch = true;
634
+ await saveTaskPlan(tasksPath, plan);
635
+ } catch {
636
+ }
637
+ }
638
+ },
639
+ execute: makeRunner(() => runExecute(void 0, { issue: issueNumber }), "execute"),
640
+ review: async () => {
641
+ let maxCycles = 3;
642
+ try {
643
+ const plan = await loadTaskPlan(tasksPath);
644
+ maxCycles = plan.maxCorrectionCycles;
645
+ } catch {
646
+ }
647
+ let code = await runReview(issueNumber);
648
+ let cycle = 0;
649
+ while (code !== 0 && cycle < maxCycles) {
650
+ cycle++;
651
+ printWarning(`Review failed. Starting correction cycle ${cycle}/${maxCycles}...`);
652
+ publisher.publish({ type: "correction:cycle", at: isoNow(), cycle, maxCycles });
653
+ try {
654
+ const plan = await loadTaskPlan(tasksPath);
655
+ plan.correctionCycle = cycle;
656
+ await saveTaskPlan(tasksPath, plan);
657
+ } catch {
658
+ }
659
+ const execCode = await runExecute(void 0, { issue: issueNumber });
660
+ if (execCode !== 0) {
661
+ throw new Error("Correction execution failed");
662
+ }
663
+ code = await runReview(issueNumber);
664
+ }
665
+ if (code !== 0) {
666
+ throw new Error(`Review failed after ${maxCycles} correction cycles`);
667
+ }
668
+ },
669
+ pr: makeRunner(() => runPr(issueNumber), "pr")
670
+ };
671
+ const instrumentedRunners = Object.fromEntries(
672
+ Object.entries(runners).map(([phase, fn]) => [
673
+ phase,
674
+ async () => {
675
+ publisher.publish({ type: "phase:start", at: isoNow(), phase });
676
+ try {
677
+ await fn();
678
+ await publishGitState(publisher);
679
+ publisher.publish({ type: "phase:end", at: isoNow(), phase, success: true });
680
+ } catch (err) {
681
+ await publishGitState(publisher);
682
+ publisher.publish({
683
+ type: "phase:end",
684
+ at: isoNow(),
685
+ phase,
686
+ success: false,
687
+ error: err instanceof Error ? err.message : String(err)
688
+ });
689
+ throw err;
690
+ }
691
+ }
692
+ ])
693
+ );
694
+ const result = await runPipelineWithRenderer({
695
+ phases: phaseOrder,
696
+ startIndex: startIdx,
697
+ verbose: isVerbose(),
698
+ runners: instrumentedRunners,
699
+ tasksPath
700
+ });
701
+ if (!result.success) {
702
+ printError(`Phase ${result.failedPhase} failed`);
703
+ return 1;
704
+ }
705
+ printInfo("Closing issue...");
706
+ try {
707
+ await execa("gh", ["issue", "close", issueNumber], { reject: false });
708
+ } catch {
709
+ printWarning("Failed to close issue automatically");
710
+ }
711
+ let prUrl = "unknown";
712
+ if (!effectiveNoBranch) {
713
+ try {
714
+ const proc = await execa(
715
+ "gh",
716
+ ["pr", "list", "--head", "", "--json", "url", "--limit", "1"],
717
+ { reject: false }
718
+ );
719
+ const parsed = JSON.parse(proc.stdout?.toString() ?? "[]");
720
+ if (parsed[0]?.url) {
721
+ prUrl = parsed[0].url;
722
+ }
723
+ } catch {
724
+ }
725
+ }
726
+ let branchName = "unknown";
727
+ try {
728
+ const proc = await execa("git", ["branch", "--show-current"], { reject: false });
729
+ branchName = proc.stdout?.toString().trim() ?? "unknown";
730
+ } catch {
731
+ }
732
+ let storyCount = 0;
733
+ try {
734
+ const plan = await loadTaskPlan(tasksPath);
735
+ storyCount = plan.userStories.length;
736
+ plan.issueStatus = "completed";
737
+ plan.completedAt = isoNow();
738
+ plan.lastAttemptAt = isoNow();
739
+ await saveTaskPlan(tasksPath, plan);
740
+ } catch {
741
+ }
742
+ const totalDuration = formatDuration(result.overallElapsedSeconds);
743
+ console.log("");
744
+ printSuccess(`Pipeline complete for issue #${issueNumber}!`);
745
+ console.log(` Branch: ${branchName}${effectiveNoBranch ? " (current)" : ""}`);
746
+ console.log(` Stories: ${storyCount}`);
747
+ console.log(` Duration: ${totalDuration}`);
748
+ if (!effectiveNoBranch) {
749
+ console.log(` PR: ${prUrl}`);
750
+ }
751
+ return 0;
752
+ }
753
+ export {
754
+ runPipeline
755
+ };
756
+ //# sourceMappingURL=run-UCSNYSQO.js.map