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