issue-flow 0.16.0 → 0.18.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.
Files changed (168) hide show
  1. package/dist/agent-HMZTXCRT.js +30 -0
  2. package/dist/{analyze-556KCJKZ.js → analyze-Q4ONIWRZ.js} +22 -22
  3. package/dist/{apply-WVV7G7WN.js → apply-EEFPY2W4.js} +14 -14
  4. package/dist/{bench-UBFGKUHR.js → bench-WWWXLTFH.js} +23 -23
  5. package/dist/{chunk-GV2DABNZ.js → chunk-34OCSKOB.js} +12 -12
  6. package/dist/chunk-3R27MJ5R.js +25 -0
  7. package/dist/chunk-3R27MJ5R.js.map +1 -0
  8. package/dist/{chunk-PGN2VX4Y.js → chunk-45WZ6GMI.js} +32 -48
  9. package/dist/chunk-45WZ6GMI.js.map +1 -0
  10. package/dist/{chunk-H3EYBJZU.js → chunk-62GVVZQO.js} +10 -10
  11. package/dist/{chunk-GPLP7EPL.js → chunk-6K2LCFG6.js} +10 -10
  12. package/dist/{chunk-GPLP7EPL.js.map → chunk-6K2LCFG6.js.map} +1 -1
  13. package/dist/{chunk-OGVWJO4S.js → chunk-7F7LGMVO.js} +3 -3
  14. package/dist/{chunk-X6GY3CM2.js → chunk-7UMMFOL2.js} +104 -104
  15. package/dist/chunk-7UMMFOL2.js.map +1 -0
  16. package/dist/{chunk-OLUOZLU5.js → chunk-CWSQVHVV.js} +1691 -1075
  17. package/dist/chunk-CWSQVHVV.js.map +1 -0
  18. package/dist/{chunk-MXCOSPLJ.js → chunk-DGRTJD5R.js} +3 -3
  19. package/dist/{chunk-ROIIEVZH.js → chunk-DUS5ZNKL.js} +474 -361
  20. package/dist/chunk-DUS5ZNKL.js.map +1 -0
  21. package/dist/{chunk-BSBJXPLC.js → chunk-E4I6MMHD.js} +10 -10
  22. package/dist/{chunk-QZOBR6YF.js → chunk-ECU7YO7S.js} +4 -4
  23. package/dist/chunk-ECU7YO7S.js.map +1 -0
  24. package/dist/{chunk-EPFC4LHT.js → chunk-EG2MGDP5.js} +9 -9
  25. package/dist/chunk-HOAHMERN.js +229 -0
  26. package/dist/chunk-HOAHMERN.js.map +1 -0
  27. package/dist/{chunk-QVLIIGBU.js → chunk-ICM463EO.js} +60 -13
  28. package/dist/chunk-ICM463EO.js.map +1 -0
  29. package/dist/{chunk-4FUWWBTP.js → chunk-KMCS76ZW.js} +8 -8
  30. package/dist/{chunk-SW2MF47D.js → chunk-KRAGAUKD.js} +19 -19
  31. package/dist/{chunk-OOX3RMD3.js → chunk-LGHND47K.js} +4 -4
  32. package/dist/{chunk-VTF4RWS5.js → chunk-LI35PIIS.js} +7 -7
  33. package/dist/{chunk-Q2TZA6AC.js → chunk-LTH6XYZ3.js} +14 -14
  34. package/dist/{chunk-YA6FWFYW.js → chunk-NEWNXZ65.js} +62 -11
  35. package/dist/chunk-NEWNXZ65.js.map +1 -0
  36. package/dist/{chunk-2H43OVTU.js → chunk-OODXFCQV.js} +11 -11
  37. package/dist/{chunk-KV2K7MK2.js → chunk-PMLE7KAQ.js} +20 -12
  38. package/dist/chunk-PMLE7KAQ.js.map +1 -0
  39. package/dist/{chunk-R42YHLDV.js → chunk-PPZ3K4YM.js} +2 -2
  40. package/dist/{chunk-J2LJB2OH.js → chunk-QF7VEL3M.js} +739 -579
  41. package/dist/chunk-QF7VEL3M.js.map +1 -0
  42. package/dist/{chunk-VK65RNFA.js → chunk-QJ44VG5G.js} +3 -3
  43. package/dist/{chunk-FKWYD44M.js → chunk-QPGEFLMY.js} +2 -2
  44. package/dist/{chunk-NQCR3HSU.js → chunk-RNFUVKC3.js} +3 -3
  45. package/dist/{chunk-G5LACAXO.js → chunk-RYOUZ6EK.js} +2 -2
  46. package/dist/{chunk-MCLPSL6B.js → chunk-SA6HXL4Y.js} +3 -3
  47. package/dist/{chunk-GVOZ6NDO.js → chunk-SOM4QA6C.js} +3 -3
  48. package/dist/{chunk-73TS2H42.js → chunk-TZYINKCC.js} +154 -34
  49. package/dist/chunk-TZYINKCC.js.map +1 -0
  50. package/dist/{chunk-DBEMD34J.js → chunk-V7QOIQYC.js} +8 -8
  51. package/dist/{chunk-IAMIBZRF.js → chunk-WCCZJL7W.js} +2 -2
  52. package/dist/{chunk-OGN2NX57.js → chunk-XUEMATVY.js} +9 -9
  53. package/dist/{chunk-SVDEWOUD.js → chunk-XZUEFTUP.js} +6 -6
  54. package/dist/{chunk-WLJVL25O.js → chunk-YUNBOUQH.js} +6 -6
  55. package/dist/{chunk-WNROH2YE.js → chunk-YVPZUN6X.js} +2 -2
  56. package/dist/chunk-ZVZMWNCG.js +415 -0
  57. package/dist/chunk-ZVZMWNCG.js.map +1 -0
  58. package/dist/cli.js +57 -49
  59. package/dist/cli.js.map +1 -1
  60. package/dist/{config-UE7IP36J.js → config-LEBZZCJP.js} +13 -13
  61. package/dist/{conventions-Z3QC2NSG.js → conventions-XOT6YW5L.js} +15 -15
  62. package/dist/{diagnostics-2Z5HFQOO.js → diagnostics-HTCWF7F4.js} +7 -7
  63. package/dist/execute-LELOSR5T.js +37 -0
  64. package/dist/{generate-PB4L7V5N.js → generate-JGW5JSKB.js} +22 -22
  65. package/dist/{git-7MUOGVRG.js → git-A2IP32PQ.js} +2 -2
  66. package/dist/init-TO6MUXO4.js +26 -0
  67. package/dist/{operations-QDJAFZUN.js → operations-O4FORFIH.js} +15 -15
  68. package/dist/{permissions-XVXXENVP.js → permissions-G4XCAESF.js} +4 -4
  69. package/dist/plan-OXAT344Q.js +34 -0
  70. package/dist/{policy-6WQZB6B2.js → policy-U5NBFAO5.js} +14 -14
  71. package/dist/pr-UZHNKKZC.js +39 -0
  72. package/dist/pr-review-Q4ACAWDM.js +32 -0
  73. package/dist/prd-6CYEMLDY.js +33 -0
  74. package/dist/{ps-CX7756QG.js → ps-76L6ZAZL.js} +12 -12
  75. package/dist/{recorder-RK6XZYF3.js → recorder-2BAKPBRA.js} +7 -7
  76. package/dist/{registry-POSB35Q4.js → registry-XLHHYLTR.js} +5 -5
  77. package/dist/{resume-OQJZV3JH.js → resume-URCU6CIJ.js} +39 -38
  78. package/dist/{resume-OQJZV3JH.js.map → resume-URCU6CIJ.js.map} +1 -1
  79. package/dist/review-LVZT2TMW.js +35 -0
  80. package/dist/routing-EK6RBZM4.js +33 -0
  81. package/dist/run-2RXJCJKT.js +56 -0
  82. package/dist/{session-publisher-VVXOCJ2S.js → session-publisher-KAJQRHNZ.js} +3 -2
  83. package/dist/{state-manager-TIC73GQY.js → state-manager-UPHO2EA2.js} +4 -4
  84. package/dist/state-manager-UPHO2EA2.js.map +1 -0
  85. package/dist/{usage-WDDNSIHP.js → usage-KTMYXAPF.js} +12 -12
  86. package/dist/{web-IPGY52AK.js → web-B5UPHHR5.js} +20 -18
  87. package/dist/{web-IPGY52AK.js.map → web-B5UPHHR5.js.map} +1 -1
  88. package/package.json +6 -5
  89. package/web/AGENTS.md +125 -18
  90. package/web/public/app.css +420 -234
  91. package/web/public/app.js +371 -80
  92. package/web/public/index.html +106 -78
  93. package/dist/agent-PMWCDWOW.js +0 -30
  94. package/dist/chunk-73TS2H42.js.map +0 -1
  95. package/dist/chunk-7Z4Y3VKM.js +0 -26
  96. package/dist/chunk-7Z4Y3VKM.js.map +0 -1
  97. package/dist/chunk-AOAQU7PD.js +0 -180
  98. package/dist/chunk-AOAQU7PD.js.map +0 -1
  99. package/dist/chunk-J2LJB2OH.js.map +0 -1
  100. package/dist/chunk-KV2K7MK2.js.map +0 -1
  101. package/dist/chunk-OLUOZLU5.js.map +0 -1
  102. package/dist/chunk-PGN2VX4Y.js.map +0 -1
  103. package/dist/chunk-QVLIIGBU.js.map +0 -1
  104. package/dist/chunk-QZOBR6YF.js.map +0 -1
  105. package/dist/chunk-ROIIEVZH.js.map +0 -1
  106. package/dist/chunk-X6GY3CM2.js.map +0 -1
  107. package/dist/chunk-YA6FWFYW.js.map +0 -1
  108. package/dist/execute-QYLJ5L4T.js +0 -36
  109. package/dist/init-XBERAZ4O.js +0 -24
  110. package/dist/plan-RXOW32KY.js +0 -34
  111. package/dist/pr-3WFYAVA7.js +0 -39
  112. package/dist/pr-review-RK63ZEMM.js +0 -32
  113. package/dist/prd-LRV6IW3E.js +0 -33
  114. package/dist/review-AEFISJZD.js +0 -35
  115. package/dist/routing-EA5NJGNU.js +0 -94
  116. package/dist/routing-EA5NJGNU.js.map +0 -1
  117. package/dist/run-HSC7R33Y.js +0 -55
  118. /package/dist/{agent-PMWCDWOW.js.map → agent-HMZTXCRT.js.map} +0 -0
  119. /package/dist/{analyze-556KCJKZ.js.map → analyze-Q4ONIWRZ.js.map} +0 -0
  120. /package/dist/{apply-WVV7G7WN.js.map → apply-EEFPY2W4.js.map} +0 -0
  121. /package/dist/{bench-UBFGKUHR.js.map → bench-WWWXLTFH.js.map} +0 -0
  122. /package/dist/{chunk-GV2DABNZ.js.map → chunk-34OCSKOB.js.map} +0 -0
  123. /package/dist/{chunk-H3EYBJZU.js.map → chunk-62GVVZQO.js.map} +0 -0
  124. /package/dist/{chunk-OGVWJO4S.js.map → chunk-7F7LGMVO.js.map} +0 -0
  125. /package/dist/{chunk-MXCOSPLJ.js.map → chunk-DGRTJD5R.js.map} +0 -0
  126. /package/dist/{chunk-BSBJXPLC.js.map → chunk-E4I6MMHD.js.map} +0 -0
  127. /package/dist/{chunk-EPFC4LHT.js.map → chunk-EG2MGDP5.js.map} +0 -0
  128. /package/dist/{chunk-4FUWWBTP.js.map → chunk-KMCS76ZW.js.map} +0 -0
  129. /package/dist/{chunk-SW2MF47D.js.map → chunk-KRAGAUKD.js.map} +0 -0
  130. /package/dist/{chunk-OOX3RMD3.js.map → chunk-LGHND47K.js.map} +0 -0
  131. /package/dist/{chunk-VTF4RWS5.js.map → chunk-LI35PIIS.js.map} +0 -0
  132. /package/dist/{chunk-Q2TZA6AC.js.map → chunk-LTH6XYZ3.js.map} +0 -0
  133. /package/dist/{chunk-2H43OVTU.js.map → chunk-OODXFCQV.js.map} +0 -0
  134. /package/dist/{chunk-R42YHLDV.js.map → chunk-PPZ3K4YM.js.map} +0 -0
  135. /package/dist/{chunk-VK65RNFA.js.map → chunk-QJ44VG5G.js.map} +0 -0
  136. /package/dist/{chunk-FKWYD44M.js.map → chunk-QPGEFLMY.js.map} +0 -0
  137. /package/dist/{chunk-NQCR3HSU.js.map → chunk-RNFUVKC3.js.map} +0 -0
  138. /package/dist/{chunk-G5LACAXO.js.map → chunk-RYOUZ6EK.js.map} +0 -0
  139. /package/dist/{chunk-MCLPSL6B.js.map → chunk-SA6HXL4Y.js.map} +0 -0
  140. /package/dist/{chunk-GVOZ6NDO.js.map → chunk-SOM4QA6C.js.map} +0 -0
  141. /package/dist/{chunk-DBEMD34J.js.map → chunk-V7QOIQYC.js.map} +0 -0
  142. /package/dist/{chunk-IAMIBZRF.js.map → chunk-WCCZJL7W.js.map} +0 -0
  143. /package/dist/{chunk-OGN2NX57.js.map → chunk-XUEMATVY.js.map} +0 -0
  144. /package/dist/{chunk-SVDEWOUD.js.map → chunk-XZUEFTUP.js.map} +0 -0
  145. /package/dist/{chunk-WLJVL25O.js.map → chunk-YUNBOUQH.js.map} +0 -0
  146. /package/dist/{chunk-WNROH2YE.js.map → chunk-YVPZUN6X.js.map} +0 -0
  147. /package/dist/{config-UE7IP36J.js.map → config-LEBZZCJP.js.map} +0 -0
  148. /package/dist/{conventions-Z3QC2NSG.js.map → conventions-XOT6YW5L.js.map} +0 -0
  149. /package/dist/{diagnostics-2Z5HFQOO.js.map → diagnostics-HTCWF7F4.js.map} +0 -0
  150. /package/dist/{execute-QYLJ5L4T.js.map → execute-LELOSR5T.js.map} +0 -0
  151. /package/dist/{generate-PB4L7V5N.js.map → generate-JGW5JSKB.js.map} +0 -0
  152. /package/dist/{git-7MUOGVRG.js.map → git-A2IP32PQ.js.map} +0 -0
  153. /package/dist/{init-XBERAZ4O.js.map → init-TO6MUXO4.js.map} +0 -0
  154. /package/dist/{operations-QDJAFZUN.js.map → operations-O4FORFIH.js.map} +0 -0
  155. /package/dist/{permissions-XVXXENVP.js.map → permissions-G4XCAESF.js.map} +0 -0
  156. /package/dist/{plan-RXOW32KY.js.map → plan-OXAT344Q.js.map} +0 -0
  157. /package/dist/{policy-6WQZB6B2.js.map → policy-U5NBFAO5.js.map} +0 -0
  158. /package/dist/{pr-3WFYAVA7.js.map → pr-UZHNKKZC.js.map} +0 -0
  159. /package/dist/{pr-review-RK63ZEMM.js.map → pr-review-Q4ACAWDM.js.map} +0 -0
  160. /package/dist/{prd-LRV6IW3E.js.map → prd-6CYEMLDY.js.map} +0 -0
  161. /package/dist/{ps-CX7756QG.js.map → ps-76L6ZAZL.js.map} +0 -0
  162. /package/dist/{recorder-RK6XZYF3.js.map → recorder-2BAKPBRA.js.map} +0 -0
  163. /package/dist/{registry-POSB35Q4.js.map → registry-XLHHYLTR.js.map} +0 -0
  164. /package/dist/{review-AEFISJZD.js.map → review-LVZT2TMW.js.map} +0 -0
  165. /package/dist/{run-HSC7R33Y.js.map → routing-EK6RBZM4.js.map} +0 -0
  166. /package/dist/{session-publisher-VVXOCJ2S.js.map → run-2RXJCJKT.js.map} +0 -0
  167. /package/dist/{state-manager-TIC73GQY.js.map → session-publisher-KAJQRHNZ.js.map} +0 -0
  168. /package/dist/{usage-WDDNSIHP.js.map → usage-KTMYXAPF.js.map} +0 -0
@@ -3,19 +3,26 @@ import {
3
3
  formatPhaseLine,
4
4
  loadPhaseTiming,
5
5
  snapshotTimingFields
6
- } from "./chunk-VK65RNFA.js";
6
+ } from "./chunk-QJ44VG5G.js";
7
+ import {
8
+ runPr
9
+ } from "./chunk-OODXFCQV.js";
7
10
  import {
8
11
  mostRecent,
9
12
  prReviewDir,
10
13
  readPrReviewIndex,
11
14
  runPrReview
12
- } from "./chunk-Q2TZA6AC.js";
15
+ } from "./chunk-LTH6XYZ3.js";
16
+ import {
17
+ listPullRequests,
18
+ publishGitState
19
+ } from "./chunk-YUNBOUQH.js";
13
20
  import {
14
21
  runPrd
15
- } from "./chunk-OGN2NX57.js";
22
+ } from "./chunk-XUEMATVY.js";
16
23
  import {
17
24
  runReview
18
- } from "./chunk-EPFC4LHT.js";
25
+ } from "./chunk-EG2MGDP5.js";
19
26
  import {
20
27
  PIPELINE_PHASES,
21
28
  PIPELINE_PHASES_NO_BRANCH,
@@ -37,65 +44,65 @@ import {
37
44
  saveExecutionPlan,
38
45
  setQueueBranch,
39
46
  setQueuePullRequest
40
- } from "./chunk-X6GY3CM2.js";
47
+ } from "./chunk-7UMMFOL2.js";
41
48
  import {
42
49
  runInit
43
- } from "./chunk-KV2K7MK2.js";
50
+ } from "./chunk-PMLE7KAQ.js";
44
51
  import {
45
52
  printQueueSummary,
46
53
  printRunSummary,
47
54
  runExecute,
48
55
  stripMarkdown
49
- } from "./chunk-SW2MF47D.js";
56
+ } from "./chunk-KRAGAUKD.js";
50
57
  import {
51
58
  ensureWebMonitor
52
- } from "./chunk-YA6FWFYW.js";
59
+ } from "./chunk-NEWNXZ65.js";
53
60
  import {
54
61
  runPlan
55
- } from "./chunk-GV2DABNZ.js";
56
- import {
57
- runPr
58
- } from "./chunk-2H43OVTU.js";
59
- import {
60
- listPullRequests,
61
- publishGitState
62
- } from "./chunk-WLJVL25O.js";
62
+ } from "./chunk-34OCSKOB.js";
63
63
  import {
64
64
  resolveCommandIssue
65
- } from "./chunk-MCLPSL6B.js";
65
+ } from "./chunk-SA6HXL4Y.js";
66
66
  import {
67
67
  getProvider
68
- } from "./chunk-H3EYBJZU.js";
68
+ } from "./chunk-62GVVZQO.js";
69
69
  import {
70
70
  beginUsageScope,
71
71
  getRunUsageTotals
72
- } from "./chunk-4FUWWBTP.js";
72
+ } from "./chunk-KMCS76ZW.js";
73
73
  import {
74
74
  assessDecomposition,
75
75
  buildDecompositionReport,
76
76
  proposeSubIssues,
77
77
  resetAgentInvocationState
78
- } from "./chunk-73TS2H42.js";
78
+ } from "./chunk-TZYINKCC.js";
79
79
  import {
80
80
  JournalPublisher,
81
81
  MultiPublisher
82
- } from "./chunk-IAMIBZRF.js";
82
+ } from "./chunk-WCCZJL7W.js";
83
+ import {
84
+ recommendedTarget
85
+ } from "./chunk-ZVZMWNCG.js";
83
86
  import {
84
87
  getPackageVersion
85
88
  } from "./chunk-C5TL7T4V.js";
86
89
  import {
87
90
  describeRunAgents,
88
91
  getActiveResilienceConfig,
92
+ getAgentCliOverrides,
93
+ hasExplicitAgentSelection,
89
94
  initResilienceConfig,
95
+ loadAgentConfig,
90
96
  loadIssuesConfig,
97
+ loadRoutingConfig,
91
98
  loadWebConfig,
92
99
  onShutdown
93
- } from "./chunk-J2LJB2OH.js";
100
+ } from "./chunk-QF7VEL3M.js";
94
101
  import {
95
102
  resolveIssuePaths,
96
103
  resolveProjectPaths,
97
104
  resolveQueuePaths
98
- } from "./chunk-VTF4RWS5.js";
105
+ } from "./chunk-LI35PIIS.js";
99
106
  import {
100
107
  formatDuration,
101
108
  getIcons,
@@ -110,18 +117,12 @@ import {
110
117
  setOutputCallback,
111
118
  setStoryStageCallback,
112
119
  setStoryUpdateCallback
113
- } from "./chunk-SVDEWOUD.js";
114
- import {
115
- FilePublisher,
116
- MemoryPublisher,
117
- getSessionPublisher,
118
- setSessionPublisher
119
- } from "./chunk-ROIIEVZH.js";
120
+ } from "./chunk-XZUEFTUP.js";
120
121
  import {
121
122
  bindDiagnosticContext,
122
123
  flushDiagnostics,
123
124
  writeDiagnostic
124
- } from "./chunk-FKWYD44M.js";
125
+ } from "./chunk-QPGEFLMY.js";
125
126
  import {
126
127
  committedStoryIds,
127
128
  describePreflight,
@@ -132,22 +133,22 @@ import {
132
133
  localBranchExists,
133
134
  preflightRepository,
134
135
  run
135
- } from "./chunk-WNROH2YE.js";
136
+ } from "./chunk-YVPZUN6X.js";
137
+ import {
138
+ FilePublisher,
139
+ MemoryPublisher,
140
+ getSessionPublisher,
141
+ setSessionPublisher
142
+ } from "./chunk-DUS5ZNKL.js";
136
143
  import {
137
144
  isoNow,
138
145
  loadTaskPlan,
139
146
  saveTaskPlan
140
- } from "./chunk-GVOZ6NDO.js";
147
+ } from "./chunk-SOM4QA6C.js";
141
148
  import {
142
149
  acquireRunLock,
143
150
  describeRunLockOwner
144
- } from "./chunk-QVLIIGBU.js";
145
-
146
- // src/commands/run.ts
147
- import { randomUUID } from "crypto";
148
- import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
149
- import { join } from "path";
150
- import { execa } from "execa";
151
+ } from "./chunk-ICM463EO.js";
151
152
 
152
153
  // src/execution/detach.ts
153
154
  import { spawn } from "child_process";
@@ -219,6 +220,42 @@ async function spawnDetachedRun(input) {
219
220
  }
220
221
  }
221
222
 
223
+ // src/issues/args.ts
224
+ var IssueArgumentError = class extends Error {
225
+ constructor(message) {
226
+ super(message);
227
+ this.name = "IssueArgumentError";
228
+ }
229
+ };
230
+ var INVALID_ID = /[/\\\s]/;
231
+ function parseIssueArguments(values) {
232
+ const ids = [];
233
+ const seen = /* @__PURE__ */ new Set();
234
+ for (const value of values) {
235
+ for (const raw of value.split(",")) {
236
+ const id = raw.trim().replace(/^#/, "").trim();
237
+ if (id === "") {
238
+ if (raw.trim() !== "" || value.includes(",")) {
239
+ throw new IssueArgumentError(
240
+ `Invalid issue list '${value}': it has an empty entry. Use '42,43' or '42 43'.`
241
+ );
242
+ }
243
+ continue;
244
+ }
245
+ if (INVALID_ID.test(id) || id === "." || id === "..") {
246
+ throw new IssueArgumentError(`Invalid issue identifier '${raw.trim()}'.`);
247
+ }
248
+ if (seen.has(id)) continue;
249
+ seen.add(id);
250
+ ids.push(id);
251
+ }
252
+ }
253
+ if (ids.length === 0) {
254
+ throw new IssueArgumentError("No issue was informed. Pass at least one issue number.");
255
+ }
256
+ return ids;
257
+ }
258
+
222
259
  // src/execution/confirm.ts
223
260
  import { createInterface } from "readline";
224
261
  var MAX_PROMPT_ATTEMPTS = 3;
@@ -476,11 +513,43 @@ async function loadResumableQueue(planFile, requested) {
476
513
  const sameRequest = plan.requested.length === requested.length && plan.requested.every((id, index) => id === requested[index]);
477
514
  return sameRequest ? plan : null;
478
515
  }
516
+ function executableGraphIds(graph, requested) {
517
+ const roots = new Set(requested);
518
+ return [...graph.nodes.values()].filter((node) => roots.has(node.id) || node.issue?.state !== "closed").map((node) => node.id);
519
+ }
520
+ async function reconcileClosedDiscoveredIssues(plan, warn) {
521
+ let next = plan;
522
+ const closed = [];
523
+ for (const entry of plan.issues) {
524
+ if (entry.origin !== "discovered" || entry.status === "completed") continue;
525
+ try {
526
+ const issue = await getProvider(entry.source).get(entry.id);
527
+ if (issue?.state !== "closed") continue;
528
+ next = markQueueIssueCompleted(next, entry.id);
529
+ closed.push(entry.id);
530
+ } catch (err) {
531
+ warn(
532
+ `Could not refresh issue #${entry.id} before resuming the queue: ${err instanceof Error ? err.message : String(err)}`
533
+ );
534
+ }
535
+ }
536
+ return { plan: next, closed };
537
+ }
479
538
  async function planQueue(input) {
480
539
  const info = input.info ?? printInfo;
481
540
  const warn = input.warn ?? printWarning;
482
541
  const single = input.requested.length === 1;
483
- const resumable = await loadResumableQueue(input.planFile, input.requested);
542
+ let resumable = await loadResumableQueue(input.planFile, input.requested);
543
+ if (resumable !== null) {
544
+ const reconciled = await reconcileClosedDiscoveredIssues(resumable, warn);
545
+ resumable = reconciled.plan;
546
+ if (reconciled.closed.length > 0) {
547
+ await saveExecutionPlan(input.planFile, resumable);
548
+ info(
549
+ `Skipping already closed issue(s) in the resumed queue: ${reconciled.closed.map((id) => `#${id}`).join(", ")}.`
550
+ );
551
+ }
552
+ }
484
553
  if (resumable !== null && isQueueComplete(resumable)) {
485
554
  const pr = resumable.pullRequest?.url;
486
555
  info(
@@ -506,10 +575,8 @@ async function planQueue(input) {
506
575
  known: input.known,
507
576
  warn
508
577
  });
509
- if (single && graph.nodes.size <= 1) {
510
- return { kind: "single" };
511
- }
512
- const suggested = computeExecutionOrder(graph);
578
+ const schedulable = executableGraphIds(graph, input.requested);
579
+ const suggested = computeExecutionOrder(graph, { include: schedulable });
513
580
  if (!suggested.ok) {
514
581
  if (single) {
515
582
  warn(
@@ -532,6 +599,9 @@ async function planQueue(input) {
532
599
  noBranch: input.noBranch,
533
600
  prReview: input.prReview
534
601
  });
602
+ if (single && suggestedPlan.issues.length === 1 && suggestedPlan.issues[0]?.role !== "container") {
603
+ return { kind: "single" };
604
+ }
535
605
  let choice;
536
606
  try {
537
607
  choice = await confirmQueue(suggestedPlan, requestedCount, {
@@ -551,7 +621,8 @@ async function planQueue(input) {
551
621
  }
552
622
  let plan = suggestedPlan;
553
623
  if (choice === "cascade") {
554
- const include = collectCascadeIds(graph, input.requested);
624
+ const eligible = new Set(schedulable);
625
+ const include = collectCascadeIds(graph, input.requested).filter((id) => eligible.has(id));
555
626
  const cascadeOrder = computeExecutionOrder(graph, { include });
556
627
  if (!cascadeOrder.ok) {
557
628
  warn(`Dependency cycle between issues: ${describeCycles(cascadeOrder.cycles)}.`);
@@ -605,131 +676,525 @@ async function planQueue(input) {
605
676
  return { kind: "queue", plan, resumed: false };
606
677
  }
607
678
 
608
- // src/issues/args.ts
609
- var IssueArgumentError = class extends Error {
610
- constructor(message) {
611
- super(message);
612
- this.name = "IssueArgumentError";
679
+ // src/commands/run/pull-request.ts
680
+ async function primaryPrCreated(plan) {
681
+ try {
682
+ const paths = await resolveIssuePaths(plan.id);
683
+ return (await loadTaskPlan(paths.tasksFile)).pipeline.prCreated;
684
+ } catch {
685
+ return false;
613
686
  }
614
- };
615
- var INVALID_ID = /[/\\\s]/;
616
- function parseIssueArguments(values) {
617
- const ids = [];
618
- const seen = /* @__PURE__ */ new Set();
619
- for (const value of values) {
620
- for (const raw of value.split(",")) {
621
- const id = raw.trim().replace(/^#/, "").trim();
622
- if (id === "") {
623
- if (raw.trim() !== "" || value.includes(",")) {
624
- throw new IssueArgumentError(
625
- `Invalid issue list '${value}': it has an empty entry. Use '42,43' or '42 43'.`
626
- );
627
- }
628
- continue;
629
- }
630
- if (INVALID_ID.test(id) || id === "." || id === "..") {
631
- throw new IssueArgumentError(`Invalid issue identifier '${raw.trim()}'.`);
687
+ }
688
+ async function buildPrQueueContext(plan) {
689
+ const pending = [];
690
+ for (const entry of plan.issues) {
691
+ try {
692
+ const paths = await resolveIssuePaths(entry.id);
693
+ const taskPlan = await loadTaskPlan(paths.tasksFile);
694
+ if (taskPlan.lastReviewFindings !== null && taskPlan.lastReviewFindings !== "") {
695
+ pending.push(`Issue #${entry.id} has unresolved review findings`);
632
696
  }
633
- if (seen.has(id)) continue;
634
- seen.add(id);
635
- ids.push(id);
697
+ } catch {
636
698
  }
637
699
  }
638
- if (ids.length === 0) {
639
- throw new IssueArgumentError("No issue was informed. Pass at least one issue number.");
640
- }
641
- return ids;
700
+ return {
701
+ issues: plan.issues.map((entry) => ({
702
+ id: entry.id,
703
+ number: entry.number,
704
+ title: entry.title,
705
+ url: entry.url,
706
+ source: entry.source,
707
+ parent: entry.parent,
708
+ role: entry.role,
709
+ complete: entry.status === "completed"
710
+ })),
711
+ excluded: plan.excluded.map((entry) => ({
712
+ id: entry.id,
713
+ number: entry.number,
714
+ title: entry.title,
715
+ reason: entry.reason
716
+ })),
717
+ pending
718
+ };
642
719
  }
643
-
644
- // src/ui/pipeline-renderer.ts
645
- import { Listr, PRESET_TIMER, PRESET_TIMESTAMP } from "listr2";
646
-
647
- // src/ui/status-view.ts
648
- function renderExecuteFocus(snapshot, options = {}) {
649
- const icons = options.icons ?? getIcons();
650
- const story = activeStory(snapshot);
651
- if (story === void 0 && snapshot.currentActivity === null) return [];
652
- const lines = [];
653
- if (story !== void 0) {
654
- const duration = story.durationSeconds !== null && story.durationSeconds !== void 0 ? ` ${formatDuration(story.durationSeconds)}` : "";
655
- lines.push(` ${icons.pending} ${story.id} ${stripMarkdown(story.title)}${duration}`);
720
+ async function propagatePullRequest(plan) {
721
+ let pullRequest = null;
722
+ try {
723
+ const primaryPaths = await resolveIssuePaths(plan.id);
724
+ pullRequest = (await loadTaskPlan(primaryPaths.tasksFile)).pullRequest ?? null;
725
+ } catch {
726
+ return null;
656
727
  }
657
- const activity = snapshot.currentActivity;
658
- if (activity?.tool) {
659
- const detail = activity.detail ? ` ${stripMarkdown(activity.detail)}` : "";
660
- lines.push(` ${activity.tool}${detail}`);
728
+ if (pullRequest === null) return null;
729
+ for (const entry of plan.issues) {
730
+ if (entry.id === plan.id) continue;
731
+ try {
732
+ const paths = await resolveIssuePaths(entry.id);
733
+ const taskPlan = await loadTaskPlan(paths.tasksFile);
734
+ taskPlan.pullRequest = pullRequest;
735
+ taskPlan.pipeline.prCreated = true;
736
+ await saveTaskPlan(paths.tasksFile, taskPlan);
737
+ } catch {
738
+ printWarning(`Could not record the Pull Request on issue #${entry.id}'s task plan.`);
739
+ }
661
740
  }
662
- return lines;
741
+ return pullRequest;
663
742
  }
664
- function activeStory(snapshot) {
665
- const fromActivity = snapshot.currentActivity?.story;
666
- if (fromActivity) {
667
- const match = snapshot.stories.find((story) => story.id === fromActivity);
668
- if (match) return match;
743
+ async function closeIssue(issueNumber, source) {
744
+ try {
745
+ const provider = getProvider(source);
746
+ if (provider.close !== void 0) {
747
+ printInfo("Closing issue...");
748
+ await provider.close(issueNumber);
749
+ }
750
+ } catch {
751
+ printWarning("Failed to close issue automatically");
669
752
  }
670
- return snapshot.stories.find((story) => story.stage === "executing");
671
753
  }
672
754
 
673
- // src/ui/pipeline-renderer.ts
674
- var PHASE_LABELS = {
675
- prd: "PRD",
676
- plan: "Plan",
677
- execute: "Execute",
678
- review: "Review",
679
- pr: "PR",
680
- "pr-review": "PR Review"
681
- };
682
- function phaseLabel(phase) {
683
- return PHASE_LABELS[phase] ?? phase.charAt(0).toUpperCase() + phase.slice(1);
684
- }
685
- function selectRenderer(verbose) {
686
- const isTTY = !!process.stdout.isTTY;
687
- const isCI = !!process.env.CI;
688
- if (!isTTY || isCI) {
689
- return "simple";
690
- }
691
- return verbose ? "verbose" : "default";
692
- }
693
- function storySubtaskTitle(story, executing) {
694
- const base = `${story.id}: ${story.title}`;
695
- return executing ? `${base} \u2192 Executando...` : base;
696
- }
697
- function createActiveStoryTracker(stories, storyTasks) {
698
- let activeStoryId;
699
- const retitle = (storyId, executing) => {
700
- const task = storyTasks.get(storyId);
701
- if (!task) return;
702
- task.title = storySubtaskTitle(
703
- stories.find((s) => s.id === storyId) ?? { id: storyId, title: "" },
704
- executing
705
- );
706
- };
707
- return {
708
- setActive(storyId) {
709
- if (activeStoryId !== void 0 && activeStoryId !== storyId) {
710
- retitle(activeStoryId, false);
711
- }
712
- activeStoryId = storyId;
713
- if (storyId !== void 0) retitle(storyId, true);
714
- },
715
- clear() {
716
- if (activeStoryId === void 0) return;
717
- retitle(activeStoryId, false);
718
- activeStoryId = void 0;
755
+ // src/commands/run/queue-failure.ts
756
+ async function handleQueueIssueFailure(input) {
757
+ let plan = input.plan;
758
+ const { planFile, entry, result, failureMode, maxIssueAttempts, exhausted, failedIds } = input;
759
+ const failure2 = {
760
+ phase: result.failedPhase,
761
+ error: {
762
+ category: "queue_issue_failed",
763
+ message: `Issue #${entry.id} failed${result.failedPhase === null ? "" : ` in phase ${result.failedPhase}`}`,
764
+ at: isoNow()
719
765
  }
720
766
  };
721
- }
722
- function executeExpandsStories(verbose) {
723
- return verbose;
724
- }
725
- function paintExecuteFocus(task) {
726
- const lines = renderExecuteFocus(getSessionPublisher().snapshot());
727
- if (lines.length > 0) task.output = lines.join("\n");
728
- }
729
- function buildExecutePhaseTask(runner, tasksPath, verbose) {
730
- return async (_ctx, task) => {
731
- let stories;
732
- try {
767
+ const where = result.failedPhase === null ? "" : ` (phase ${result.failedPhase})`;
768
+ if (failureMode === "stop") {
769
+ plan = markQueueIssueFailed(plan, entry.id, failure2);
770
+ await saveExecutionPlan(planFile, plan);
771
+ printError(
772
+ `Queue stopped at issue #${entry.id}${where}. The branch and every commit made so far were kept.`
773
+ );
774
+ printInfo(`Resume with: issue-flow run ${plan.requested.join(",")}`);
775
+ return { action: "stop", code: result.code };
776
+ }
777
+ if (failureMode === "block") {
778
+ plan = markQueueIssueBlocked(
779
+ plan,
780
+ entry.id,
781
+ `Failed${where} and --on-issue-failure block was in force`,
782
+ failure2
783
+ );
784
+ await saveExecutionPlan(planFile, plan);
785
+ exhausted.add(entry.id);
786
+ failedIds.add(entry.id);
787
+ printWarning(
788
+ `Issue #${entry.id} failed${where} and is blocked for review. Continuing with the rest of the queue.`
789
+ );
790
+ return { action: "continue", plan };
791
+ }
792
+ const attempts = entry.attempts + 1;
793
+ if (attempts >= maxIssueAttempts) {
794
+ plan = markQueueIssueFailed(plan, entry.id, failure2);
795
+ exhausted.add(entry.id);
796
+ failedIds.add(entry.id);
797
+ printWarning(
798
+ `Issue #${entry.id} failed${where} after ${attempts} attempt(s). Continuing with the rest of the queue.`
799
+ );
800
+ } else {
801
+ plan = markQueueIssueSkipped(plan, entry.id, failure2);
802
+ printWarning(
803
+ `Issue #${entry.id} failed${where}. Skipping it for now and coming back at the end of the queue.`
804
+ );
805
+ }
806
+ await saveExecutionPlan(planFile, plan);
807
+ return { action: "continue", plan };
808
+ }
809
+
810
+ // src/commands/run/multi-issue.ts
811
+ var DEFAULT_MAX_ISSUE_ATTEMPTS = 2;
812
+ async function decideQueue(input) {
813
+ const primary = input.requested[0];
814
+ const single = input.requested.length === 1;
815
+ if (single && input.runOptions.only === true) {
816
+ return { kind: "single" };
817
+ }
818
+ try {
819
+ const [project, queuePaths] = await Promise.all([
820
+ resolveProjectPaths(),
821
+ resolveQueuePaths(primary)
822
+ ]);
823
+ return await planQueue({
824
+ requested: input.requested,
825
+ source: input.resolved.source,
826
+ known: [input.resolved.issue],
827
+ projectId: project.projectId,
828
+ planFile: queuePaths.planFile,
829
+ noBranch: input.noBranch,
830
+ prReview: input.prReview,
831
+ confirm: {
832
+ yes: input.runOptions.yes,
833
+ only: input.runOptions.only,
834
+ cascade: input.runOptions.cascade
835
+ }
836
+ });
837
+ } catch (err) {
838
+ const message = err instanceof Error ? err.message : String(err);
839
+ if (single) {
840
+ printWarning(
841
+ `Could not inspect related issues (${message}). Running issue #${primary} alone.`
842
+ );
843
+ return { kind: "single" };
844
+ }
845
+ printError(`Could not plan the execution of ${input.requested.length} issues: ${message}`);
846
+ return { kind: "stop", code: 1 };
847
+ }
848
+ }
849
+ async function detachAfterConfirm(requested, noBranch, prReview, options) {
850
+ const childFlags = [...process.argv.slice(2)];
851
+ if (options.yes !== true && options.only !== true) {
852
+ try {
853
+ const issuesConfig = await loadIssuesConfig();
854
+ const resolution = await resolveCommandIssue(requested[0], void 0, {
855
+ config: issuesConfig
856
+ });
857
+ if (resolution.ok) {
858
+ const decision = await decideQueue({
859
+ requested,
860
+ resolved: resolution.resolved,
861
+ noBranch: noBranch ?? false,
862
+ prReview: prReview ?? false,
863
+ runOptions: options
864
+ });
865
+ if (decision.kind === "stop") return decision.code;
866
+ if (decision.kind === "queue" && !childFlags.includes("--yes")) childFlags.push("--yes");
867
+ if (decision.kind === "single" && !childFlags.includes("--only")) childFlags.push("--only");
868
+ }
869
+ } catch {
870
+ }
871
+ }
872
+ const paths = await resolveIssuePaths(requested[0]);
873
+ const { pid, logFile } = await spawnDetachedRun({ paths, argv: childFlags });
874
+ printSuccess(`Detached run started (pid ${pid}).`);
875
+ printInfo(`Log: ${logFile}`);
876
+ printInfo("Follow with: issue-flow ps");
877
+ printInfo(`Stop with: kill ${pid}`);
878
+ return 0;
879
+ }
880
+ async function runQueue(initialPlan, options, resolvedPrimary, runIssueSession3) {
881
+ const planFile = (await resolveQueuePaths(initialPlan.id)).planFile;
882
+ const queueUsage = beginUsageScope();
883
+ const startedAtMs = Date.now();
884
+ const resilience = getActiveResilienceConfig();
885
+ const failureMode = options.runOptions?.onIssueFailure ?? resilience.queue?.onIssueFailure ?? "stop";
886
+ const maxIssueAttempts = resilience.queue?.maxIssueAttempts ?? DEFAULT_MAX_ISSUE_ATTEMPTS;
887
+ try {
888
+ return await processQueueIssues({
889
+ plan: initialPlan,
890
+ planFile,
891
+ initialPlan,
892
+ options,
893
+ resolvedPrimary,
894
+ runIssueSession: runIssueSession3,
895
+ failureMode,
896
+ maxIssueAttempts,
897
+ startedAtMs,
898
+ queueUsage
899
+ });
900
+ } finally {
901
+ queueUsage.end();
902
+ }
903
+ }
904
+ async function processQueueEntry(input) {
905
+ let plan = input.plan;
906
+ const {
907
+ planFile,
908
+ entry,
909
+ initialPlan,
910
+ options,
911
+ resolvedPrimary,
912
+ runIssueSession: runIssueSession3,
913
+ failureMode,
914
+ maxIssueAttempts,
915
+ exhausted,
916
+ failedIds,
917
+ summaries
918
+ } = input;
919
+ let firstOfThisRun = input.firstOfThisRun;
920
+ plan = markQueueIssueInProgress(plan, entry.id);
921
+ await saveExecutionPlan(planFile, plan);
922
+ printInfo(
923
+ `Queue ${entry.position}/${plan.issues.length}: issue #${entry.id}${entry.title === "" ? "" : ` \u2014 ${entry.title}`}`
924
+ );
925
+ const issueUsage = beginUsageScope();
926
+ let result;
927
+ try {
928
+ result = await runIssueSession3(entry.id, options.mode, {
929
+ // `--from` / `--start-us` apply only to the first issue of this invocation.
930
+ from: firstOfThisRun ? options.from : void 0,
931
+ noBranch: options.noBranch,
932
+ runOptions: {
933
+ ...options.runOptions,
934
+ startUs: firstOfThisRun ? options.runOptions?.startUs : void 0
935
+ },
936
+ queue: {
937
+ plan,
938
+ preChecked: true,
939
+ resolved: entry.id === initialPlan.id ? resolvedPrimary : void 0
940
+ }
941
+ });
942
+ } finally {
943
+ firstOfThisRun = false;
944
+ issueUsage.end();
945
+ }
946
+ if (result.code !== 0) {
947
+ const handled = await handleQueueIssueFailure({
948
+ plan,
949
+ planFile,
950
+ entry,
951
+ result,
952
+ failureMode,
953
+ maxIssueAttempts,
954
+ exhausted,
955
+ failedIds
956
+ });
957
+ if (handled.action === "stop") return { action: "stop", code: handled.code };
958
+ return { action: "continue", plan: handled.plan, firstOfThisRun };
959
+ }
960
+ plan = await recordQueueIssueSuccess({
961
+ plan,
962
+ planFile,
963
+ entry,
964
+ result,
965
+ usage: issueUsage.totals(),
966
+ summaries
967
+ });
968
+ return { action: "continue", plan, firstOfThisRun };
969
+ }
970
+ async function recordQueueIssueSuccess(input) {
971
+ let { plan } = input;
972
+ const { planFile, entry, result, usage, summaries } = input;
973
+ if (plan.branchName === null && result.branchName !== null) {
974
+ plan = setQueueBranch(plan, result.branchName);
975
+ }
976
+ plan = markQueueIssueCompleted(plan, entry.id);
977
+ await saveExecutionPlan(planFile, plan);
978
+ summaries.push({
979
+ id: entry.id,
980
+ title: entry.title,
981
+ storyCount: result.storyCount,
982
+ elapsedSeconds: result.elapsedSeconds,
983
+ usage
984
+ });
985
+ return plan;
986
+ }
987
+ async function processQueueIssues(input) {
988
+ let plan = input.plan;
989
+ const {
990
+ planFile,
991
+ initialPlan,
992
+ options,
993
+ resolvedPrimary,
994
+ runIssueSession: runIssueSession3,
995
+ failureMode,
996
+ maxIssueAttempts,
997
+ startedAtMs,
998
+ queueUsage
999
+ } = input;
1000
+ const summaries = [];
1001
+ let firstOfThisRun = true;
1002
+ const exhausted = /* @__PURE__ */ new Set();
1003
+ const failedIds = /* @__PURE__ */ new Set();
1004
+ while (true) {
1005
+ const entry = nextQueueIssue(plan, { exclude: exhausted });
1006
+ if (entry === null) break;
1007
+ const processed = await processQueueEntry({
1008
+ plan,
1009
+ planFile,
1010
+ entry,
1011
+ initialPlan,
1012
+ options,
1013
+ resolvedPrimary,
1014
+ runIssueSession: runIssueSession3,
1015
+ failureMode,
1016
+ maxIssueAttempts,
1017
+ firstOfThisRun,
1018
+ exhausted,
1019
+ failedIds,
1020
+ summaries
1021
+ });
1022
+ if (processed.action === "stop") return processed.code;
1023
+ plan = processed.plan;
1024
+ firstOfThisRun = processed.firstOfThisRun;
1025
+ }
1026
+ const code = await finishQueue(
1027
+ plan,
1028
+ planFile,
1029
+ summaries,
1030
+ startedAtMs,
1031
+ queueUsage,
1032
+ options,
1033
+ resolvedPrimary,
1034
+ runIssueSession3
1035
+ );
1036
+ if (failedIds.size > 0) {
1037
+ printError(
1038
+ `${failedIds.size} issue(s) did not finish: ${[...failedIds].map((id) => `#${id}`).join(", ")}.`
1039
+ );
1040
+ printInfo(`Resume with: issue-flow run ${plan.requested.join(",")}`);
1041
+ return code === 0 ? 1 : code;
1042
+ }
1043
+ return code;
1044
+ }
1045
+ async function finishQueue(plan, planFile, summaries, startedAtMs, queueUsage, options, resolvedPrimary, runIssueSession3) {
1046
+ let current = plan;
1047
+ let review = null;
1048
+ const alreadyOpened = current.pullRequest !== void 0 || await primaryPrCreated(current);
1049
+ if (!current.noBranch && options.noBranch !== true && !alreadyOpened) {
1050
+ const outcome = await runIssueSession3(current.id, options.mode, {
1051
+ prReview: options.prReview ?? current.prReview,
1052
+ queue: {
1053
+ plan: current,
1054
+ preChecked: true,
1055
+ resolved: resolvedPrimary,
1056
+ finalPr: await buildPrQueueContext(current)
1057
+ }
1058
+ });
1059
+ review = outcome.review ?? null;
1060
+ if (outcome.code !== 0) {
1061
+ printError("The consolidated Pull Request could not be created.");
1062
+ await saveExecutionPlan(planFile, current);
1063
+ return outcome.code;
1064
+ }
1065
+ const pullRequest = await propagatePullRequest(current);
1066
+ if (pullRequest !== null) {
1067
+ current = setQueuePullRequest(current, pullRequest);
1068
+ }
1069
+ }
1070
+ await saveExecutionPlan(planFile, current);
1071
+ if (review?.requestedChanges === true) {
1072
+ printInfo("Issues left open until the review blockers are addressed.");
1073
+ } else {
1074
+ for (const entry of current.issues) {
1075
+ await closeIssue(entry.id, entry.source);
1076
+ }
1077
+ }
1078
+ printQueueSummary({
1079
+ queueId: current.id,
1080
+ branchName: current.branchName,
1081
+ issues: summaries,
1082
+ excluded: current.excluded.map((entry) => ({ id: entry.id, title: entry.title })),
1083
+ elapsedSeconds: Math.round((Date.now() - startedAtMs) / 1e3),
1084
+ prUrl: current.pullRequest?.url ?? null,
1085
+ // Read here, after the consolidated Pull Request (and the optional
1086
+ // pr-review) already ran: reading it before would report a queue total
1087
+ // that leaves out the phase that just spent tokens.
1088
+ usage: queueUsage.totals(),
1089
+ prReview: review
1090
+ });
1091
+ return 0;
1092
+ }
1093
+ async function adoptQueueBranch(tasksPath, queueBranch) {
1094
+ try {
1095
+ const plan = await loadTaskPlan(tasksPath);
1096
+ if (queueBranch === null || queueBranch === "") {
1097
+ return plan.branchName === "" ? null : plan.branchName;
1098
+ }
1099
+ if (plan.branchName !== queueBranch) {
1100
+ plan.branchName = queueBranch;
1101
+ await saveTaskPlan(tasksPath, plan);
1102
+ }
1103
+ return queueBranch;
1104
+ } catch {
1105
+ return queueBranch;
1106
+ }
1107
+ }
1108
+
1109
+ // src/ui/pipeline-renderer.ts
1110
+ import { Listr, PRESET_TIMER, PRESET_TIMESTAMP } from "listr2";
1111
+
1112
+ // src/ui/status-view.ts
1113
+ function renderExecuteFocus(snapshot, options = {}) {
1114
+ const icons = options.icons ?? getIcons();
1115
+ const story = activeStory(snapshot);
1116
+ if (story === void 0 && snapshot.currentActivity === null) return [];
1117
+ const lines = [];
1118
+ if (story !== void 0) {
1119
+ const duration = story.durationSeconds !== null && story.durationSeconds !== void 0 ? ` ${formatDuration(story.durationSeconds)}` : "";
1120
+ lines.push(` ${icons.pending} ${story.id} ${stripMarkdown(story.title)}${duration}`);
1121
+ }
1122
+ const activity = snapshot.currentActivity;
1123
+ if (activity?.tool) {
1124
+ const detail = activity.detail ? ` ${stripMarkdown(activity.detail)}` : "";
1125
+ lines.push(` ${activity.tool}${detail}`);
1126
+ }
1127
+ return lines;
1128
+ }
1129
+ function activeStory(snapshot) {
1130
+ const fromActivity = snapshot.currentActivity?.story;
1131
+ if (fromActivity) {
1132
+ const match = snapshot.stories.find((story) => story.id === fromActivity);
1133
+ if (match) return match;
1134
+ }
1135
+ return snapshot.stories.find((story) => story.stage === "executing");
1136
+ }
1137
+
1138
+ // src/ui/pipeline-renderer.ts
1139
+ var PHASE_LABELS = {
1140
+ prd: "PRD",
1141
+ plan: "Plan",
1142
+ execute: "Execute",
1143
+ review: "Review",
1144
+ pr: "PR",
1145
+ "pr-review": "PR Review"
1146
+ };
1147
+ function phaseLabel(phase) {
1148
+ return PHASE_LABELS[phase] ?? phase.charAt(0).toUpperCase() + phase.slice(1);
1149
+ }
1150
+ function selectRenderer(verbose) {
1151
+ const isTTY = !!process.stdout.isTTY;
1152
+ const isCI = !!process.env.CI;
1153
+ if (!isTTY || isCI) {
1154
+ return "simple";
1155
+ }
1156
+ return verbose ? "verbose" : "default";
1157
+ }
1158
+ function storySubtaskTitle(story, executing) {
1159
+ const base = `${story.id}: ${story.title}`;
1160
+ return executing ? `${base} \u2192 Executando...` : base;
1161
+ }
1162
+ function createActiveStoryTracker(stories, storyTasks) {
1163
+ let activeStoryId;
1164
+ const retitle = (storyId, executing) => {
1165
+ const task = storyTasks.get(storyId);
1166
+ if (!task) return;
1167
+ task.title = storySubtaskTitle(
1168
+ stories.find((s) => s.id === storyId) ?? { id: storyId, title: "" },
1169
+ executing
1170
+ );
1171
+ };
1172
+ return {
1173
+ setActive(storyId) {
1174
+ if (activeStoryId !== void 0 && activeStoryId !== storyId) {
1175
+ retitle(activeStoryId, false);
1176
+ }
1177
+ activeStoryId = storyId;
1178
+ if (storyId !== void 0) retitle(storyId, true);
1179
+ },
1180
+ clear() {
1181
+ if (activeStoryId === void 0) return;
1182
+ retitle(activeStoryId, false);
1183
+ activeStoryId = void 0;
1184
+ }
1185
+ };
1186
+ }
1187
+ function executeExpandsStories(verbose) {
1188
+ return verbose;
1189
+ }
1190
+ function paintExecuteFocus(task) {
1191
+ const lines = renderExecuteFocus(getSessionPublisher().snapshot());
1192
+ if (lines.length > 0) task.output = lines.join("\n");
1193
+ }
1194
+ function buildExecutePhaseTask(runner, tasksPath, verbose) {
1195
+ return async (_ctx, task) => {
1196
+ let stories;
1197
+ try {
733
1198
  const plan = await loadTaskPlan(tasksPath);
734
1199
  stories = [...plan.userStories].sort((a, b) => a.priority - b.priority);
735
1200
  } catch {
@@ -907,7 +1372,11 @@ async function runPipelineWithRenderer(options) {
907
1372
  };
908
1373
  }
909
1374
 
910
- // src/commands/run.ts
1375
+ // src/commands/run/phase-finalize.ts
1376
+ import { execa } from "execa";
1377
+
1378
+ // src/commands/run/publish.ts
1379
+ import { join } from "path";
911
1380
  function toIssueNumber(id) {
912
1381
  const parsed = Number.parseInt(id, 10);
913
1382
  return Number.isNaN(parsed) ? null : parsed;
@@ -960,17 +1429,6 @@ function publishIssueDetails(publisher, issue, at) {
960
1429
  state: issue.state
961
1430
  });
962
1431
  }
963
- var RUNNABLE_PHASES = ["prd", "plan", "execute", "review", "pr"];
964
- var RUNNABLE_PHASES_NO_BRANCH = ["prd", "plan", "execute", "review"];
965
- var RUNNABLE_PHASES_WITH_PR_REVIEW = [...RUNNABLE_PHASES, "pr-review"];
966
- var QUEUE_PR_PHASES = ["init", "pr"];
967
- var QUEUE_PR_PHASES_WITH_REVIEW = [
968
- "init",
969
- "pr",
970
- "pr-review"
971
- ];
972
- var RUNNABLE_QUEUE_PR_PHASES = ["pr"];
973
- var RUNNABLE_QUEUE_PR_PHASES_WITH_REVIEW = ["pr", "pr-review"];
974
1432
  function verificationForSummary(value) {
975
1433
  if (value === null || value.verdict === null) return null;
976
1434
  return { verdict: value.verdict, level: value.level };
@@ -997,200 +1455,608 @@ async function readPrReviewOutcome(issue, tasksPath, requestedChanges) {
997
1455
  }
998
1456
  return outcome;
999
1457
  }
1000
- var DEFAULT_MAX_ISSUE_ATTEMPTS = 2;
1001
- async function runPipeline(issue, mode, from, noBranch, prReview, options = {}) {
1002
- let requested;
1003
- try {
1004
- requested = parseIssueArguments(typeof issue === "string" ? [issue] : issue);
1005
- } catch (err) {
1006
- printError(err instanceof Error ? err.message : String(err));
1007
- return 1;
1008
- }
1009
- if (options.background === true && options.detachedChild !== true) {
1010
- const reason = backgroundRejection(mode);
1011
- if (reason !== null) {
1012
- printError(reason);
1013
- return 1;
1458
+
1459
+ // src/commands/run/phase-finalize.ts
1460
+ async function applyReviewOutcomeAndClose(input) {
1461
+ const { issueNumber, resolvedIssue, review, inQueue } = input;
1462
+ if (review?.requestedChanges) {
1463
+ console.log("");
1464
+ printWarning("PR review requested changes \u2014 the Pull Request is not ready to merge.");
1465
+ if (review.reportPath !== null) {
1466
+ console.log(` Report: ${review.reportPath}`);
1014
1467
  }
1015
- return detachAfterConfirm(requested, noBranch, prReview, options);
1016
- }
1017
- const ownership = await claimRunOwnership(requested[0], options.detachedChild === true);
1018
- if (!ownership.ok) {
1019
- printError(
1020
- `Another issue-flow run owns this project: ${describeRunLockOwner(ownership.owner)}.`
1021
- );
1022
- printInfo("Wait for it to finish, or stop that process before running again.");
1023
- return 1;
1024
1468
  }
1025
- try {
1026
- const first = await runIssueSession(requested[0], mode, {
1027
- from,
1028
- noBranch,
1029
- prReview,
1030
- requested,
1031
- runOptions: options,
1032
- restartWeb: options.restartWeb,
1033
- ...ownership.interruptedBy === null ? {} : { interruptedBy: ownership.interruptedBy }
1034
- });
1035
- if (first.queue === void 0) {
1036
- return first.code;
1037
- }
1038
- return runQueue(
1039
- first.queue.plan,
1040
- { mode, from, noBranch, prReview, runOptions: options },
1041
- first.queue.resolved
1042
- );
1043
- } finally {
1044
- await ownership.release();
1469
+ if (review?.requestedChanges) {
1470
+ printInfo("Issue left open until the review blockers are addressed.");
1471
+ } else if (!inQueue) {
1472
+ await closeIssue(issueNumber, resolvedIssue.source);
1045
1473
  }
1046
1474
  }
1047
- var WRITING_PHASES = /* @__PURE__ */ new Set(["plan", "execute", "pr"]);
1048
- async function ensureRepositoryWritable(phase) {
1049
- if (!WRITING_PHASES.has(phase)) return;
1050
- const preflight = await preflightRepository({ intent: "resume-same-phase" });
1051
- if (preflight.ok) return;
1052
- for (const line of describePreflight(preflight)) {
1053
- printError(line);
1475
+ async function resolveSummaryPrUrl(input) {
1476
+ let prUrl = "unknown";
1477
+ if (!input.effectiveNoBranch && input.source === "github") {
1478
+ if (input.planPrUrl !== null) {
1479
+ prUrl = input.planPrUrl;
1480
+ } else if (input.branchName !== "unknown" && input.branchName !== "") {
1481
+ try {
1482
+ const latest = mostRecent(await listPullRequests(input.branchName));
1483
+ if (latest !== null) {
1484
+ prUrl = latest.url;
1485
+ }
1486
+ } catch {
1487
+ }
1488
+ }
1054
1489
  }
1055
- throw new Error(`The repository is not in a state the ${phase} phase can write to`);
1490
+ return prUrl;
1056
1491
  }
1057
- async function claimRunOwnership(target, detached = false) {
1058
- let lockFile;
1492
+ async function finalizeSuccessfulIssueRun(input) {
1493
+ const {
1494
+ issueNumber,
1495
+ tasksPath,
1496
+ resolvedIssue,
1497
+ review,
1498
+ inQueue,
1499
+ finalPr,
1500
+ producedBranch,
1501
+ effectiveNoBranch,
1502
+ elapsedSeconds
1503
+ } = input;
1504
+ await applyReviewOutcomeAndClose({ issueNumber, resolvedIssue, review, inQueue });
1505
+ let branchName = "unknown";
1059
1506
  try {
1060
- lockFile = (await resolveProjectPaths()).runLockFile;
1507
+ const proc = await execa("git", ["branch", "--show-current"], { reject: false });
1508
+ branchName = proc.stdout?.toString().trim() ?? "unknown";
1061
1509
  } catch {
1062
- return { ok: true, interruptedBy: null, release: async () => {
1063
- } };
1064
1510
  }
1065
- const result = await acquireRunLock(lockFile, { target, detached });
1066
- if (!result.ok) return result;
1067
- return {
1068
- ok: true,
1069
- interruptedBy: result.handle.reclaimedFrom,
1070
- release: () => result.handle.release()
1071
- };
1072
- }
1073
- async function runIssueSession(issueNumber, mode, input) {
1074
- resetAgentInvocationState();
1075
- const paths = await resolveIssuePaths(issueNumber);
1511
+ let storyCount = 0;
1512
+ let planPrUrl = null;
1076
1513
  try {
1077
- const project = await resolveProjectPaths();
1078
- bindDiagnosticContext({
1079
- project: project.projectId,
1080
- projectRoot: await getProjectRoot(),
1081
- issue: issueNumber,
1082
- sessionId: null,
1083
- executionId: null,
1084
- phase: null,
1085
- story: null,
1086
- harness: null,
1087
- model: null
1088
- });
1514
+ const plan = await loadTaskPlan(tasksPath);
1515
+ storyCount = plan.userStories.length;
1516
+ planPrUrl = plan.pullRequest?.url ?? null;
1517
+ plan.lastAttemptAt = isoNow();
1518
+ if (!review?.requestedChanges) {
1519
+ plan.issueStatus = "completed";
1520
+ plan.completedAt = isoNow();
1521
+ }
1522
+ await saveTaskPlan(tasksPath, plan);
1089
1523
  } catch {
1090
1524
  }
1091
- const resilience = await initResilienceConfig();
1092
- const configuredInactivity = resilience.watchdog?.inactivityTimeoutMs;
1093
- if (configuredInactivity !== void 0 && getInactivityTimeout() === void 0) {
1094
- setInactivityTimeout(configuredInactivity);
1095
- }
1096
- const webConfig = await loadWebConfig();
1097
- const surfaces = [];
1098
- const journalEnabled = resilience.journal?.enabled === true;
1099
- const persistSnapshot = webConfig.enabled || input.runOptions?.detachedChild === true;
1100
- if (persistSnapshot || journalEnabled) {
1101
- await mkdir2(paths.issueDir, { recursive: true });
1102
- }
1103
- if (persistSnapshot) {
1104
- surfaces.push(
1105
- new FilePublisher(paths.sessionFile, {
1106
- logLimit: webConfig.logLimit,
1107
- includeLogs: webConfig.includeLogs
1108
- })
1109
- );
1110
- }
1111
- if (journalEnabled) {
1112
- surfaces.push(
1113
- new JournalPublisher(paths.eventsFile, paths.rotatedEventsFile, {
1114
- logLimit: webConfig.logLimit,
1115
- includeLogs: webConfig.includeLogs,
1116
- ...resilience.journal?.maxFileBytes === void 0 ? {} : { maxFileBytes: resilience.journal.maxFileBytes }
1117
- })
1118
- );
1525
+ if (inQueue) {
1526
+ if (finalPr === void 0) {
1527
+ printSuccess(`Issue #${issueNumber} completed (${storyCount} stories).`);
1528
+ }
1529
+ return {
1530
+ code: 0,
1531
+ failedPhase: null,
1532
+ branchName: producedBranch ?? (branchName === "unknown" ? null : branchName),
1533
+ storyCount,
1534
+ elapsedSeconds,
1535
+ review
1536
+ };
1119
1537
  }
1120
- const publisher = surfaces.length === 0 ? new MemoryPublisher() : surfaces.length === 1 ? surfaces[0] : new MultiPublisher(surfaces);
1121
- setSessionPublisher(publisher);
1122
- if (input.interruptedBy !== void 0) {
1123
- const previous = input.interruptedBy;
1124
- publisher.publish({
1125
- type: "log",
1126
- at: isoNow(),
1127
- level: "warn",
1128
- message: `Previous run interrupted: ${describeRunLockOwner(previous)}. Its lock was stale and has been taken over.`
1129
- });
1538
+ const prUrl = await resolveSummaryPrUrl({
1539
+ effectiveNoBranch,
1540
+ source: resolvedIssue.source,
1541
+ planPrUrl,
1542
+ branchName
1543
+ });
1544
+ printRunSummary({
1545
+ issueNumber,
1546
+ branchName,
1547
+ noBranch: effectiveNoBranch,
1548
+ storyCount,
1549
+ elapsedSeconds,
1550
+ prUrl,
1551
+ prReview: review,
1552
+ // Process-owned counters: the session snapshot is empty whenever web
1553
+ // monitoring is off, so it cannot be the source of these totals.
1554
+ usage: getRunUsageTotals(),
1555
+ verification: verificationForSummary(getSessionPublisher().snapshot().verification)
1556
+ });
1557
+ return {
1558
+ code: 0,
1559
+ failedPhase: null,
1560
+ branchName: branchName === "unknown" ? null : branchName,
1561
+ storyCount,
1562
+ elapsedSeconds
1563
+ };
1564
+ }
1565
+
1566
+ // src/commands/run/phase-bootstrap.ts
1567
+ import { randomUUID } from "crypto";
1568
+
1569
+ // src/commands/run/phase-config.ts
1570
+ async function buildAgentConfiguration(prReview) {
1571
+ const agentSummary = await describeRunAgents(
1572
+ prReview ? ["prd", "plan", "execute", "review", "pr", "pr-review"] : ["prd", "plan", "execute", "review", "pr"]
1573
+ );
1574
+ const fallbacks = getActiveResilienceConfig().providers?.chain ?? [];
1575
+ const routingConfig = await loadRoutingConfig();
1576
+ const agentConfig = await loadAgentConfig();
1577
+ const displayedPhases = Object.entries(agentSummary.byPhase).map(([phase, resolved]) => {
1578
+ const recommended = routingConfig.mode === "active" && routingConfig.policy === "recommended" && !hasExplicitAgentSelection(
1579
+ agentConfig,
1580
+ getAgentCliOverrides(),
1581
+ phase
1582
+ ) ? recommendedTarget(phase) : null;
1583
+ return {
1584
+ phase,
1585
+ provider: recommended?.provider ?? resolved.provider,
1586
+ model: recommended?.model ?? resolved.model,
1587
+ providerSource: recommended ? "recommended" : resolved.origin.provider,
1588
+ modelSource: recommended ? "recommended" : resolved.origin.model
1589
+ };
1590
+ });
1591
+ const configurationSnapshot = {
1592
+ precedence: ["default", "global", "project", "env", "cli", "step override"],
1593
+ defaultProvider: {
1594
+ value: agentSummary.defaultProvider,
1595
+ source: agentSummary.defaultOrigin.provider
1596
+ },
1597
+ defaultModel: {
1598
+ value: agentSummary.defaultModel,
1599
+ source: agentSummary.defaultOrigin.model
1600
+ },
1601
+ phases: displayedPhases.map((resolved) => ({
1602
+ phase: resolved.phase,
1603
+ provider: { value: resolved.provider, source: resolved.providerSource },
1604
+ model: { value: resolved.model, source: resolved.modelSource }
1605
+ })),
1606
+ fallbacks,
1607
+ overrides: displayedPhases.filter(
1608
+ (resolved) => resolved.provider !== agentSummary.defaultProvider || resolved.model !== agentSummary.defaultModel
1609
+ ).map(
1610
+ (resolved) => `${resolved.phase}: ${resolved.provider}${resolved.model ? ` \xB7 ${resolved.model}` : ""}`
1611
+ )
1612
+ };
1613
+ return { agentSummary, configurationSnapshot };
1614
+ }
1615
+
1616
+ // src/commands/run/types.ts
1617
+ var RUNNABLE_PHASES = ["prd", "plan", "execute", "review", "pr"];
1618
+ var RUNNABLE_PHASES_NO_BRANCH = ["prd", "plan", "execute", "review"];
1619
+ var RUNNABLE_PHASES_WITH_PR_REVIEW = [...RUNNABLE_PHASES, "pr-review"];
1620
+ var QUEUE_PR_PHASES = ["init", "pr"];
1621
+ var QUEUE_PR_PHASES_WITH_REVIEW = [
1622
+ "init",
1623
+ "pr",
1624
+ "pr-review"
1625
+ ];
1626
+ var RUNNABLE_QUEUE_PR_PHASES = ["pr"];
1627
+ var RUNNABLE_QUEUE_PR_PHASES_WITH_REVIEW = ["pr", "pr-review"];
1628
+ function failure(code) {
1629
+ return { code, failedPhase: null, branchName: null, storyCount: 0, elapsedSeconds: 0 };
1630
+ }
1631
+
1632
+ // src/commands/run/phase-options.ts
1633
+ function resolveExecuteRetry(runOptions) {
1634
+ return {
1635
+ retryLimit: runOptions?.retryLimit,
1636
+ retryForever: runOptions?.retryForever
1637
+ };
1638
+ }
1639
+ function resolveStoryNumbering(runOptions) {
1640
+ return {
1641
+ continueNumbering: runOptions?.continueNumbering,
1642
+ startUs: runOptions?.startUs
1643
+ };
1644
+ }
1645
+ function resolveBranchAndReviewModes(input) {
1646
+ const warnings = [];
1647
+ if (input.persisted === null) {
1648
+ const effectiveNoBranch2 = input.noBranch ?? false;
1649
+ let effectivePrReview2 = input.prReview ?? false;
1650
+ if (effectiveNoBranch2 && effectivePrReview2) {
1651
+ warnings.push(
1652
+ "This pipeline runs with --no-branch and opens no PR. Skipping the pr-review phase."
1653
+ );
1654
+ effectivePrReview2 = false;
1655
+ }
1656
+ return {
1657
+ effectiveNoBranch: effectiveNoBranch2,
1658
+ effectivePrReview: effectivePrReview2,
1659
+ warnings,
1660
+ planIssueUrl: void 0,
1661
+ planBranch: void 0,
1662
+ planStories: []
1663
+ };
1130
1664
  }
1131
- if (webConfig.enabled) {
1132
- await ensureWebMonitor(
1133
- {
1134
- publisher,
1135
- port: webConfig.port,
1136
- host: webConfig.host,
1137
- refreshSeconds: webConfig.refreshSeconds
1138
- },
1139
- { restart: input.restartWeb === true }
1665
+ const persistedNoBranch = input.persisted.noBranch;
1666
+ let effectivePrReview = input.prReview ?? input.persisted.prReviewEnabled;
1667
+ if (input.noBranch !== void 0 && input.noBranch !== persistedNoBranch) {
1668
+ if (persistedNoBranch) {
1669
+ warnings.push(
1670
+ "This pipeline was started with --no-branch. Ignoring current flag; using persisted mode."
1671
+ );
1672
+ } else {
1673
+ warnings.push(
1674
+ "This pipeline was started without --no-branch. Ignoring current flag; using persisted mode."
1675
+ );
1676
+ }
1677
+ }
1678
+ const effectiveNoBranch = persistedNoBranch;
1679
+ if (effectiveNoBranch && effectivePrReview) {
1680
+ warnings.push(
1681
+ "This pipeline runs with --no-branch and opens no PR. Skipping the pr-review phase."
1140
1682
  );
1683
+ effectivePrReview = false;
1141
1684
  }
1142
- let result = {
1143
- code: 1,
1144
- failedPhase: null,
1145
- branchName: null,
1146
- storyCount: 0,
1147
- elapsedSeconds: 0
1685
+ return {
1686
+ effectiveNoBranch,
1687
+ effectivePrReview,
1688
+ warnings,
1689
+ planIssueUrl: input.persisted.issueUrl,
1690
+ planBranch: input.persisted.branchName,
1691
+ planStories: [...input.persisted.userStories]
1148
1692
  };
1149
- const releaseCheckpoint = onShutdown({
1150
- phase: "checkpoint",
1151
- run: async () => {
1152
- await pauseIssue(paths.tasksFile, issueNumber);
1693
+ }
1694
+ function selectPhaseLists(input) {
1695
+ const { finalPr, inQueue, effectiveNoBranch, effectivePrReview } = input;
1696
+ 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;
1697
+ 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;
1698
+ return { activePhases, phaseOrder };
1699
+ }
1700
+
1701
+ // src/commands/run/phase-bootstrap.ts
1702
+ async function runInitGate(input) {
1703
+ const { queue, issuesConfig, publishSessionStart, publisher } = input;
1704
+ const sessionStartedAt = isoNow();
1705
+ if (queue?.preChecked !== true) {
1706
+ if (isVerbose()) {
1707
+ printInfo("Running prerequisite checks...");
1708
+ }
1709
+ const initCode = await runInit(issuesConfig.preferredProvider, { compact: !isVerbose() });
1710
+ if (initCode !== 0) {
1711
+ publishSessionStart(PIPELINE_PHASES, sessionStartedAt);
1712
+ publisher.publish({ type: "phase:start", at: sessionStartedAt, phase: "init" });
1153
1713
  publisher.publish({
1154
- type: "log",
1714
+ type: "phase:end",
1155
1715
  at: isoNow(),
1156
- level: "warn",
1157
- message: `Interrupted during issue #${issueNumber}. A checkpoint was saved; resume with \`issue-flow resume ${issueNumber}\`.`
1716
+ phase: "init",
1717
+ success: false,
1718
+ error: "Prerequisites not met"
1158
1719
  });
1159
- publisher.publish({ type: "session:end", at: isoNow(), status: "failed" });
1720
+ printError("Prerequisites not met. Fix the issues above and try again.");
1721
+ return { kind: "done", result: failure(1) };
1160
1722
  }
1723
+ }
1724
+ return { kind: "ok", sessionStartedAt };
1725
+ }
1726
+ async function beginBootstrapLocals(issueNumber, paths, runOptions) {
1727
+ const { continueNumbering, startUs } = resolveStoryNumbering(runOptions);
1728
+ const executeRetry = resolveExecuteRetry(runOptions);
1729
+ const tasksPath = paths.tasksFile;
1730
+ const sessionId = randomUUID();
1731
+ bindDiagnosticContext({
1732
+ sessionId,
1733
+ issue: issueNumber,
1734
+ executionId: null,
1735
+ phase: null,
1736
+ story: null,
1737
+ harness: null,
1738
+ model: null
1161
1739
  });
1162
- const releaseClose = onShutdown({
1163
- phase: "close",
1164
- run: async () => {
1165
- await publisher.close();
1166
- }
1740
+ const [initialBranch, initialCommit] = await Promise.all([
1741
+ getCurrentBranch().catch(() => ""),
1742
+ getHeadCommit()
1743
+ ]);
1744
+ return {
1745
+ continueNumbering,
1746
+ startUs,
1747
+ executeRetry,
1748
+ tasksPath,
1749
+ sessionId,
1750
+ initialBranch,
1751
+ initialCommit
1752
+ };
1753
+ }
1754
+ async function resolvePrimaryIssue(issueNumber, queue, issuesConfig) {
1755
+ const resolution = await resolveCommandIssue(issueNumber, queue?.resolved, {
1756
+ config: issuesConfig
1757
+ });
1758
+ if (!resolution.ok) {
1759
+ return { kind: "done", result: failure(resolution.code) };
1760
+ }
1761
+ return { kind: "ok", resolvedIssue: resolution.resolved };
1762
+ }
1763
+ async function bootstrapThroughIssueResolution(issueNumber, paths, mode, publisher, input) {
1764
+ const { from, noBranch, prReview, queue } = input;
1765
+ const {
1766
+ continueNumbering,
1767
+ startUs,
1768
+ executeRetry,
1769
+ tasksPath,
1770
+ sessionId,
1771
+ initialBranch,
1772
+ initialCommit
1773
+ } = await beginBootstrapLocals(issueNumber, paths, input.runOptions);
1774
+ let publishedIssueNumber = toIssueNumber(issueNumber);
1775
+ const publishSessionStart = (phases, at, info) => {
1776
+ publisher.publish({
1777
+ type: "session:start",
1778
+ at,
1779
+ sessionId,
1780
+ issueNumber: publishedIssueNumber,
1781
+ issueUrl: info?.issueUrl,
1782
+ branch: info?.branch,
1783
+ branchCreated: info?.branchCreated,
1784
+ startCommit: info?.startCommit,
1785
+ phases: [...phases],
1786
+ configuration: configurationSnapshot,
1787
+ environment: {
1788
+ node: process.version,
1789
+ platform: process.platform,
1790
+ agent: agentSummary.defaultProvider,
1791
+ model: agentSummary.defaultModel,
1792
+ cliVersion: getPackageVersion()
1793
+ }
1794
+ });
1795
+ };
1796
+ const { agentSummary, configurationSnapshot } = await buildAgentConfiguration(prReview);
1797
+ printInfo(
1798
+ `Issue Flow v${getPackageVersion()} \xB7 starting pipeline for issue #${issueNumber} (mode: ${mode}, agent: ${agentSummary.label})`
1799
+ );
1800
+ const issuesConfig = await loadIssuesConfig();
1801
+ const initGate = await runInitGate({
1802
+ queue,
1803
+ issuesConfig,
1804
+ publishSessionStart,
1805
+ publisher
1167
1806
  });
1807
+ if (initGate.kind === "done") return initGate;
1808
+ const sessionStartedAt = initGate.sessionStartedAt;
1809
+ const resolved = await resolvePrimaryIssue(issueNumber, queue, issuesConfig);
1810
+ if (resolved.kind === "done") return resolved;
1811
+ const resolvedIssue = resolved.resolvedIssue;
1812
+ publishedIssueNumber = resolvedIssue.issue.number;
1813
+ return {
1814
+ kind: "continue",
1815
+ issueNumber,
1816
+ paths,
1817
+ mode,
1818
+ publisher,
1819
+ input,
1820
+ from,
1821
+ noBranch,
1822
+ prReview,
1823
+ queue,
1824
+ continueNumbering,
1825
+ startUs,
1826
+ executeRetry,
1827
+ tasksPath,
1828
+ sessionId,
1829
+ initialBranch,
1830
+ initialCommit,
1831
+ publishedIssueNumber,
1832
+ publishSessionStart,
1833
+ agentSummary,
1834
+ configurationSnapshot,
1835
+ sessionStartedAt,
1836
+ resolvedIssue
1837
+ };
1838
+ }
1839
+
1840
+ // src/commands/run/phase-prepare.ts
1841
+ async function loadModesAndPhases(input) {
1842
+ const { noBranch, prReview, tasksPath, queue } = input;
1843
+ let persistedFlags = null;
1168
1844
  try {
1169
- result = await runPipelinePhases(issueNumber, paths, mode, publisher, input);
1170
- if (result.code !== 0) {
1171
- await reportIfOversized(issueNumber, paths, result);
1845
+ const existingPlan = await loadTaskPlan(tasksPath);
1846
+ persistedFlags = {
1847
+ noBranch: existingPlan.noBranch ?? false,
1848
+ prReviewEnabled: existingPlan.prReview?.enabled ?? false,
1849
+ issueUrl: existingPlan.issueUrl || void 0,
1850
+ branchName: existingPlan.branchName || void 0,
1851
+ userStories: existingPlan.userStories
1852
+ };
1853
+ } catch {
1854
+ }
1855
+ const modes = resolveBranchAndReviewModes({
1856
+ noBranch,
1857
+ prReview,
1858
+ persisted: persistedFlags
1859
+ });
1860
+ for (const warning of modes.warnings) {
1861
+ printWarning(warning);
1862
+ }
1863
+ const { effectiveNoBranch, effectivePrReview } = modes;
1864
+ const planIssueUrl = modes.planIssueUrl;
1865
+ const planBranch = modes.planBranch;
1866
+ const planStories = modes.planStories;
1867
+ if (effectivePrReview) {
1868
+ try {
1869
+ const plan = await loadTaskPlan(tasksPath);
1870
+ if (plan.prReview?.enabled !== true) {
1871
+ plan.prReview = { ...plan.prReview, enabled: true, rounds: plan.prReview?.rounds ?? 0 };
1872
+ await saveTaskPlan(tasksPath, plan);
1873
+ }
1874
+ } catch {
1172
1875
  }
1173
- return result;
1174
- } finally {
1175
- releaseCheckpoint();
1176
- releaseClose();
1177
- if (result.queue === void 0) {
1178
- publisher.publish({
1179
- type: "session:end",
1180
- at: isoNow(),
1181
- status: result.code === 0 ? "completed" : "failed"
1182
- });
1876
+ }
1877
+ const inQueue = queue !== void 0;
1878
+ const finalPr = queue?.finalPr;
1879
+ const { activePhases, phaseOrder } = selectPhaseLists({
1880
+ finalPr: finalPr !== void 0,
1881
+ inQueue,
1882
+ effectiveNoBranch,
1883
+ effectivePrReview
1884
+ });
1885
+ return {
1886
+ effectiveNoBranch,
1887
+ effectivePrReview,
1888
+ planIssueUrl,
1889
+ planBranch,
1890
+ planStories,
1891
+ inQueue,
1892
+ finalPr,
1893
+ activePhases,
1894
+ phaseOrder
1895
+ };
1896
+ }
1897
+ async function openPhaseSession(input) {
1898
+ const {
1899
+ issueNumber,
1900
+ tasksPath,
1901
+ queue,
1902
+ finalPr,
1903
+ effectiveNoBranch,
1904
+ planIssueUrl,
1905
+ planStories,
1906
+ initialBranch,
1907
+ initialCommit,
1908
+ activePhases,
1909
+ sessionStartedAt,
1910
+ resolvedIssue,
1911
+ publisher,
1912
+ publishSessionStart
1913
+ } = input;
1914
+ let planBranch = input.planBranch;
1915
+ const queueCommitScope = queue === void 0 ? void 0 : `issue-${issueNumber}`;
1916
+ let producedBranch = null;
1917
+ const plannedExecutionBranch = planBranch ?? null;
1918
+ const branchExistedBeforeExecution = effectiveNoBranch || planBranch === void 0 || planBranch === null ? null : await localBranchExists(planBranch).catch(() => null);
1919
+ if (queue !== void 0 && finalPr === void 0 && !effectiveNoBranch) {
1920
+ producedBranch = await adoptQueueBranch(tasksPath, queue.plan.branchName);
1921
+ if (producedBranch !== null) {
1922
+ planBranch = producedBranch;
1183
1923
  }
1184
- await publisher.close();
1185
- writeDiagnostic({
1186
- level: result.code === 0 ? "info" : "error",
1187
- message: `Issue Flow session ${result.code === 0 ? "completed" : "failed"}`,
1188
- context: { code: result.code, failedPhase: result.failedPhase }
1189
- });
1190
- await flushDiagnostics();
1191
- setSessionPublisher(void 0);
1192
1924
  }
1925
+ publishSessionStart(activePhases, sessionStartedAt, {
1926
+ issueUrl: planIssueUrl ?? resolvedIssue.issue.remoteRef ?? void 0,
1927
+ branch: effectiveNoBranch ? initialBranch : planBranch,
1928
+ branchCreated: effectiveNoBranch ? false : branchExistedBeforeExecution === true ? false : null,
1929
+ startCommit: initialCommit
1930
+ });
1931
+ publishIssueDetails(publisher, resolvedIssue.issue, sessionStartedAt);
1932
+ publishStorySeed(publisher, planStories, sessionStartedAt);
1933
+ publisher.publish({ type: "phase:start", at: sessionStartedAt, phase: "init" });
1934
+ publisher.publish({ type: "phase:end", at: isoNow(), phase: "init", success: true });
1935
+ await publishGitState(publisher);
1936
+ return {
1937
+ queueCommitScope,
1938
+ producedBranch,
1939
+ plannedExecutionBranch,
1940
+ branchExistedBeforeExecution,
1941
+ planBranch
1942
+ };
1943
+ }
1944
+ async function maybeHandoffToQueue(input) {
1945
+ const { queue, issueNumber, requested, resolvedIssue, noBranch, prReview, runOptions } = input;
1946
+ if (queue !== void 0) return null;
1947
+ const decision = await decideQueue({
1948
+ requested: requested ?? [issueNumber],
1949
+ resolved: resolvedIssue,
1950
+ noBranch: noBranch ?? false,
1951
+ prReview: prReview ?? false,
1952
+ runOptions: runOptions ?? {}
1953
+ });
1954
+ if (decision.kind === "stop") {
1955
+ return { kind: "done", result: failure(decision.code) };
1956
+ }
1957
+ if (decision.kind === "queue") {
1958
+ return {
1959
+ kind: "done",
1960
+ result: {
1961
+ ...failure(0),
1962
+ queue: { plan: decision.plan, resumed: decision.resumed, resolved: resolvedIssue }
1963
+ }
1964
+ };
1965
+ }
1966
+ return null;
1967
+ }
1968
+ async function preparePhaseRun(issueNumber, paths, mode, publisher, input) {
1969
+ const boot = await bootstrapThroughIssueResolution(issueNumber, paths, mode, publisher, input);
1970
+ if (boot.kind === "done") return boot;
1971
+ const {
1972
+ from,
1973
+ noBranch,
1974
+ prReview,
1975
+ queue,
1976
+ continueNumbering,
1977
+ startUs,
1978
+ executeRetry,
1979
+ tasksPath,
1980
+ initialBranch,
1981
+ initialCommit,
1982
+ publishSessionStart,
1983
+ agentSummary,
1984
+ sessionStartedAt,
1985
+ resolvedIssue
1986
+ } = boot;
1987
+ const _publishedIssueNumber = boot.publishedIssueNumber;
1988
+ const handoff = await maybeHandoffToQueue({
1989
+ queue,
1990
+ issueNumber,
1991
+ requested: input.requested,
1992
+ resolvedIssue,
1993
+ noBranch,
1994
+ prReview,
1995
+ runOptions: input.runOptions
1996
+ });
1997
+ if (handoff !== null) return handoff;
1998
+ const modes = await loadModesAndPhases({ noBranch, prReview, tasksPath, queue });
1999
+ const {
2000
+ effectiveNoBranch,
2001
+ effectivePrReview,
2002
+ planIssueUrl,
2003
+ planStories,
2004
+ inQueue,
2005
+ finalPr,
2006
+ activePhases,
2007
+ phaseOrder
2008
+ } = modes;
2009
+ let planBranch = modes.planBranch;
2010
+ const opened = await openPhaseSession({
2011
+ issueNumber,
2012
+ tasksPath,
2013
+ queue,
2014
+ finalPr,
2015
+ effectiveNoBranch,
2016
+ planBranch,
2017
+ planIssueUrl,
2018
+ planStories,
2019
+ initialBranch,
2020
+ initialCommit,
2021
+ activePhases,
2022
+ sessionStartedAt,
2023
+ resolvedIssue,
2024
+ publisher,
2025
+ publishSessionStart
2026
+ });
2027
+ const { queueCommitScope, producedBranch, plannedExecutionBranch, branchExistedBeforeExecution } = opened;
2028
+ planBranch = opened.planBranch;
2029
+ return {
2030
+ kind: "ready",
2031
+ issueNumber,
2032
+ tasksPath,
2033
+ publisher,
2034
+ from,
2035
+ continueNumbering,
2036
+ startUs,
2037
+ executeRetry,
2038
+ initialBranch,
2039
+ agentSummary,
2040
+ resolvedIssue,
2041
+ effectiveNoBranch,
2042
+ effectivePrReview,
2043
+ inQueue,
2044
+ finalPr,
2045
+ activePhases,
2046
+ phaseOrder,
2047
+ queueCommitScope,
2048
+ queue,
2049
+ producedBranch,
2050
+ plannedExecutionBranch,
2051
+ branchExistedBeforeExecution
2052
+ };
1193
2053
  }
2054
+
2055
+ // src/commands/run/session.ts
2056
+ import { mkdir as mkdir3 } from "fs/promises";
2057
+
2058
+ // src/commands/run/oversized.ts
2059
+ import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
1194
2060
  async function reportIfOversized(issueNumber, paths, result) {
1195
2061
  try {
1196
2062
  const journal = `${await readFileIfPresent(paths.rotatedEventsFile)}${await readFileIfPresent(paths.eventsFile)}`;
@@ -1249,7 +2115,7 @@ async function maybeCreateSubIssues(issueNumber, plan, reportFile) {
1249
2115
  printInfo("Nothing left to split: no pending story.");
1250
2116
  return;
1251
2117
  }
1252
- const { runGenerate } = await import("./generate-PB4L7V5N.js");
2118
+ const { runGenerate } = await import("./generate-JGW5JSKB.js");
1253
2119
  for (const proposal of proposals) {
1254
2120
  const instruction = [
1255
2121
  `Create a sub-issue of #${issueNumber}: ${proposal.title}.`,
@@ -1283,366 +2149,306 @@ async function readFileIfPresent(path) {
1283
2149
  return "";
1284
2150
  }
1285
2151
  }
1286
- async function pauseIssue(tasksFile, issueNumber) {
2152
+
2153
+ // src/commands/run/session.ts
2154
+ var WRITING_PHASES = /* @__PURE__ */ new Set(["plan", "execute", "pr"]);
2155
+ async function ensureRepositoryWritable(phase) {
2156
+ if (!WRITING_PHASES.has(phase)) return;
2157
+ const preflight = await preflightRepository({ intent: "resume-same-phase" });
2158
+ if (preflight.ok) return;
2159
+ for (const line of describePreflight(preflight)) {
2160
+ printError(line);
2161
+ }
2162
+ throw new Error(`The repository is not in a state the ${phase} phase can write to`);
2163
+ }
2164
+ async function claimRunOwnership(target, detached = false) {
2165
+ let lockFile;
1287
2166
  try {
1288
- const plan = await loadTaskPlan(tasksFile);
1289
- await saveTaskPlan(tasksFile, {
1290
- ...plan,
1291
- runState: {
1292
- ...plan.runState ?? {
1293
- currentPhase: null,
1294
- attempt: 0,
1295
- lastHeartbeatAt: null,
1296
- blockedReason: null,
1297
- owner: null
1298
- },
1299
- status: "paused",
1300
- lastHeartbeatAt: isoNow()
1301
- }
1302
- });
2167
+ lockFile = (await resolveProjectPaths()).runLockFile;
1303
2168
  } catch {
1304
- printWarning(`Could not write a checkpoint for issue #${issueNumber}.`);
2169
+ return { ok: true, interruptedBy: null, release: async () => {
2170
+ } };
1305
2171
  }
1306
- }
1307
- async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
1308
- const { from, noBranch, prReview, queue } = input;
1309
- const continueNumbering = input.runOptions?.continueNumbering;
1310
- const startUs = input.runOptions?.startUs;
1311
- const executeRetry = {
1312
- retryLimit: input.runOptions?.retryLimit,
1313
- retryForever: input.runOptions?.retryForever
1314
- };
1315
- const tasksPath = paths.tasksFile;
1316
- const sessionId = randomUUID();
1317
- bindDiagnosticContext({
1318
- sessionId,
1319
- issue: issueNumber,
1320
- executionId: null,
1321
- phase: null,
1322
- story: null,
1323
- harness: null,
1324
- model: null
1325
- });
1326
- const [initialBranch, initialCommit] = await Promise.all([
1327
- getCurrentBranch().catch(() => ""),
1328
- getHeadCommit()
1329
- ]);
1330
- let publishedIssueNumber = toIssueNumber(issueNumber);
1331
- const publishSessionStart = (phases, at, info) => {
1332
- publisher.publish({
1333
- type: "session:start",
1334
- at,
1335
- sessionId,
1336
- issueNumber: publishedIssueNumber,
1337
- issueUrl: info?.issueUrl,
1338
- branch: info?.branch,
1339
- branchCreated: info?.branchCreated,
1340
- startCommit: info?.startCommit,
1341
- phases: [...phases],
1342
- configuration: configurationSnapshot,
1343
- environment: {
1344
- node: process.version,
1345
- platform: process.platform,
1346
- agent: agentSummary.defaultProvider,
1347
- model: agentSummary.defaultModel,
1348
- cliVersion: getPackageVersion()
2172
+ const result = await acquireRunLock(lockFile, { target, detached });
2173
+ if (!result.ok) return result;
2174
+ return {
2175
+ ok: true,
2176
+ interruptedBy: result.handle.reclaimedFrom,
2177
+ release: () => result.handle.release()
2178
+ };
2179
+ }
2180
+ async function pauseIssue(tasksFile, issueNumber) {
2181
+ try {
2182
+ const plan = await loadTaskPlan(tasksFile);
2183
+ await saveTaskPlan(tasksFile, {
2184
+ ...plan,
2185
+ runState: {
2186
+ ...plan.runState ?? {
2187
+ currentPhase: null,
2188
+ attempt: 0,
2189
+ lastHeartbeatAt: null,
2190
+ blockedReason: null,
2191
+ owner: null
2192
+ },
2193
+ status: "paused",
2194
+ lastHeartbeatAt: isoNow()
1349
2195
  }
1350
2196
  });
1351
- };
1352
- const agentSummary = await describeRunAgents(
1353
- prReview ? ["prd", "plan", "execute", "review", "pr", "pr-review"] : ["prd", "plan", "execute", "review", "pr"]
1354
- );
1355
- const fallbacks = getActiveResilienceConfig().providers?.chain ?? [];
1356
- const configurationSnapshot = {
1357
- precedence: ["default", "global", "project", "env", "cli", "step override"],
1358
- defaultProvider: {
1359
- value: agentSummary.defaultProvider,
1360
- source: agentSummary.defaultOrigin.provider
1361
- },
1362
- defaultModel: {
1363
- value: agentSummary.defaultModel,
1364
- source: agentSummary.defaultOrigin.model
1365
- },
1366
- phases: Object.entries(agentSummary.byPhase).map(([phase, resolved]) => ({
1367
- phase,
1368
- provider: { value: resolved.provider, source: resolved.origin.provider },
1369
- model: { value: resolved.model, source: resolved.origin.model }
1370
- })),
1371
- fallbacks,
1372
- overrides: Object.entries(agentSummary.byPhase).filter(
1373
- ([, resolved]) => resolved.provider !== agentSummary.defaultProvider || resolved.model !== agentSummary.defaultModel
1374
- ).map(
1375
- ([phase, resolved]) => `${phase}: ${resolved.provider}${resolved.model ? ` \xB7 ${resolved.model}` : ""}`
1376
- )
1377
- };
1378
- printInfo(
1379
- `Issue Flow v${getPackageVersion()} \xB7 starting pipeline for issue #${issueNumber} (mode: ${mode}, agent: ${agentSummary.label})`
1380
- );
1381
- const issuesConfig = await loadIssuesConfig();
1382
- const sessionStartedAt = isoNow();
1383
- if (queue?.preChecked !== true) {
1384
- if (isVerbose()) {
1385
- printInfo("Running prerequisite checks...");
1386
- }
1387
- const initCode = await runInit(issuesConfig.preferredProvider, { compact: !isVerbose() });
1388
- if (initCode !== 0) {
1389
- publishSessionStart(PIPELINE_PHASES, sessionStartedAt);
1390
- publisher.publish({ type: "phase:start", at: sessionStartedAt, phase: "init" });
2197
+ } catch {
2198
+ printWarning(`Could not write a checkpoint for issue #${issueNumber}.`);
2199
+ }
2200
+ }
2201
+ async function createSessionPublisher(input) {
2202
+ const { paths, persistSnapshot, journalEnabled, webConfig, maxFileBytes } = input;
2203
+ const surfaces = [];
2204
+ if (persistSnapshot || journalEnabled) {
2205
+ await mkdir3(paths.issueDir, { recursive: true });
2206
+ }
2207
+ if (persistSnapshot) {
2208
+ surfaces.push(
2209
+ new FilePublisher(paths.sessionFile, {
2210
+ logLimit: webConfig.logLimit,
2211
+ includeLogs: webConfig.includeLogs
2212
+ })
2213
+ );
2214
+ }
2215
+ if (journalEnabled) {
2216
+ surfaces.push(
2217
+ new JournalPublisher(paths.eventsFile, paths.rotatedEventsFile, {
2218
+ logLimit: webConfig.logLimit,
2219
+ includeLogs: webConfig.includeLogs,
2220
+ ...maxFileBytes === void 0 ? {} : { maxFileBytes }
2221
+ })
2222
+ );
2223
+ }
2224
+ return surfaces.length === 0 ? new MemoryPublisher() : surfaces.length === 1 ? surfaces[0] : new MultiPublisher(surfaces);
2225
+ }
2226
+ function registerIssueShutdownHooks(input) {
2227
+ const { paths, issueNumber, publisher } = input;
2228
+ const releaseCheckpoint = onShutdown({
2229
+ phase: "checkpoint",
2230
+ run: async () => {
2231
+ await pauseIssue(paths.tasksFile, issueNumber);
1391
2232
  publisher.publish({
1392
- type: "phase:end",
2233
+ type: "log",
1393
2234
  at: isoNow(),
1394
- phase: "init",
1395
- success: false,
1396
- error: "Prerequisites not met"
2235
+ level: "warn",
2236
+ message: `Interrupted during issue #${issueNumber}. A checkpoint was saved; resume with \`issue-flow resume ${issueNumber}\`.`
1397
2237
  });
1398
- printError("Prerequisites not met. Fix the issues above and try again.");
1399
- return failure(1);
2238
+ publisher.publish({ type: "session:end", at: isoNow(), status: "failed" });
1400
2239
  }
1401
- }
1402
- const resolution = await resolveCommandIssue(issueNumber, queue?.resolved, {
1403
- config: issuesConfig
1404
2240
  });
1405
- if (!resolution.ok) {
1406
- return failure(resolution.code);
1407
- }
1408
- const resolvedIssue = resolution.resolved;
1409
- publishedIssueNumber = resolvedIssue.issue.number;
1410
- if (queue === void 0) {
1411
- const decision = await decideQueue({
1412
- requested: input.requested ?? [issueNumber],
1413
- resolved: resolvedIssue,
1414
- noBranch: noBranch ?? false,
1415
- prReview: prReview ?? false,
1416
- runOptions: input.runOptions ?? {}
1417
- });
1418
- if (decision.kind === "stop") {
1419
- return failure(decision.code);
1420
- }
1421
- if (decision.kind === "queue") {
1422
- return {
1423
- ...failure(0),
1424
- queue: { plan: decision.plan, resumed: decision.resumed, resolved: resolvedIssue }
1425
- };
2241
+ const releaseClose = onShutdown({
2242
+ phase: "close",
2243
+ run: async () => {
2244
+ await publisher.close();
1426
2245
  }
2246
+ });
2247
+ return { releaseCheckpoint, releaseClose };
2248
+ }
2249
+ async function closeIssueSession(input) {
2250
+ const { releaseCheckpoint, releaseClose, result, publisher } = input;
2251
+ releaseCheckpoint();
2252
+ releaseClose();
2253
+ if (result.queue === void 0) {
2254
+ publisher.publish({
2255
+ type: "session:end",
2256
+ at: isoNow(),
2257
+ status: result.code === 0 ? "completed" : "failed"
2258
+ });
1427
2259
  }
1428
- let effectiveNoBranch = noBranch ?? false;
1429
- let effectivePrReview = prReview ?? false;
1430
- let planIssueUrl;
1431
- let planBranch;
1432
- let planStories = [];
1433
- try {
1434
- const existingPlan = await loadTaskPlan(tasksPath);
1435
- const persistedNoBranch = existingPlan.noBranch ?? false;
1436
- planIssueUrl = existingPlan.issueUrl || void 0;
1437
- planBranch = existingPlan.branchName || void 0;
1438
- planStories = existingPlan.userStories;
1439
- effectivePrReview = prReview ?? existingPlan.prReview?.enabled ?? false;
1440
- if (noBranch !== void 0 && noBranch !== persistedNoBranch) {
1441
- if (persistedNoBranch) {
1442
- printWarning(
1443
- "This pipeline was started with --no-branch. Ignoring current flag; using persisted mode."
1444
- );
1445
- } else {
1446
- printWarning(
1447
- "This pipeline was started without --no-branch. Ignoring current flag; using persisted mode."
1448
- );
1449
- }
1450
- }
1451
- effectiveNoBranch = persistedNoBranch;
1452
- } catch {
2260
+ await publisher.close();
2261
+ writeDiagnostic({
2262
+ level: result.code === 0 ? "info" : "error",
2263
+ message: `Issue Flow session ${result.code === 0 ? "completed" : "failed"}`,
2264
+ context: { code: result.code, failedPhase: result.failedPhase }
2265
+ });
2266
+ await flushDiagnostics();
2267
+ setSessionPublisher(void 0);
2268
+ }
2269
+ async function applySessionSideEffects(input) {
2270
+ const { publisher, interruptedBy, webConfig, restartWeb } = input;
2271
+ if (interruptedBy !== void 0) {
2272
+ publisher.publish({
2273
+ type: "log",
2274
+ at: isoNow(),
2275
+ level: "warn",
2276
+ message: `Previous run interrupted: ${describeRunLockOwner(interruptedBy)}. Its lock was stale and has been taken over.`
2277
+ });
1453
2278
  }
1454
- if (effectiveNoBranch && effectivePrReview) {
1455
- printWarning(
1456
- "This pipeline runs with --no-branch and opens no PR. Skipping the pr-review phase."
2279
+ if (webConfig.enabled) {
2280
+ await ensureWebMonitor(
2281
+ {
2282
+ publisher,
2283
+ port: webConfig.port,
2284
+ host: webConfig.host,
2285
+ refreshSeconds: webConfig.refreshSeconds
2286
+ },
2287
+ { restart: restartWeb === true }
1457
2288
  );
1458
- effectivePrReview = false;
1459
2289
  }
1460
- if (effectivePrReview) {
1461
- try {
1462
- const plan = await loadTaskPlan(tasksPath);
1463
- if (plan.prReview?.enabled !== true) {
1464
- plan.prReview = { ...plan.prReview, enabled: true, rounds: plan.prReview?.rounds ?? 0 };
1465
- await saveTaskPlan(tasksPath, plan);
1466
- }
1467
- } catch {
1468
- }
2290
+ }
2291
+ async function runIssueSession(issueNumber, mode, input, runPipelinePhases2) {
2292
+ resetAgentInvocationState();
2293
+ const paths = await resolveIssuePaths(issueNumber);
2294
+ try {
2295
+ const project = await resolveProjectPaths();
2296
+ bindDiagnosticContext({
2297
+ project: project.projectId,
2298
+ projectRoot: await getProjectRoot(),
2299
+ issue: issueNumber,
2300
+ sessionId: null,
2301
+ executionId: null,
2302
+ phase: null,
2303
+ story: null,
2304
+ harness: null,
2305
+ model: null
2306
+ });
2307
+ } catch {
1469
2308
  }
1470
- const inQueue = queue !== void 0;
1471
- const finalPr = queue?.finalPr;
1472
- 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;
1473
- 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;
1474
- const queueCommitScope = queue === void 0 ? void 0 : `issue-${issueNumber}`;
1475
- let producedBranch = null;
1476
- let plannedExecutionBranch = planBranch ?? null;
1477
- let branchExistedBeforeExecution = effectiveNoBranch || planBranch === void 0 || planBranch === null ? null : await localBranchExists(planBranch).catch(() => null);
1478
- if (queue !== void 0 && finalPr === void 0 && !effectiveNoBranch) {
1479
- producedBranch = await adoptQueueBranch(tasksPath, queue.plan.branchName);
1480
- if (producedBranch !== null) {
1481
- planBranch = producedBranch;
1482
- }
2309
+ const resilience = await initResilienceConfig();
2310
+ const configuredInactivity = resilience.watchdog?.inactivityTimeoutMs;
2311
+ if (configuredInactivity !== void 0 && getInactivityTimeout() === void 0) {
2312
+ setInactivityTimeout(configuredInactivity);
1483
2313
  }
1484
- publishSessionStart(activePhases, sessionStartedAt, {
1485
- issueUrl: planIssueUrl ?? resolvedIssue.issue.remoteRef ?? void 0,
1486
- branch: effectiveNoBranch ? initialBranch : planBranch,
1487
- branchCreated: effectiveNoBranch ? false : branchExistedBeforeExecution === true ? false : null,
1488
- startCommit: initialCommit
2314
+ const webConfig = await loadWebConfig();
2315
+ const journalEnabled = resilience.journal?.enabled === true;
2316
+ const persistSnapshot = webConfig.enabled || input.runOptions?.detachedChild === true;
2317
+ const publisher = await createSessionPublisher({
2318
+ paths,
2319
+ persistSnapshot,
2320
+ journalEnabled,
2321
+ webConfig,
2322
+ maxFileBytes: resilience.journal?.maxFileBytes
1489
2323
  });
1490
- publishIssueDetails(publisher, resolvedIssue.issue, sessionStartedAt);
1491
- publishStorySeed(publisher, planStories, sessionStartedAt);
1492
- publisher.publish({ type: "phase:start", at: sessionStartedAt, phase: "init" });
1493
- publisher.publish({ type: "phase:end", at: isoNow(), phase: "init", success: true });
1494
- await publishGitState(publisher);
1495
- let startPhase = "prd";
1496
- if (from) {
1497
- if (!activePhases.includes(from)) {
1498
- const validPhases = activePhases.filter((p) => p !== "init").join(", ");
1499
- if (effectiveNoBranch && PIPELINE_PHASES.includes(from)) {
1500
- printError(
1501
- `The '${from}' phase is not available in --no-branch mode. Valid phases: ${validPhases}`
1502
- );
1503
- } else {
1504
- printError(`Invalid phase: ${from}. Valid phases: ${validPhases}`);
1505
- }
1506
- return failure(1);
1507
- }
1508
- startPhase = from;
1509
- } else {
1510
- try {
1511
- const plan = await loadTaskPlan(tasksPath);
1512
- const mgr = new PipelineManager(plan, tasksPath, activePhases);
1513
- const nextPhase = mgr.getNextPhase();
1514
- if (nextPhase && nextPhase !== "init") {
1515
- startPhase = nextPhase;
1516
- printInfo(`Resuming from phase: ${startPhase}`);
1517
- }
1518
- } catch {
2324
+ setSessionPublisher(publisher);
2325
+ await applySessionSideEffects({
2326
+ publisher,
2327
+ interruptedBy: input.interruptedBy,
2328
+ webConfig,
2329
+ restartWeb: input.restartWeb
2330
+ });
2331
+ let result = {
2332
+ code: 1,
2333
+ failedPhase: null,
2334
+ branchName: null,
2335
+ storyCount: 0,
2336
+ elapsedSeconds: 0
2337
+ };
2338
+ const { releaseCheckpoint, releaseClose } = registerIssueShutdownHooks({
2339
+ paths,
2340
+ issueNumber,
2341
+ publisher
2342
+ });
2343
+ try {
2344
+ result = await runPipelinePhases2(issueNumber, paths, mode, publisher, input);
2345
+ if (result.code !== 0) {
2346
+ await reportIfOversized(issueNumber, paths, result);
1519
2347
  }
2348
+ return result;
2349
+ } finally {
2350
+ await closeIssueSession({ releaseCheckpoint, releaseClose, result, publisher });
1520
2351
  }
1521
- if (from) {
2352
+ }
2353
+
2354
+ // src/commands/run/phase-runners.ts
2355
+ function createPlanRunner(input, makeRunner) {
2356
+ const {
2357
+ issueNumber,
2358
+ tasksPath,
2359
+ publisher,
2360
+ resolvedIssue,
2361
+ queue,
2362
+ continueNumbering,
2363
+ startUs,
2364
+ effectiveNoBranch,
2365
+ effectivePrReview,
2366
+ initialBranch,
2367
+ branchState
2368
+ } = input;
2369
+ return async () => {
2370
+ await makeRunner(
2371
+ () => runPlan(issueNumber, resolvedIssue, {
2372
+ continueFlag: continueNumbering,
2373
+ startUs,
2374
+ ...effectiveNoBranch && initialBranch ? { branchName: initialBranch } : {}
2375
+ }),
2376
+ "plan"
2377
+ )();
1522
2378
  try {
1523
2379
  const plan = await loadTaskPlan(tasksPath);
1524
- const mgr = new PipelineManager(plan, tasksPath, activePhases);
1525
- if (!mgr.canResume(startPhase)) {
1526
- printError(`Cannot resume from ${startPhase}: prerequisite phases not complete`);
1527
- return failure(1);
1528
- }
1529
- } catch {
1530
- if (startPhase !== "prd") {
1531
- printError(`Cannot resume from ${startPhase}: no pipeline state found`);
1532
- return failure(1);
2380
+ if (!effectiveNoBranch) {
2381
+ branchState.plannedExecutionBranch = plan.branchName;
2382
+ branchState.branchExistedBeforeExecution = await localBranchExists(plan.branchName).catch(
2383
+ () => null
2384
+ );
2385
+ publisher.publish({
2386
+ type: "git:update",
2387
+ at: isoNow(),
2388
+ branchCreated: branchState.branchExistedBeforeExecution === true ? false : null
2389
+ });
1533
2390
  }
1534
- }
1535
- }
1536
- const startIdx = Math.max(phaseOrder.indexOf(startPhase), 0);
1537
- const makeRunner = (fn, phase) => async () => {
1538
- await ensureRepositoryWritable(phase);
1539
- const code = await fn();
1540
- if (code !== 0) {
1541
- throw new Error(`Phase ${phase} failed with exit code ${code}`);
2391
+ publishStorySeed(publisher, plan.userStories, isoNow());
2392
+ if (effectiveNoBranch || effectivePrReview) {
2393
+ if (effectiveNoBranch) plan.noBranch = true;
2394
+ if (effectivePrReview) {
2395
+ plan.prReview = { ...plan.prReview, enabled: true, rounds: plan.prReview?.rounds ?? 0 };
2396
+ }
2397
+ await saveTaskPlan(tasksPath, plan);
2398
+ }
2399
+ } catch {
2400
+ }
2401
+ if (queue !== void 0 && !effectiveNoBranch) {
2402
+ branchState.producedBranch = await adoptQueueBranch(tasksPath, queue.plan.branchName);
1542
2403
  }
1543
2404
  };
1544
- const runners = {
1545
- prd: makeRunner(() => runPrd(issueNumber, resolvedIssue), "prd"),
1546
- plan: async () => {
1547
- await makeRunner(
1548
- () => runPlan(issueNumber, resolvedIssue, {
1549
- continueFlag: continueNumbering,
1550
- startUs,
1551
- ...effectiveNoBranch && initialBranch ? { branchName: initialBranch } : {}
1552
- }),
1553
- "plan"
1554
- )();
2405
+ }
2406
+ function createReviewRunner(input) {
2407
+ const { issueNumber, tasksPath, publisher, resolvedIssue, queueCommitScope, executeRetry } = input;
2408
+ return async () => {
2409
+ let maxCycles = 3;
2410
+ try {
2411
+ const plan = await loadTaskPlan(tasksPath);
2412
+ maxCycles = plan.maxCorrectionCycles;
2413
+ } catch {
2414
+ }
2415
+ let code = await runReview(issueNumber, resolvedIssue);
2416
+ let cycle = 0;
2417
+ while (code !== 0 && cycle < maxCycles) {
2418
+ cycle++;
2419
+ printWarning(`Review failed. Starting correction cycle ${cycle}/${maxCycles}...`);
2420
+ publisher.publish({ type: "correction:cycle", at: isoNow(), cycle, maxCycles });
1555
2421
  try {
1556
2422
  const plan = await loadTaskPlan(tasksPath);
1557
- if (!effectiveNoBranch) {
1558
- plannedExecutionBranch = plan.branchName;
1559
- branchExistedBeforeExecution = await localBranchExists(plan.branchName).catch(() => null);
1560
- publisher.publish({
1561
- type: "git:update",
1562
- at: isoNow(),
1563
- branchCreated: branchExistedBeforeExecution === true ? false : null
1564
- });
1565
- }
1566
- publishStorySeed(publisher, plan.userStories, isoNow());
1567
- if (effectiveNoBranch || effectivePrReview) {
1568
- if (effectiveNoBranch) plan.noBranch = true;
1569
- if (effectivePrReview) {
1570
- plan.prReview = { ...plan.prReview, enabled: true, rounds: plan.prReview?.rounds ?? 0 };
1571
- }
1572
- await saveTaskPlan(tasksPath, plan);
1573
- }
2423
+ plan.correctionCycle = cycle;
2424
+ await saveTaskPlan(tasksPath, plan);
1574
2425
  } catch {
1575
2426
  }
1576
- if (queue !== void 0 && !effectiveNoBranch) {
1577
- producedBranch = await adoptQueueBranch(tasksPath, queue.plan.branchName);
1578
- }
1579
- },
1580
- execute: makeRunner(
1581
- () => runExecute(void 0, {
2427
+ const execCode = await runExecute(void 0, {
1582
2428
  issue: issueNumber,
1583
2429
  commitScope: queueCommitScope,
1584
2430
  ...executeRetry
1585
- }),
1586
- "execute"
1587
- ),
1588
- review: async () => {
1589
- let maxCycles = 3;
1590
- try {
1591
- const plan = await loadTaskPlan(tasksPath);
1592
- maxCycles = plan.maxCorrectionCycles;
1593
- } catch {
1594
- }
1595
- let code = await runReview(issueNumber, resolvedIssue);
1596
- let cycle = 0;
1597
- while (code !== 0 && cycle < maxCycles) {
1598
- cycle++;
1599
- printWarning(`Review failed. Starting correction cycle ${cycle}/${maxCycles}...`);
1600
- publisher.publish({ type: "correction:cycle", at: isoNow(), cycle, maxCycles });
1601
- try {
1602
- const plan = await loadTaskPlan(tasksPath);
1603
- plan.correctionCycle = cycle;
1604
- await saveTaskPlan(tasksPath, plan);
1605
- } catch {
1606
- }
1607
- const execCode = await runExecute(void 0, {
1608
- issue: issueNumber,
1609
- commitScope: queueCommitScope,
1610
- ...executeRetry
1611
- });
1612
- if (execCode !== 0) {
1613
- throw new Error("Correction execution failed");
1614
- }
1615
- code = await runReview(issueNumber, resolvedIssue);
1616
- }
1617
- if (code !== 0) {
1618
- throw new Error(`Review failed after ${maxCycles} correction cycles`);
2431
+ });
2432
+ if (execCode !== 0) {
2433
+ throw new Error("Correction execution failed");
1619
2434
  }
1620
- },
1621
- pr: makeRunner(
1622
- // The options argument is omitted outside a queue so a standalone run
1623
- // calls `runPr` exactly as it always did.
1624
- () => finalPr === void 0 ? runPr(issueNumber, resolvedIssue) : runPr(issueNumber, resolvedIssue, { queue: finalPr }),
1625
- "pr"
1626
- )
2435
+ code = await runReview(issueNumber, resolvedIssue);
2436
+ }
2437
+ if (code !== 0) {
2438
+ throw new Error(`Review failed after ${maxCycles} correction cycles`);
2439
+ }
1627
2440
  };
1628
- const reviewBox = { outcome: null };
1629
- if (effectivePrReview) {
1630
- runners["pr-review"] = async () => {
1631
- const code = await runPrReview(void 0, { issue: issueNumber, yes: true });
1632
- if (code === 1) {
1633
- throw new Error("Phase pr-review failed with exit code 1");
1634
- }
1635
- reviewBox.outcome = await readPrReviewOutcome(issueNumber, tasksPath, code === 2);
1636
- };
1637
- }
1638
- const instrumentedRunners = Object.fromEntries(
2441
+ }
2442
+ function instrumentPhaseRunners(runners, input) {
2443
+ const { publisher, issueNumber, branchState } = input;
2444
+ return Object.fromEntries(
1639
2445
  Object.entries(runners).map(([phase, fn]) => [
1640
2446
  phase,
1641
2447
  async () => {
1642
2448
  publisher.publish({ type: "phase:start", at: isoNow(), phase });
1643
2449
  try {
1644
2450
  await fn();
1645
- if (phase === "execute" && branchExistedBeforeExecution === false && plannedExecutionBranch !== null && await getCurrentBranch().catch(() => "") === plannedExecutionBranch) {
2451
+ if (phase === "execute" && branchState.branchExistedBeforeExecution === false && branchState.plannedExecutionBranch !== null && await getCurrentBranch().catch(() => "") === branchState.plannedExecutionBranch) {
1646
2452
  publisher.publish({ type: "git:update", at: isoNow(), branchCreated: true });
1647
2453
  }
1648
2454
  await publishGitState(publisher);
@@ -1661,447 +2467,257 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
1661
2467
  }
1662
2468
  ])
1663
2469
  );
1664
- const phaseSuffixes = {};
1665
- for (const [phase, resolved] of Object.entries(agentSummary.byPhase)) {
1666
- if (resolved.provider !== agentSummary.defaultProvider) {
1667
- phaseSuffixes[phase] = resolved.model ? `${resolved.provider} \xB7 ${resolved.model}` : resolved.provider;
1668
- }
1669
- }
1670
- const result = await runPipelineWithRenderer({
1671
- phases: phaseOrder,
1672
- startIndex: startIdx,
1673
- verbose: isVerbose(),
1674
- runners: instrumentedRunners,
1675
- tasksPath,
1676
- phaseSuffixes
1677
- });
1678
- if (!result.success) {
1679
- printError(`Phase ${result.failedPhase} failed`);
1680
- return {
1681
- ...failure(1),
1682
- failedPhase: result.failedPhase ?? null,
1683
- branchName: producedBranch,
1684
- elapsedSeconds: result.overallElapsedSeconds
1685
- };
1686
- }
1687
- const review = reviewBox.outcome;
1688
- if (review?.requestedChanges) {
1689
- console.log("");
1690
- printWarning("PR review requested changes \u2014 the Pull Request is not ready to merge.");
1691
- if (review.reportPath !== null) {
1692
- console.log(` Report: ${review.reportPath}`);
1693
- }
1694
- }
1695
- if (review?.requestedChanges) {
1696
- printInfo("Issue left open until the review blockers are addressed.");
1697
- } else if (!inQueue) {
1698
- await closeIssue(issueNumber, resolvedIssue.source);
1699
- }
1700
- let branchName = "unknown";
1701
- try {
1702
- const proc = await execa("git", ["branch", "--show-current"], { reject: false });
1703
- branchName = proc.stdout?.toString().trim() ?? "unknown";
1704
- } catch {
1705
- }
1706
- let storyCount = 0;
1707
- let planPrUrl = null;
1708
- try {
1709
- const plan = await loadTaskPlan(tasksPath);
1710
- storyCount = plan.userStories.length;
1711
- planPrUrl = plan.pullRequest?.url ?? null;
1712
- plan.lastAttemptAt = isoNow();
1713
- if (!review?.requestedChanges) {
1714
- plan.issueStatus = "completed";
1715
- plan.completedAt = isoNow();
1716
- }
1717
- await saveTaskPlan(tasksPath, plan);
1718
- } catch {
1719
- }
1720
- if (inQueue) {
1721
- if (finalPr === void 0) {
1722
- printSuccess(`Issue #${issueNumber} completed (${storyCount} stories).`);
1723
- }
1724
- return {
1725
- code: 0,
1726
- failedPhase: null,
1727
- branchName: producedBranch ?? (branchName === "unknown" ? null : branchName),
1728
- storyCount,
1729
- elapsedSeconds: result.overallElapsedSeconds,
1730
- review
1731
- };
1732
- }
1733
- let prUrl = "unknown";
1734
- if (!effectiveNoBranch && resolvedIssue.source === "github") {
1735
- if (planPrUrl !== null) {
1736
- prUrl = planPrUrl;
1737
- } else if (branchName !== "unknown" && branchName !== "") {
1738
- try {
1739
- const latest = mostRecent(await listPullRequests(branchName));
1740
- if (latest !== null) {
1741
- prUrl = latest.url;
1742
- }
1743
- } catch {
1744
- }
1745
- }
1746
- }
1747
- printRunSummary({
1748
- issueNumber,
1749
- branchName,
1750
- noBranch: effectiveNoBranch,
1751
- storyCount,
1752
- elapsedSeconds: result.overallElapsedSeconds,
1753
- prUrl,
1754
- prReview: review,
1755
- // Process-owned counters: the session snapshot is empty whenever web
1756
- // monitoring is off, so it cannot be the source of these totals.
1757
- usage: getRunUsageTotals(),
1758
- verification: verificationForSummary(getSessionPublisher().snapshot().verification)
1759
- });
1760
- return {
1761
- code: 0,
1762
- failedPhase: null,
1763
- branchName: branchName === "unknown" ? null : branchName,
1764
- storyCount,
1765
- elapsedSeconds: result.overallElapsedSeconds
1766
- };
1767
- }
1768
- async function detachAfterConfirm(requested, noBranch, prReview, options) {
1769
- const childFlags = [...process.argv.slice(2)];
1770
- if (options.yes !== true && options.only !== true) {
1771
- try {
1772
- const issuesConfig = await loadIssuesConfig();
1773
- const resolution = await resolveCommandIssue(requested[0], void 0, {
1774
- config: issuesConfig
1775
- });
1776
- if (resolution.ok) {
1777
- const decision = await decideQueue({
1778
- requested,
1779
- resolved: resolution.resolved,
1780
- noBranch: noBranch ?? false,
1781
- prReview: prReview ?? false,
1782
- runOptions: options
1783
- });
1784
- if (decision.kind === "stop") return decision.code;
1785
- if (decision.kind === "queue" && !childFlags.includes("--yes")) childFlags.push("--yes");
1786
- if (decision.kind === "single" && !childFlags.includes("--only")) childFlags.push("--only");
1787
- }
1788
- } catch {
1789
- }
1790
- }
1791
- const paths = await resolveIssuePaths(requested[0]);
1792
- const { pid, logFile } = await spawnDetachedRun({ paths, argv: childFlags });
1793
- printSuccess(`Detached run started (pid ${pid}).`);
1794
- printInfo(`Log: ${logFile}`);
1795
- printInfo("Follow with: issue-flow ps");
1796
- printInfo(`Stop with: kill ${pid}`);
1797
- return 0;
1798
- }
1799
- async function decideQueue(input) {
1800
- const primary = input.requested[0];
1801
- const single = input.requested.length === 1;
1802
- if (single && input.runOptions.only === true) {
1803
- return { kind: "single" };
1804
- }
1805
- try {
1806
- const [project, queuePaths] = await Promise.all([
1807
- resolveProjectPaths(),
1808
- resolveQueuePaths(primary)
1809
- ]);
1810
- return await planQueue({
1811
- requested: input.requested,
1812
- source: input.resolved.source,
1813
- known: [input.resolved.issue],
1814
- projectId: project.projectId,
1815
- planFile: queuePaths.planFile,
1816
- noBranch: input.noBranch,
1817
- prReview: input.prReview,
1818
- confirm: {
1819
- yes: input.runOptions.yes,
1820
- only: input.runOptions.only,
1821
- cascade: input.runOptions.cascade
1822
- }
1823
- });
1824
- } catch (err) {
1825
- const message = err instanceof Error ? err.message : String(err);
1826
- if (single) {
1827
- printWarning(
1828
- `Could not inspect related issues (${message}). Running issue #${primary} alone.`
1829
- );
1830
- return { kind: "single" };
1831
- }
1832
- printError(`Could not plan the execution of ${input.requested.length} issues: ${message}`);
1833
- return { kind: "stop", code: 1 };
1834
- }
1835
2470
  }
1836
- async function runQueue(initialPlan, options, resolvedPrimary) {
1837
- const planFile = (await resolveQueuePaths(initialPlan.id)).planFile;
1838
- let plan = initialPlan;
1839
- const queueUsage = beginUsageScope();
1840
- const startedAtMs = Date.now();
1841
- const summaries = [];
1842
- let firstOfThisRun = true;
1843
- const resilience = getActiveResilienceConfig();
1844
- const failureMode = options.runOptions?.onIssueFailure ?? resilience.queue?.onIssueFailure ?? "stop";
1845
- const maxIssueAttempts = resilience.queue?.maxIssueAttempts ?? DEFAULT_MAX_ISSUE_ATTEMPTS;
1846
- const exhausted = /* @__PURE__ */ new Set();
1847
- const failedIds = /* @__PURE__ */ new Set();
1848
- try {
1849
- while (true) {
1850
- const entry = nextQueueIssue(plan, { exclude: exhausted });
1851
- if (entry === null) break;
1852
- plan = markQueueIssueInProgress(plan, entry.id);
1853
- await saveExecutionPlan(planFile, plan);
1854
- printInfo(
1855
- `Queue ${entry.position}/${plan.issues.length}: issue #${entry.id}${entry.title === "" ? "" : ` \u2014 ${entry.title}`}`
1856
- );
1857
- const issueUsage = beginUsageScope();
1858
- let result;
1859
- try {
1860
- result = await runIssueSession(entry.id, options.mode, {
1861
- // `--from` addresses the issue the queue is resuming, never the ones
1862
- // that come after it — those start from their own beginning.
1863
- from: firstOfThisRun ? options.from : void 0,
1864
- noBranch: options.noBranch,
1865
- // `--start-us` belongs to the first issue this invocation runs; the
1866
- // ones after it continue from the history those plans just wrote.
1867
- runOptions: {
1868
- ...options.runOptions,
1869
- startUs: firstOfThisRun ? options.runOptions?.startUs : void 0
1870
- },
1871
- queue: {
1872
- plan,
1873
- preChecked: true,
1874
- resolved: entry.id === initialPlan.id ? resolvedPrimary : void 0
1875
- }
1876
- });
1877
- } finally {
1878
- firstOfThisRun = false;
1879
- issueUsage.end();
1880
- }
1881
- const usage = issueUsage.totals();
1882
- if (result.code !== 0) {
1883
- const failure2 = {
1884
- phase: result.failedPhase,
1885
- error: {
1886
- category: "queue_issue_failed",
1887
- message: `Issue #${entry.id} failed${result.failedPhase === null ? "" : ` in phase ${result.failedPhase}`}`,
1888
- at: isoNow()
1889
- }
1890
- };
1891
- const where = result.failedPhase === null ? "" : ` (phase ${result.failedPhase})`;
1892
- if (failureMode === "stop") {
1893
- plan = markQueueIssueFailed(plan, entry.id, failure2);
1894
- await saveExecutionPlan(planFile, plan);
1895
- printError(
1896
- `Queue stopped at issue #${entry.id}${where}. The branch and every commit made so far were kept.`
1897
- );
1898
- printInfo(`Resume with: issue-flow run ${plan.requested.join(",")}`);
1899
- return result.code;
1900
- }
1901
- if (failureMode === "block") {
1902
- plan = markQueueIssueBlocked(
1903
- plan,
1904
- entry.id,
1905
- `Failed${where} and --on-issue-failure block was in force`,
1906
- failure2
1907
- );
1908
- await saveExecutionPlan(planFile, plan);
1909
- exhausted.add(entry.id);
1910
- failedIds.add(entry.id);
1911
- printWarning(
1912
- `Issue #${entry.id} failed${where} and is blocked for review. Continuing with the rest of the queue.`
1913
- );
1914
- continue;
1915
- }
1916
- const attempts = entry.attempts + 1;
1917
- if (attempts >= maxIssueAttempts) {
1918
- plan = markQueueIssueFailed(plan, entry.id, failure2);
1919
- exhausted.add(entry.id);
1920
- failedIds.add(entry.id);
1921
- printWarning(
1922
- `Issue #${entry.id} failed${where} after ${attempts} attempt(s). Continuing with the rest of the queue.`
1923
- );
1924
- } else {
1925
- plan = markQueueIssueSkipped(plan, entry.id, failure2);
1926
- printWarning(
1927
- `Issue #${entry.id} failed${where}. Skipping it for now and coming back at the end of the queue.`
1928
- );
1929
- }
1930
- await saveExecutionPlan(planFile, plan);
1931
- continue;
1932
- }
1933
- if (plan.branchName === null && result.branchName !== null) {
1934
- plan = setQueueBranch(plan, result.branchName);
1935
- }
1936
- plan = markQueueIssueCompleted(plan, entry.id);
1937
- await saveExecutionPlan(planFile, plan);
1938
- summaries.push({
1939
- id: entry.id,
1940
- title: entry.title,
1941
- storyCount: result.storyCount,
1942
- elapsedSeconds: result.elapsedSeconds,
1943
- usage
1944
- });
1945
- }
1946
- const code = await finishQueue(
1947
- plan,
1948
- planFile,
1949
- summaries,
1950
- startedAtMs,
1951
- queueUsage,
1952
- options,
1953
- resolvedPrimary
1954
- );
1955
- if (failedIds.size > 0) {
1956
- printError(
1957
- `${failedIds.size} issue(s) did not finish: ${[...failedIds].map((id) => `#${id}`).join(", ")}.`
1958
- );
1959
- printInfo(`Resume with: issue-flow run ${plan.requested.join(",")}`);
1960
- return code === 0 ? 1 : code;
2471
+ function buildInstrumentedPhaseRunners(input) {
2472
+ const {
2473
+ issueNumber,
2474
+ tasksPath,
2475
+ publisher,
2476
+ resolvedIssue,
2477
+ finalPr,
2478
+ executeRetry,
2479
+ effectivePrReview,
2480
+ queueCommitScope,
2481
+ branchState
2482
+ } = input;
2483
+ const makeRunner = (fn, phase) => async () => {
2484
+ await ensureRepositoryWritable(phase);
2485
+ const code = await fn();
2486
+ if (code !== 0) {
2487
+ throw new Error(`Phase ${phase} failed with exit code ${code}`);
1961
2488
  }
1962
- return code;
1963
- } finally {
1964
- queueUsage.end();
2489
+ };
2490
+ const runners = {
2491
+ prd: makeRunner(() => runPrd(issueNumber, resolvedIssue), "prd"),
2492
+ plan: createPlanRunner(input, makeRunner),
2493
+ execute: makeRunner(
2494
+ () => runExecute(void 0, {
2495
+ issue: issueNumber,
2496
+ commitScope: queueCommitScope,
2497
+ ...executeRetry
2498
+ }),
2499
+ "execute"
2500
+ ),
2501
+ review: createReviewRunner(input),
2502
+ pr: makeRunner(
2503
+ // The options argument is omitted outside a queue so a standalone run
2504
+ // calls `runPr` exactly as it always did.
2505
+ () => finalPr === void 0 ? runPr(issueNumber, resolvedIssue) : runPr(issueNumber, resolvedIssue, { queue: finalPr }),
2506
+ "pr"
2507
+ )
2508
+ };
2509
+ const reviewBox = { outcome: null };
2510
+ if (effectivePrReview) {
2511
+ runners["pr-review"] = async () => {
2512
+ const code = await runPrReview(void 0, { issue: issueNumber, yes: true });
2513
+ if (code === 1) {
2514
+ throw new Error("Phase pr-review failed with exit code 1");
2515
+ }
2516
+ reviewBox.outcome = await readPrReviewOutcome(issueNumber, tasksPath, code === 2);
2517
+ };
1965
2518
  }
2519
+ const instrumentedRunners = instrumentPhaseRunners(runners, {
2520
+ publisher,
2521
+ issueNumber,
2522
+ branchState
2523
+ });
2524
+ return { instrumentedRunners, reviewBox };
1966
2525
  }
1967
- async function finishQueue(plan, planFile, summaries, startedAtMs, queueUsage, options, resolvedPrimary) {
1968
- let current = plan;
1969
- let review = null;
1970
- const alreadyOpened = current.pullRequest !== void 0 || await primaryPrCreated(current);
1971
- if (!current.noBranch && options.noBranch !== true && !alreadyOpened) {
1972
- const outcome = await runIssueSession(current.id, options.mode, {
1973
- prReview: options.prReview ?? current.prReview,
1974
- queue: {
1975
- plan: current,
1976
- preChecked: true,
1977
- resolved: resolvedPrimary,
1978
- finalPr: await buildPrQueueContext(current)
2526
+
2527
+ // src/commands/run/phase-start.ts
2528
+ async function resolveStartPhase(input) {
2529
+ const { from, activePhases, effectiveNoBranch, tasksPath } = input;
2530
+ let startPhase = "prd";
2531
+ if (from) {
2532
+ if (!activePhases.includes(from)) {
2533
+ const validPhases = activePhases.filter((p) => p !== "init").join(", ");
2534
+ if (effectiveNoBranch && PIPELINE_PHASES.includes(from)) {
2535
+ printError(
2536
+ `The '${from}' phase is not available in --no-branch mode. Valid phases: ${validPhases}`
2537
+ );
2538
+ } else {
2539
+ printError(`Invalid phase: ${from}. Valid phases: ${validPhases}`);
1979
2540
  }
1980
- });
1981
- review = outcome.review ?? null;
1982
- if (outcome.code !== 0) {
1983
- printError("The consolidated Pull Request could not be created.");
1984
- await saveExecutionPlan(planFile, current);
1985
- return outcome.code;
1986
- }
1987
- const pullRequest = await propagatePullRequest(current);
1988
- if (pullRequest !== null) {
1989
- current = setQueuePullRequest(current, pullRequest);
2541
+ return { ok: false };
1990
2542
  }
1991
- }
1992
- await saveExecutionPlan(planFile, current);
1993
- if (review?.requestedChanges === true) {
1994
- printInfo("Issues left open until the review blockers are addressed.");
2543
+ startPhase = from;
1995
2544
  } else {
1996
- for (const entry of current.issues) {
1997
- await closeIssue(entry.id, entry.source);
1998
- }
1999
- }
2000
- printQueueSummary({
2001
- queueId: current.id,
2002
- branchName: current.branchName,
2003
- issues: summaries,
2004
- excluded: current.excluded.map((entry) => ({ id: entry.id, title: entry.title })),
2005
- elapsedSeconds: Math.round((Date.now() - startedAtMs) / 1e3),
2006
- prUrl: current.pullRequest?.url ?? null,
2007
- // Read here, after the consolidated Pull Request (and the optional
2008
- // pr-review) already ran: reading it before would report a queue total
2009
- // that leaves out the phase that just spent tokens.
2010
- usage: queueUsage.totals(),
2011
- prReview: review
2012
- });
2013
- return 0;
2014
- }
2015
- async function primaryPrCreated(plan) {
2016
- try {
2017
- const paths = await resolveIssuePaths(plan.id);
2018
- return (await loadTaskPlan(paths.tasksFile)).pipeline.prCreated;
2019
- } catch {
2020
- return false;
2021
- }
2022
- }
2023
- async function buildPrQueueContext(plan) {
2024
- const pending = [];
2025
- for (const entry of plan.issues) {
2026
2545
  try {
2027
- const paths = await resolveIssuePaths(entry.id);
2028
- const taskPlan = await loadTaskPlan(paths.tasksFile);
2029
- if (taskPlan.lastReviewFindings !== null && taskPlan.lastReviewFindings !== "") {
2030
- pending.push(`Issue #${entry.id} has unresolved review findings`);
2546
+ const plan = await loadTaskPlan(tasksPath);
2547
+ const mgr = new PipelineManager(plan, tasksPath, activePhases);
2548
+ const nextPhase = mgr.getNextPhase();
2549
+ if (nextPhase && nextPhase !== "init") {
2550
+ startPhase = nextPhase;
2551
+ printInfo(`Resuming from phase: ${startPhase}`);
2031
2552
  }
2032
2553
  } catch {
2033
2554
  }
2034
2555
  }
2035
- return {
2036
- issues: plan.issues.map((entry) => ({
2037
- id: entry.id,
2038
- number: entry.number,
2039
- title: entry.title,
2040
- url: entry.url,
2041
- source: entry.source,
2042
- parent: entry.parent,
2043
- role: entry.role,
2044
- complete: entry.status === "completed"
2045
- })),
2046
- excluded: plan.excluded.map((entry) => ({
2047
- id: entry.id,
2048
- number: entry.number,
2049
- title: entry.title,
2050
- reason: entry.reason
2051
- })),
2052
- pending
2053
- };
2054
- }
2055
- async function propagatePullRequest(plan) {
2056
- let pullRequest = null;
2057
- try {
2058
- const primaryPaths = await resolveIssuePaths(plan.id);
2059
- pullRequest = (await loadTaskPlan(primaryPaths.tasksFile)).pullRequest ?? null;
2060
- } catch {
2061
- return null;
2062
- }
2063
- if (pullRequest === null) return null;
2064
- for (const entry of plan.issues) {
2065
- if (entry.id === plan.id) continue;
2556
+ if (from) {
2066
2557
  try {
2067
- const paths = await resolveIssuePaths(entry.id);
2068
- const taskPlan = await loadTaskPlan(paths.tasksFile);
2069
- taskPlan.pullRequest = pullRequest;
2070
- taskPlan.pipeline.prCreated = true;
2071
- await saveTaskPlan(paths.tasksFile, taskPlan);
2558
+ const plan = await loadTaskPlan(tasksPath);
2559
+ const mgr = new PipelineManager(plan, tasksPath, activePhases);
2560
+ if (!mgr.canResume(startPhase)) {
2561
+ printError(`Cannot resume from ${startPhase}: prerequisite phases not complete`);
2562
+ return { ok: false };
2563
+ }
2072
2564
  } catch {
2073
- printWarning(`Could not record the Pull Request on issue #${entry.id}'s task plan.`);
2565
+ if (startPhase !== "prd") {
2566
+ printError(`Cannot resume from ${startPhase}: no pipeline state found`);
2567
+ return { ok: false };
2568
+ }
2074
2569
  }
2075
2570
  }
2076
- return pullRequest;
2571
+ return { ok: true, startPhase };
2077
2572
  }
2078
- function failure(code) {
2079
- return { code, failedPhase: null, branchName: null, storyCount: 0, elapsedSeconds: 0 };
2573
+
2574
+ // src/commands/run/phases.ts
2575
+ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
2576
+ const prepared = await preparePhaseRun(issueNumber, paths, mode, publisher, input);
2577
+ if (prepared.kind === "done") {
2578
+ return prepared.result;
2579
+ }
2580
+ const {
2581
+ tasksPath,
2582
+ from,
2583
+ continueNumbering,
2584
+ startUs,
2585
+ executeRetry,
2586
+ initialBranch,
2587
+ agentSummary,
2588
+ resolvedIssue,
2589
+ effectiveNoBranch,
2590
+ effectivePrReview,
2591
+ inQueue,
2592
+ finalPr,
2593
+ activePhases,
2594
+ phaseOrder,
2595
+ queueCommitScope,
2596
+ queue
2597
+ } = prepared;
2598
+ let { producedBranch, plannedExecutionBranch, branchExistedBeforeExecution } = prepared;
2599
+ const startResolved = await resolveStartPhase({
2600
+ from,
2601
+ activePhases,
2602
+ effectiveNoBranch,
2603
+ tasksPath
2604
+ });
2605
+ if (!startResolved.ok) {
2606
+ return failure(1);
2607
+ }
2608
+ const startPhase = startResolved.startPhase;
2609
+ const startIdx = Math.max(phaseOrder.indexOf(startPhase), 0);
2610
+ const branchState = {
2611
+ producedBranch,
2612
+ plannedExecutionBranch,
2613
+ branchExistedBeforeExecution
2614
+ };
2615
+ const { instrumentedRunners, reviewBox } = buildInstrumentedPhaseRunners({
2616
+ issueNumber,
2617
+ tasksPath,
2618
+ publisher,
2619
+ resolvedIssue,
2620
+ queue,
2621
+ finalPr,
2622
+ continueNumbering,
2623
+ startUs,
2624
+ executeRetry,
2625
+ effectiveNoBranch,
2626
+ effectivePrReview,
2627
+ initialBranch,
2628
+ queueCommitScope,
2629
+ branchState
2630
+ });
2631
+ const phaseSuffixes = phaseSuffixesFor(agentSummary);
2632
+ const result = await runPipelineWithRenderer({
2633
+ phases: phaseOrder,
2634
+ startIndex: startIdx,
2635
+ verbose: isVerbose(),
2636
+ runners: instrumentedRunners,
2637
+ tasksPath,
2638
+ phaseSuffixes
2639
+ });
2640
+ producedBranch = branchState.producedBranch;
2641
+ plannedExecutionBranch = branchState.plannedExecutionBranch;
2642
+ branchExistedBeforeExecution = branchState.branchExistedBeforeExecution;
2643
+ if (!result.success) {
2644
+ printError(`Phase ${result.failedPhase} failed`);
2645
+ return {
2646
+ ...failure(1),
2647
+ failedPhase: result.failedPhase ?? null,
2648
+ branchName: producedBranch,
2649
+ elapsedSeconds: result.overallElapsedSeconds
2650
+ };
2651
+ }
2652
+ return finalizeSuccessfulIssueRun({
2653
+ issueNumber,
2654
+ tasksPath,
2655
+ resolvedIssue,
2656
+ review: reviewBox.outcome,
2657
+ inQueue,
2658
+ finalPr,
2659
+ producedBranch,
2660
+ effectiveNoBranch,
2661
+ elapsedSeconds: result.overallElapsedSeconds
2662
+ });
2080
2663
  }
2081
- async function closeIssue(issueNumber, source) {
2082
- try {
2083
- const provider = getProvider(source);
2084
- if (provider.close !== void 0) {
2085
- printInfo("Closing issue...");
2086
- await provider.close(issueNumber);
2664
+ function phaseSuffixesFor(agentSummary) {
2665
+ const phaseSuffixes = {};
2666
+ for (const [phase, resolved] of Object.entries(agentSummary.byPhase)) {
2667
+ if (resolved.provider !== agentSummary.defaultProvider) {
2668
+ phaseSuffixes[phase] = resolved.model ? `${resolved.provider} \xB7 ${resolved.model}` : resolved.provider;
2087
2669
  }
2088
- } catch {
2089
- printWarning("Failed to close issue automatically");
2090
2670
  }
2671
+ return phaseSuffixes;
2091
2672
  }
2092
- async function adoptQueueBranch(tasksPath, queueBranch) {
2673
+
2674
+ // src/commands/run.ts
2675
+ var runIssueSession2 = (issueNumber, mode, input) => runIssueSession(issueNumber, mode, input, runPipelinePhases);
2676
+ async function runPipeline(issue, mode, from, noBranch, prReview, options = {}) {
2677
+ let requested;
2093
2678
  try {
2094
- const plan = await loadTaskPlan(tasksPath);
2095
- if (queueBranch === null || queueBranch === "") {
2096
- return plan.branchName === "" ? null : plan.branchName;
2679
+ requested = parseIssueArguments(typeof issue === "string" ? [issue] : issue);
2680
+ } catch (err) {
2681
+ printError(err instanceof Error ? err.message : String(err));
2682
+ return 1;
2683
+ }
2684
+ if (options.background === true && options.detachedChild !== true) {
2685
+ const reason = backgroundRejection(mode);
2686
+ if (reason !== null) {
2687
+ printError(reason);
2688
+ return 1;
2097
2689
  }
2098
- if (plan.branchName !== queueBranch) {
2099
- plan.branchName = queueBranch;
2100
- await saveTaskPlan(tasksPath, plan);
2690
+ return detachAfterConfirm(requested, noBranch, prReview, options);
2691
+ }
2692
+ const ownership = await claimRunOwnership(requested[0], options.detachedChild === true);
2693
+ if (!ownership.ok) {
2694
+ printError(
2695
+ `Another issue-flow run owns this project: ${describeRunLockOwner(ownership.owner)}.`
2696
+ );
2697
+ printInfo("Wait for it to finish, or stop that process before running again.");
2698
+ return 1;
2699
+ }
2700
+ try {
2701
+ const first = await runIssueSession2(requested[0], mode, {
2702
+ from,
2703
+ noBranch,
2704
+ prReview,
2705
+ requested,
2706
+ runOptions: options,
2707
+ restartWeb: options.restartWeb,
2708
+ ...ownership.interruptedBy === null ? {} : { interruptedBy: ownership.interruptedBy }
2709
+ });
2710
+ if (first.queue === void 0) {
2711
+ return first.code;
2101
2712
  }
2102
- return queueBranch;
2103
- } catch {
2104
- return queueBranch;
2713
+ return runQueue(
2714
+ first.queue.plan,
2715
+ { mode, from, noBranch, prReview, runOptions: options },
2716
+ first.queue.resolved,
2717
+ runIssueSession2
2718
+ );
2719
+ } finally {
2720
+ await ownership.release();
2105
2721
  }
2106
2722
  }
2107
2723
 
@@ -2110,4 +2726,4 @@ export {
2110
2726
  publishIssueDetails,
2111
2727
  runPipeline
2112
2728
  };
2113
- //# sourceMappingURL=chunk-OLUOZLU5.js.map
2729
+ //# sourceMappingURL=chunk-CWSQVHVV.js.map