issue-flow 0.16.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 (165) hide show
  1. package/dist/agent-TB6W7PT7.js +30 -0
  2. package/dist/{analyze-556KCJKZ.js → analyze-6M3KEOX7.js} +22 -22
  3. package/dist/{apply-WVV7G7WN.js → apply-ADB3ZK7W.js} +14 -14
  4. package/dist/{bench-UBFGKUHR.js → bench-FM7I7EIV.js} +23 -23
  5. package/dist/{chunk-YA6FWFYW.js → chunk-2SUFAE6X.js} +53 -11
  6. package/dist/chunk-2SUFAE6X.js.map +1 -0
  7. package/dist/{chunk-DBEMD34J.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-QZOBR6YF.js → chunk-6YQT3SSM.js} +4 -4
  11. package/dist/chunk-6YQT3SSM.js.map +1 -0
  12. package/dist/{chunk-OGVWJO4S.js → chunk-7F7LGMVO.js} +3 -3
  13. package/dist/{chunk-X6GY3CM2.js → chunk-A4H7P7T7.js} +102 -102
  14. package/dist/chunk-A4H7P7T7.js.map +1 -0
  15. package/dist/{chunk-OGN2NX57.js → chunk-ANI2NTR4.js} +9 -9
  16. package/dist/{chunk-VK65RNFA.js → chunk-B3TSS7BT.js} +3 -3
  17. package/dist/chunk-D6ZBGJ3L.js +321 -0
  18. package/dist/chunk-D6ZBGJ3L.js.map +1 -0
  19. package/dist/{chunk-MXCOSPLJ.js → chunk-DGRTJD5R.js} +3 -3
  20. package/dist/{chunk-ROIIEVZH.js → chunk-DUS5ZNKL.js} +474 -361
  21. package/dist/chunk-DUS5ZNKL.js.map +1 -0
  22. package/dist/{chunk-VTF4RWS5.js → chunk-DV7HUC4Z.js} +7 -7
  23. package/dist/{chunk-OOX3RMD3.js → chunk-E6KXCV3T.js} +4 -4
  24. package/dist/{chunk-OLUOZLU5.js → chunk-HJBF4WD2.js} +1653 -1071
  25. package/dist/chunk-HJBF4WD2.js.map +1 -0
  26. package/dist/{chunk-4FUWWBTP.js → chunk-HLAPOUEM.js} +8 -8
  27. package/dist/{chunk-J2LJB2OH.js → chunk-HY5HLFDU.js} +630 -571
  28. package/dist/chunk-HY5HLFDU.js.map +1 -0
  29. package/dist/{chunk-QVLIIGBU.js → chunk-ICM463EO.js} +60 -13
  30. package/dist/chunk-ICM463EO.js.map +1 -0
  31. package/dist/{chunk-KV2K7MK2.js → chunk-ISZ54SVE.js} +9 -9
  32. package/dist/{chunk-GV2DABNZ.js → chunk-KZ5EY5XX.js} +12 -12
  33. package/dist/{chunk-SW2MF47D.js → chunk-KZXQWC7U.js} +19 -19
  34. package/dist/{chunk-2H43OVTU.js → chunk-LPWPFIIB.js} +11 -11
  35. package/dist/{chunk-EPFC4LHT.js → chunk-MI5TREN3.js} +9 -9
  36. package/dist/{chunk-R42YHLDV.js → chunk-PPZ3K4YM.js} +2 -2
  37. package/dist/{chunk-H3EYBJZU.js → chunk-QMIQR7EE.js} +10 -10
  38. package/dist/{chunk-FKWYD44M.js → chunk-QPGEFLMY.js} +2 -2
  39. package/dist/chunk-RCZJQUTC.js +196 -0
  40. package/dist/chunk-RCZJQUTC.js.map +1 -0
  41. package/dist/{chunk-NQCR3HSU.js → chunk-RNFUVKC3.js} +3 -3
  42. package/dist/{chunk-G5LACAXO.js → chunk-RYOUZ6EK.js} +2 -2
  43. package/dist/{chunk-BSBJXPLC.js → chunk-SGTTR4XW.js} +10 -10
  44. package/dist/{chunk-GVOZ6NDO.js → chunk-SOM4QA6C.js} +3 -3
  45. package/dist/{chunk-73TS2H42.js → chunk-UK4E2FUA.js} +132 -34
  46. package/dist/chunk-UK4E2FUA.js.map +1 -0
  47. package/dist/{chunk-PGN2VX4Y.js → chunk-UWDCIFTP.js} +7 -7
  48. package/dist/{chunk-MCLPSL6B.js → chunk-VE3IYNDN.js} +3 -3
  49. package/dist/{chunk-IAMIBZRF.js → chunk-WCCZJL7W.js} +2 -2
  50. package/dist/{chunk-SVDEWOUD.js → chunk-XZUEFTUP.js} +6 -6
  51. package/dist/{chunk-GPLP7EPL.js → chunk-YII4U7ZD.js} +10 -10
  52. package/dist/{chunk-GPLP7EPL.js.map → chunk-YII4U7ZD.js.map} +1 -1
  53. package/dist/{chunk-WLJVL25O.js → chunk-YUNBOUQH.js} +6 -6
  54. package/dist/{chunk-WNROH2YE.js → chunk-YVPZUN6X.js} +2 -2
  55. package/dist/{chunk-Q2TZA6AC.js → chunk-Z5O2XBWA.js} +14 -14
  56. package/dist/cli.js +57 -49
  57. package/dist/cli.js.map +1 -1
  58. package/dist/{config-UE7IP36J.js → config-KGZUPAUP.js} +13 -13
  59. package/dist/{conventions-Z3QC2NSG.js → conventions-RYN7OXJN.js} +15 -15
  60. package/dist/{diagnostics-2Z5HFQOO.js → diagnostics-HTCWF7F4.js} +7 -7
  61. package/dist/execute-QL3ZQIAP.js +37 -0
  62. package/dist/{generate-PB4L7V5N.js → generate-CX6KNHZS.js} +22 -22
  63. package/dist/{git-7MUOGVRG.js → git-A2IP32PQ.js} +2 -2
  64. package/dist/init-PDVQDRS6.js +24 -0
  65. package/dist/{operations-QDJAFZUN.js → operations-CAKIT2BO.js} +15 -15
  66. package/dist/{permissions-XVXXENVP.js → permissions-G4XCAESF.js} +4 -4
  67. package/dist/plan-PRORGS3L.js +34 -0
  68. package/dist/{policy-6WQZB6B2.js → policy-WX6O2JID.js} +14 -14
  69. package/dist/pr-27RMQI5F.js +39 -0
  70. package/dist/pr-review-VNESMAMT.js +32 -0
  71. package/dist/prd-ZNC4T5BS.js +33 -0
  72. package/dist/{ps-CX7756QG.js → ps-76L6ZAZL.js} +12 -12
  73. package/dist/{recorder-RK6XZYF3.js → recorder-N7U66HCO.js} +7 -7
  74. package/dist/{registry-POSB35Q4.js → registry-XLHHYLTR.js} +5 -5
  75. package/dist/{resume-OQJZV3JH.js → resume-VLWIE264.js} +39 -38
  76. package/dist/{resume-OQJZV3JH.js.map → resume-VLWIE264.js.map} +1 -1
  77. package/dist/review-7CREJDKB.js +35 -0
  78. package/dist/routing-VCDYHSXA.js +33 -0
  79. package/dist/run-3MPORLF5.js +56 -0
  80. package/dist/{session-publisher-VVXOCJ2S.js → session-publisher-KAJQRHNZ.js} +3 -2
  81. package/dist/{state-manager-TIC73GQY.js → state-manager-UPHO2EA2.js} +4 -4
  82. package/dist/state-manager-UPHO2EA2.js.map +1 -0
  83. package/dist/{usage-WDDNSIHP.js → usage-U7QNGCPK.js} +12 -12
  84. package/dist/{web-IPGY52AK.js → web-HUNDHCJ4.js} +20 -18
  85. package/dist/{web-IPGY52AK.js.map → web-HUNDHCJ4.js.map} +1 -1
  86. package/package.json +6 -5
  87. package/web/AGENTS.md +10 -11
  88. package/web/public/app.css +38 -0
  89. package/web/public/app.js +277 -56
  90. package/dist/agent-PMWCDWOW.js +0 -30
  91. package/dist/chunk-73TS2H42.js.map +0 -1
  92. package/dist/chunk-7Z4Y3VKM.js +0 -26
  93. package/dist/chunk-7Z4Y3VKM.js.map +0 -1
  94. package/dist/chunk-AOAQU7PD.js +0 -180
  95. package/dist/chunk-AOAQU7PD.js.map +0 -1
  96. package/dist/chunk-J2LJB2OH.js.map +0 -1
  97. package/dist/chunk-OLUOZLU5.js.map +0 -1
  98. package/dist/chunk-QVLIIGBU.js.map +0 -1
  99. package/dist/chunk-QZOBR6YF.js.map +0 -1
  100. package/dist/chunk-ROIIEVZH.js.map +0 -1
  101. package/dist/chunk-X6GY3CM2.js.map +0 -1
  102. package/dist/chunk-YA6FWFYW.js.map +0 -1
  103. package/dist/execute-QYLJ5L4T.js +0 -36
  104. package/dist/init-XBERAZ4O.js +0 -24
  105. package/dist/plan-RXOW32KY.js +0 -34
  106. package/dist/pr-3WFYAVA7.js +0 -39
  107. package/dist/pr-review-RK63ZEMM.js +0 -32
  108. package/dist/prd-LRV6IW3E.js +0 -33
  109. package/dist/review-AEFISJZD.js +0 -35
  110. package/dist/routing-EA5NJGNU.js +0 -94
  111. package/dist/routing-EA5NJGNU.js.map +0 -1
  112. package/dist/run-HSC7R33Y.js +0 -55
  113. /package/dist/{agent-PMWCDWOW.js.map → agent-TB6W7PT7.js.map} +0 -0
  114. /package/dist/{analyze-556KCJKZ.js.map → analyze-6M3KEOX7.js.map} +0 -0
  115. /package/dist/{apply-WVV7G7WN.js.map → apply-ADB3ZK7W.js.map} +0 -0
  116. /package/dist/{bench-UBFGKUHR.js.map → bench-FM7I7EIV.js.map} +0 -0
  117. /package/dist/{chunk-DBEMD34J.js.map → chunk-32JSYXEK.js.map} +0 -0
  118. /package/dist/{chunk-OGVWJO4S.js.map → chunk-7F7LGMVO.js.map} +0 -0
  119. /package/dist/{chunk-OGN2NX57.js.map → chunk-ANI2NTR4.js.map} +0 -0
  120. /package/dist/{chunk-VK65RNFA.js.map → chunk-B3TSS7BT.js.map} +0 -0
  121. /package/dist/{chunk-MXCOSPLJ.js.map → chunk-DGRTJD5R.js.map} +0 -0
  122. /package/dist/{chunk-VTF4RWS5.js.map → chunk-DV7HUC4Z.js.map} +0 -0
  123. /package/dist/{chunk-OOX3RMD3.js.map → chunk-E6KXCV3T.js.map} +0 -0
  124. /package/dist/{chunk-4FUWWBTP.js.map → chunk-HLAPOUEM.js.map} +0 -0
  125. /package/dist/{chunk-KV2K7MK2.js.map → chunk-ISZ54SVE.js.map} +0 -0
  126. /package/dist/{chunk-GV2DABNZ.js.map → chunk-KZ5EY5XX.js.map} +0 -0
  127. /package/dist/{chunk-SW2MF47D.js.map → chunk-KZXQWC7U.js.map} +0 -0
  128. /package/dist/{chunk-2H43OVTU.js.map → chunk-LPWPFIIB.js.map} +0 -0
  129. /package/dist/{chunk-EPFC4LHT.js.map → chunk-MI5TREN3.js.map} +0 -0
  130. /package/dist/{chunk-R42YHLDV.js.map → chunk-PPZ3K4YM.js.map} +0 -0
  131. /package/dist/{chunk-H3EYBJZU.js.map → chunk-QMIQR7EE.js.map} +0 -0
  132. /package/dist/{chunk-FKWYD44M.js.map → chunk-QPGEFLMY.js.map} +0 -0
  133. /package/dist/{chunk-NQCR3HSU.js.map → chunk-RNFUVKC3.js.map} +0 -0
  134. /package/dist/{chunk-G5LACAXO.js.map → chunk-RYOUZ6EK.js.map} +0 -0
  135. /package/dist/{chunk-BSBJXPLC.js.map → chunk-SGTTR4XW.js.map} +0 -0
  136. /package/dist/{chunk-GVOZ6NDO.js.map → chunk-SOM4QA6C.js.map} +0 -0
  137. /package/dist/{chunk-PGN2VX4Y.js.map → chunk-UWDCIFTP.js.map} +0 -0
  138. /package/dist/{chunk-MCLPSL6B.js.map → chunk-VE3IYNDN.js.map} +0 -0
  139. /package/dist/{chunk-IAMIBZRF.js.map → chunk-WCCZJL7W.js.map} +0 -0
  140. /package/dist/{chunk-SVDEWOUD.js.map → chunk-XZUEFTUP.js.map} +0 -0
  141. /package/dist/{chunk-WLJVL25O.js.map → chunk-YUNBOUQH.js.map} +0 -0
  142. /package/dist/{chunk-WNROH2YE.js.map → chunk-YVPZUN6X.js.map} +0 -0
  143. /package/dist/{chunk-Q2TZA6AC.js.map → chunk-Z5O2XBWA.js.map} +0 -0
  144. /package/dist/{config-UE7IP36J.js.map → config-KGZUPAUP.js.map} +0 -0
  145. /package/dist/{conventions-Z3QC2NSG.js.map → conventions-RYN7OXJN.js.map} +0 -0
  146. /package/dist/{diagnostics-2Z5HFQOO.js.map → diagnostics-HTCWF7F4.js.map} +0 -0
  147. /package/dist/{execute-QYLJ5L4T.js.map → execute-QL3ZQIAP.js.map} +0 -0
  148. /package/dist/{generate-PB4L7V5N.js.map → generate-CX6KNHZS.js.map} +0 -0
  149. /package/dist/{git-7MUOGVRG.js.map → git-A2IP32PQ.js.map} +0 -0
  150. /package/dist/{init-XBERAZ4O.js.map → init-PDVQDRS6.js.map} +0 -0
  151. /package/dist/{operations-QDJAFZUN.js.map → operations-CAKIT2BO.js.map} +0 -0
  152. /package/dist/{permissions-XVXXENVP.js.map → permissions-G4XCAESF.js.map} +0 -0
  153. /package/dist/{plan-RXOW32KY.js.map → plan-PRORGS3L.js.map} +0 -0
  154. /package/dist/{policy-6WQZB6B2.js.map → policy-WX6O2JID.js.map} +0 -0
  155. /package/dist/{pr-3WFYAVA7.js.map → pr-27RMQI5F.js.map} +0 -0
  156. /package/dist/{pr-review-RK63ZEMM.js.map → pr-review-VNESMAMT.js.map} +0 -0
  157. /package/dist/{prd-LRV6IW3E.js.map → prd-ZNC4T5BS.js.map} +0 -0
  158. /package/dist/{ps-CX7756QG.js.map → ps-76L6ZAZL.js.map} +0 -0
  159. /package/dist/{recorder-RK6XZYF3.js.map → recorder-N7U66HCO.js.map} +0 -0
  160. /package/dist/{registry-POSB35Q4.js.map → registry-XLHHYLTR.js.map} +0 -0
  161. /package/dist/{review-AEFISJZD.js.map → review-7CREJDKB.js.map} +0 -0
  162. /package/dist/{run-HSC7R33Y.js.map → routing-VCDYHSXA.js.map} +0 -0
  163. /package/dist/{session-publisher-VVXOCJ2S.js.map → run-3MPORLF5.js.map} +0 -0
  164. /package/dist/{state-manager-TIC73GQY.js.map → session-publisher-KAJQRHNZ.js.map} +0 -0
  165. /package/dist/{usage-WDDNSIHP.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-VK65RNFA.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-Q2TZA6AC.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-OGN2NX57.js";
22
+ } from "./chunk-ANI2NTR4.js";
16
23
  import {
17
24
  runReview
18
- } from "./chunk-EPFC4LHT.js";
25
+ } from "./chunk-MI5TREN3.js";
19
26
  import {
20
27
  PIPELINE_PHASES,
21
28
  PIPELINE_PHASES_NO_BRANCH,
@@ -37,65 +44,65 @@ import {
37
44
  saveExecutionPlan,
38
45
  setQueueBranch,
39
46
  setQueuePullRequest
40
- } from "./chunk-X6GY3CM2.js";
47
+ } from "./chunk-A4H7P7T7.js";
41
48
  import {
42
49
  runInit
43
- } from "./chunk-KV2K7MK2.js";
50
+ } from "./chunk-ISZ54SVE.js";
44
51
  import {
45
52
  printQueueSummary,
46
53
  printRunSummary,
47
54
  runExecute,
48
55
  stripMarkdown
49
- } from "./chunk-SW2MF47D.js";
56
+ } from "./chunk-KZXQWC7U.js";
50
57
  import {
51
58
  ensureWebMonitor
52
- } from "./chunk-YA6FWFYW.js";
59
+ } from "./chunk-2SUFAE6X.js";
53
60
  import {
54
61
  runPlan
55
- } from "./chunk-GV2DABNZ.js";
56
- import {
57
- runPr
58
- } from "./chunk-2H43OVTU.js";
59
- import {
60
- listPullRequests,
61
- publishGitState
62
- } from "./chunk-WLJVL25O.js";
62
+ } from "./chunk-KZ5EY5XX.js";
63
63
  import {
64
64
  resolveCommandIssue
65
- } from "./chunk-MCLPSL6B.js";
65
+ } from "./chunk-VE3IYNDN.js";
66
66
  import {
67
67
  getProvider
68
- } from "./chunk-H3EYBJZU.js";
68
+ } from "./chunk-QMIQR7EE.js";
69
69
  import {
70
70
  beginUsageScope,
71
71
  getRunUsageTotals
72
- } from "./chunk-4FUWWBTP.js";
72
+ } from "./chunk-HLAPOUEM.js";
73
73
  import {
74
74
  assessDecomposition,
75
75
  buildDecompositionReport,
76
76
  proposeSubIssues,
77
77
  resetAgentInvocationState
78
- } from "./chunk-73TS2H42.js";
78
+ } from "./chunk-UK4E2FUA.js";
79
79
  import {
80
80
  JournalPublisher,
81
81
  MultiPublisher
82
- } from "./chunk-IAMIBZRF.js";
82
+ } from "./chunk-WCCZJL7W.js";
83
+ import {
84
+ recommendedTarget
85
+ } from "./chunk-D6ZBGJ3L.js";
83
86
  import {
84
87
  getPackageVersion
85
88
  } from "./chunk-C5TL7T4V.js";
86
89
  import {
87
90
  describeRunAgents,
88
91
  getActiveResilienceConfig,
92
+ getAgentCliOverrides,
93
+ hasExplicitAgentSelection,
89
94
  initResilienceConfig,
95
+ loadAgentConfig,
90
96
  loadIssuesConfig,
97
+ loadRoutingConfig,
91
98
  loadWebConfig,
92
99
  onShutdown
93
- } from "./chunk-J2LJB2OH.js";
100
+ } from "./chunk-HY5HLFDU.js";
94
101
  import {
95
102
  resolveIssuePaths,
96
103
  resolveProjectPaths,
97
104
  resolveQueuePaths
98
- } from "./chunk-VTF4RWS5.js";
105
+ } from "./chunk-DV7HUC4Z.js";
99
106
  import {
100
107
  formatDuration,
101
108
  getIcons,
@@ -110,18 +117,12 @@ import {
110
117
  setOutputCallback,
111
118
  setStoryStageCallback,
112
119
  setStoryUpdateCallback
113
- } from "./chunk-SVDEWOUD.js";
114
- import {
115
- FilePublisher,
116
- MemoryPublisher,
117
- getSessionPublisher,
118
- setSessionPublisher
119
- } from "./chunk-ROIIEVZH.js";
120
+ } from "./chunk-XZUEFTUP.js";
120
121
  import {
121
122
  bindDiagnosticContext,
122
123
  flushDiagnostics,
123
124
  writeDiagnostic
124
- } from "./chunk-FKWYD44M.js";
125
+ } from "./chunk-QPGEFLMY.js";
125
126
  import {
126
127
  committedStoryIds,
127
128
  describePreflight,
@@ -132,22 +133,22 @@ import {
132
133
  localBranchExists,
133
134
  preflightRepository,
134
135
  run
135
- } from "./chunk-WNROH2YE.js";
136
+ } from "./chunk-YVPZUN6X.js";
137
+ import {
138
+ FilePublisher,
139
+ MemoryPublisher,
140
+ getSessionPublisher,
141
+ setSessionPublisher
142
+ } from "./chunk-DUS5ZNKL.js";
136
143
  import {
137
144
  isoNow,
138
145
  loadTaskPlan,
139
146
  saveTaskPlan
140
- } from "./chunk-GVOZ6NDO.js";
147
+ } from "./chunk-SOM4QA6C.js";
141
148
  import {
142
149
  acquireRunLock,
143
150
  describeRunLockOwner
144
- } from "./chunk-QVLIIGBU.js";
145
-
146
- // src/commands/run.ts
147
- import { randomUUID } from "crypto";
148
- import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
149
- import { join } from "path";
150
- import { execa } from "execa";
151
+ } from "./chunk-ICM463EO.js";
151
152
 
152
153
  // src/execution/detach.ts
153
154
  import { spawn } from "child_process";
@@ -219,6 +220,42 @@ async function spawnDetachedRun(input) {
219
220
  }
220
221
  }
221
222
 
223
+ // src/issues/args.ts
224
+ var IssueArgumentError = class extends Error {
225
+ constructor(message) {
226
+ super(message);
227
+ this.name = "IssueArgumentError";
228
+ }
229
+ };
230
+ var INVALID_ID = /[/\\\s]/;
231
+ function parseIssueArguments(values) {
232
+ const ids = [];
233
+ const seen = /* @__PURE__ */ new Set();
234
+ for (const value of values) {
235
+ for (const raw of value.split(",")) {
236
+ const id = raw.trim().replace(/^#/, "").trim();
237
+ if (id === "") {
238
+ if (raw.trim() !== "" || value.includes(",")) {
239
+ throw new IssueArgumentError(
240
+ `Invalid issue list '${value}': it has an empty entry. Use '42,43' or '42 43'.`
241
+ );
242
+ }
243
+ continue;
244
+ }
245
+ if (INVALID_ID.test(id) || id === "." || id === "..") {
246
+ throw new IssueArgumentError(`Invalid issue identifier '${raw.trim()}'.`);
247
+ }
248
+ if (seen.has(id)) continue;
249
+ seen.add(id);
250
+ ids.push(id);
251
+ }
252
+ }
253
+ if (ids.length === 0) {
254
+ throw new IssueArgumentError("No issue was informed. Pass at least one issue number.");
255
+ }
256
+ return ids;
257
+ }
258
+
222
259
  // src/execution/confirm.ts
223
260
  import { createInterface } from "readline";
224
261
  var MAX_PROMPT_ATTEMPTS = 3;
@@ -605,134 +642,528 @@ async function planQueue(input) {
605
642
  return { kind: "queue", plan, resumed: false };
606
643
  }
607
644
 
608
- // src/issues/args.ts
609
- var IssueArgumentError = class extends Error {
610
- constructor(message) {
611
- super(message);
612
- 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;
613
652
  }
614
- };
615
- var INVALID_ID = /[/\\\s]/;
616
- function parseIssueArguments(values) {
617
- const ids = [];
618
- const seen = /* @__PURE__ */ new Set();
619
- for (const value of values) {
620
- for (const raw of value.split(",")) {
621
- const id = raw.trim().replace(/^#/, "").trim();
622
- if (id === "") {
623
- if (raw.trim() !== "" || value.includes(",")) {
624
- throw new IssueArgumentError(
625
- `Invalid issue list '${value}': it has an empty entry. Use '42,43' or '42 43'.`
626
- );
627
- }
628
- continue;
629
- }
630
- if (INVALID_ID.test(id) || id === "." || id === "..") {
631
- throw new IssueArgumentError(`Invalid issue identifier '${raw.trim()}'.`);
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`);
632
662
  }
633
- if (seen.has(id)) continue;
634
- seen.add(id);
635
- ids.push(id);
663
+ } catch {
636
664
  }
637
665
  }
638
- if (ids.length === 0) {
639
- throw new IssueArgumentError("No issue was informed. Pass at least one issue number.");
640
- }
641
- 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
+ };
642
685
  }
643
-
644
- // src/ui/pipeline-renderer.ts
645
- import { Listr, PRESET_TIMER, PRESET_TIMESTAMP } from "listr2";
646
-
647
- // src/ui/status-view.ts
648
- function renderExecuteFocus(snapshot, options = {}) {
649
- const icons = options.icons ?? getIcons();
650
- const story = activeStory(snapshot);
651
- if (story === void 0 && snapshot.currentActivity === null) return [];
652
- const lines = [];
653
- if (story !== void 0) {
654
- const duration = story.durationSeconds !== null && story.durationSeconds !== void 0 ? ` ${formatDuration(story.durationSeconds)}` : "";
655
- lines.push(` ${icons.pending} ${story.id} ${stripMarkdown(story.title)}${duration}`);
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;
656
693
  }
657
- const activity = snapshot.currentActivity;
658
- if (activity?.tool) {
659
- const detail = activity.detail ? ` ${stripMarkdown(activity.detail)}` : "";
660
- 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
+ }
661
706
  }
662
- return lines;
707
+ return pullRequest;
663
708
  }
664
- function activeStory(snapshot) {
665
- const fromActivity = snapshot.currentActivity?.story;
666
- if (fromActivity) {
667
- const match = snapshot.stories.find((story) => story.id === fromActivity);
668
- if (match) return match;
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");
669
718
  }
670
- return snapshot.stories.find((story) => story.stage === "executing");
671
719
  }
672
720
 
673
- // src/ui/pipeline-renderer.ts
674
- var PHASE_LABELS = {
675
- prd: "PRD",
676
- plan: "Plan",
677
- execute: "Execute",
678
- review: "Review",
679
- pr: "PR",
680
- "pr-review": "PR Review"
681
- };
682
- function phaseLabel(phase) {
683
- return PHASE_LABELS[phase] ?? phase.charAt(0).toUpperCase() + phase.slice(1);
684
- }
685
- function selectRenderer(verbose) {
686
- const isTTY = !!process.stdout.isTTY;
687
- const isCI = !!process.env.CI;
688
- if (!isTTY || isCI) {
689
- return "simple";
690
- }
691
- return verbose ? "verbose" : "default";
692
- }
693
- function storySubtaskTitle(story, executing) {
694
- const base = `${story.id}: ${story.title}`;
695
- return executing ? `${base} \u2192 Executando...` : base;
696
- }
697
- function createActiveStoryTracker(stories, storyTasks) {
698
- let activeStoryId;
699
- const retitle = (storyId, executing) => {
700
- const task = storyTasks.get(storyId);
701
- if (!task) return;
702
- task.title = storySubtaskTitle(
703
- stories.find((s) => s.id === storyId) ?? { id: storyId, title: "" },
704
- executing
705
- );
706
- };
707
- return {
708
- setActive(storyId) {
709
- if (activeStoryId !== void 0 && activeStoryId !== storyId) {
710
- retitle(activeStoryId, false);
711
- }
712
- activeStoryId = storyId;
713
- if (storyId !== void 0) retitle(storyId, true);
714
- },
715
- clear() {
716
- if (activeStoryId === void 0) return;
717
- retitle(activeStoryId, false);
718
- activeStoryId = void 0;
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()
719
731
  }
720
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 };
721
774
  }
722
- function executeExpandsStories(verbose) {
723
- return verbose;
724
- }
725
- function paintExecuteFocus(task) {
726
- const lines = renderExecuteFocus(getSessionPublisher().snapshot());
727
- if (lines.length > 0) task.output = lines.join("\n");
728
- }
729
- function buildExecutePhaseTask(runner, tasksPath, verbose) {
730
- return async (_ctx, task) => {
731
- let stories;
732
- try {
733
- const plan = await loadTaskPlan(tasksPath);
734
- stories = [...plan.userStories].sort((a, b) => a.priority - b.priority);
735
- } catch {
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);
1166
+ } catch {
736
1167
  setOutputCallback((line) => {
737
1168
  task.output = line;
738
1169
  });
@@ -907,7 +1338,11 @@ async function runPipelineWithRenderer(options) {
907
1338
  };
908
1339
  }
909
1340
 
910
- // 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";
911
1346
  function toIssueNumber(id) {
912
1347
  const parsed = Number.parseInt(id, 10);
913
1348
  return Number.isNaN(parsed) ? null : parsed;
@@ -960,17 +1395,6 @@ function publishIssueDetails(publisher, issue, at) {
960
1395
  state: issue.state
961
1396
  });
962
1397
  }
963
- var RUNNABLE_PHASES = ["prd", "plan", "execute", "review", "pr"];
964
- var RUNNABLE_PHASES_NO_BRANCH = ["prd", "plan", "execute", "review"];
965
- var RUNNABLE_PHASES_WITH_PR_REVIEW = [...RUNNABLE_PHASES, "pr-review"];
966
- var QUEUE_PR_PHASES = ["init", "pr"];
967
- var QUEUE_PR_PHASES_WITH_REVIEW = [
968
- "init",
969
- "pr",
970
- "pr-review"
971
- ];
972
- var RUNNABLE_QUEUE_PR_PHASES = ["pr"];
973
- var RUNNABLE_QUEUE_PR_PHASES_WITH_REVIEW = ["pr", "pr-review"];
974
1398
  function verificationForSummary(value) {
975
1399
  if (value === null || value.verdict === null) return null;
976
1400
  return { verdict: value.verdict, level: value.level };
@@ -997,200 +1421,608 @@ async function readPrReviewOutcome(issue, tasksPath, requestedChanges) {
997
1421
  }
998
1422
  return outcome;
999
1423
  }
1000
- var DEFAULT_MAX_ISSUE_ATTEMPTS = 2;
1001
- async function runPipeline(issue, mode, from, noBranch, prReview, options = {}) {
1002
- let requested;
1003
- try {
1004
- requested = parseIssueArguments(typeof issue === "string" ? [issue] : issue);
1005
- } catch (err) {
1006
- printError(err instanceof Error ? err.message : String(err));
1007
- return 1;
1008
- }
1009
- if (options.background === true && options.detachedChild !== true) {
1010
- const reason = backgroundRejection(mode);
1011
- if (reason !== null) {
1012
- printError(reason);
1013
- return 1;
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}`);
1014
1433
  }
1015
- return detachAfterConfirm(requested, noBranch, prReview, options);
1016
- }
1017
- const ownership = await claimRunOwnership(requested[0], options.detachedChild === true);
1018
- if (!ownership.ok) {
1019
- printError(
1020
- `Another issue-flow run owns this project: ${describeRunLockOwner(ownership.owner)}.`
1021
- );
1022
- printInfo("Wait for it to finish, or stop that process before running again.");
1023
- return 1;
1024
1434
  }
1025
- try {
1026
- const first = await runIssueSession(requested[0], mode, {
1027
- from,
1028
- noBranch,
1029
- prReview,
1030
- requested,
1031
- runOptions: options,
1032
- restartWeb: options.restartWeb,
1033
- ...ownership.interruptedBy === null ? {} : { interruptedBy: ownership.interruptedBy }
1034
- });
1035
- if (first.queue === void 0) {
1036
- return first.code;
1037
- }
1038
- return runQueue(
1039
- first.queue.plan,
1040
- { mode, from, noBranch, prReview, runOptions: options },
1041
- first.queue.resolved
1042
- );
1043
- } finally {
1044
- await ownership.release();
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);
1045
1439
  }
1046
1440
  }
1047
- var WRITING_PHASES = /* @__PURE__ */ new Set(["plan", "execute", "pr"]);
1048
- async function ensureRepositoryWritable(phase) {
1049
- if (!WRITING_PHASES.has(phase)) return;
1050
- const preflight = await preflightRepository({ intent: "resume-same-phase" });
1051
- if (preflight.ok) return;
1052
- for (const line of describePreflight(preflight)) {
1053
- printError(line);
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
+ }
1054
1455
  }
1055
- throw new Error(`The repository is not in a state the ${phase} phase can write to`);
1456
+ return prUrl;
1056
1457
  }
1057
- async function claimRunOwnership(target, detached = false) {
1058
- 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";
1059
1472
  try {
1060
- lockFile = (await resolveProjectPaths()).runLockFile;
1473
+ const proc = await execa("git", ["branch", "--show-current"], { reject: false });
1474
+ branchName = proc.stdout?.toString().trim() ?? "unknown";
1061
1475
  } catch {
1062
- return { ok: true, interruptedBy: null, release: async () => {
1063
- } };
1064
1476
  }
1065
- const result = await acquireRunLock(lockFile, { target, detached });
1066
- if (!result.ok) return result;
1067
- return {
1068
- ok: true,
1069
- interruptedBy: result.handle.reclaimedFrom,
1070
- release: () => result.handle.release()
1071
- };
1072
- }
1073
- async function runIssueSession(issueNumber, mode, input) {
1074
- resetAgentInvocationState();
1075
- const paths = await resolveIssuePaths(issueNumber);
1477
+ let storyCount = 0;
1478
+ let planPrUrl = null;
1076
1479
  try {
1077
- const project = await resolveProjectPaths();
1078
- bindDiagnosticContext({
1079
- project: project.projectId,
1080
- projectRoot: await getProjectRoot(),
1081
- issue: issueNumber,
1082
- sessionId: null,
1083
- executionId: null,
1084
- phase: null,
1085
- story: null,
1086
- harness: null,
1087
- model: null
1088
- });
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);
1089
1489
  } catch {
1090
1490
  }
1091
- const resilience = await initResilienceConfig();
1092
- const configuredInactivity = resilience.watchdog?.inactivityTimeoutMs;
1093
- if (configuredInactivity !== void 0 && getInactivityTimeout() === void 0) {
1094
- setInactivityTimeout(configuredInactivity);
1095
- }
1096
- const webConfig = await loadWebConfig();
1097
- const surfaces = [];
1098
- const journalEnabled = resilience.journal?.enabled === true;
1099
- const persistSnapshot = webConfig.enabled || input.runOptions?.detachedChild === true;
1100
- if (persistSnapshot || journalEnabled) {
1101
- await mkdir2(paths.issueDir, { recursive: true });
1102
- }
1103
- if (persistSnapshot) {
1104
- surfaces.push(
1105
- new FilePublisher(paths.sessionFile, {
1106
- logLimit: webConfig.logLimit,
1107
- includeLogs: webConfig.includeLogs
1108
- })
1109
- );
1110
- }
1111
- if (journalEnabled) {
1112
- surfaces.push(
1113
- new JournalPublisher(paths.eventsFile, paths.rotatedEventsFile, {
1114
- logLimit: webConfig.logLimit,
1115
- includeLogs: webConfig.includeLogs,
1116
- ...resilience.journal?.maxFileBytes === void 0 ? {} : { maxFileBytes: resilience.journal.maxFileBytes }
1117
- })
1118
- );
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
+ };
1119
1503
  }
1120
- const publisher = surfaces.length === 0 ? new MemoryPublisher() : surfaces.length === 1 ? surfaces[0] : new MultiPublisher(surfaces);
1121
- setSessionPublisher(publisher);
1122
- if (input.interruptedBy !== void 0) {
1123
- const previous = input.interruptedBy;
1124
- publisher.publish({
1125
- type: "log",
1126
- at: isoNow(),
1127
- level: "warn",
1128
- message: `Previous run interrupted: ${describeRunLockOwner(previous)}. Its lock was stale and has been taken over.`
1129
- });
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
+ };
1130
1630
  }
1131
- if (webConfig.enabled) {
1132
- await ensureWebMonitor(
1133
- {
1134
- publisher,
1135
- port: webConfig.port,
1136
- host: webConfig.host,
1137
- refreshSeconds: webConfig.refreshSeconds
1138
- },
1139
- { restart: input.restartWeb === true }
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."
1140
1648
  );
1649
+ effectivePrReview = false;
1141
1650
  }
1142
- let result = {
1143
- code: 1,
1144
- failedPhase: null,
1145
- branchName: null,
1146
- storyCount: 0,
1147
- 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]
1148
1658
  };
1149
- const releaseCheckpoint = onShutdown({
1150
- phase: "checkpoint",
1151
- run: async () => {
1152
- 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" });
1153
1679
  publisher.publish({
1154
- type: "log",
1680
+ type: "phase:end",
1155
1681
  at: isoNow(),
1156
- level: "warn",
1157
- 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"
1158
1685
  });
1159
- 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) };
1160
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
1161
1705
  });
1162
- const releaseClose = onShutdown({
1163
- phase: "close",
1164
- run: async () => {
1165
- await publisher.close();
1166
- }
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
1167
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;
1168
1810
  try {
1169
- result = await runPipelinePhases(issueNumber, paths, mode, publisher, input);
1170
- if (result.code !== 0) {
1171
- 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 {
1172
1841
  }
1173
- return result;
1174
- } finally {
1175
- releaseCheckpoint();
1176
- releaseClose();
1177
- if (result.queue === void 0) {
1178
- publisher.publish({
1179
- type: "session:end",
1180
- at: isoNow(),
1181
- status: result.code === 0 ? "completed" : "failed"
1182
- });
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;
1183
1889
  }
1184
- await publisher.close();
1185
- writeDiagnostic({
1186
- level: result.code === 0 ? "info" : "error",
1187
- message: `Issue Flow session ${result.code === 0 ? "completed" : "failed"}`,
1188
- context: { code: result.code, failedPhase: result.failedPhase }
1189
- });
1190
- await flushDiagnostics();
1191
- setSessionPublisher(void 0);
1192
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
+ };
1193
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";
1194
2026
  async function reportIfOversized(issueNumber, paths, result) {
1195
2027
  try {
1196
2028
  const journal = `${await readFileIfPresent(paths.rotatedEventsFile)}${await readFileIfPresent(paths.eventsFile)}`;
@@ -1249,7 +2081,7 @@ async function maybeCreateSubIssues(issueNumber, plan, reportFile) {
1249
2081
  printInfo("Nothing left to split: no pending story.");
1250
2082
  return;
1251
2083
  }
1252
- const { runGenerate } = await import("./generate-PB4L7V5N.js");
2084
+ const { runGenerate } = await import("./generate-CX6KNHZS.js");
1253
2085
  for (const proposal of proposals) {
1254
2086
  const instruction = [
1255
2087
  `Create a sub-issue of #${issueNumber}: ${proposal.title}.`,
@@ -1283,366 +2115,306 @@ async function readFileIfPresent(path) {
1283
2115
  return "";
1284
2116
  }
1285
2117
  }
1286
- 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;
1287
2132
  try {
1288
- const plan = await loadTaskPlan(tasksFile);
1289
- await saveTaskPlan(tasksFile, {
1290
- ...plan,
1291
- runState: {
1292
- ...plan.runState ?? {
1293
- currentPhase: null,
1294
- attempt: 0,
1295
- lastHeartbeatAt: null,
1296
- blockedReason: null,
1297
- owner: null
1298
- },
1299
- status: "paused",
1300
- lastHeartbeatAt: isoNow()
1301
- }
1302
- });
2133
+ lockFile = (await resolveProjectPaths()).runLockFile;
1303
2134
  } catch {
1304
- printWarning(`Could not write a checkpoint for issue #${issueNumber}.`);
2135
+ return { ok: true, interruptedBy: null, release: async () => {
2136
+ } };
1305
2137
  }
1306
- }
1307
- async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
1308
- const { from, noBranch, prReview, queue } = input;
1309
- const continueNumbering = input.runOptions?.continueNumbering;
1310
- const startUs = input.runOptions?.startUs;
1311
- const executeRetry = {
1312
- retryLimit: input.runOptions?.retryLimit,
1313
- retryForever: input.runOptions?.retryForever
1314
- };
1315
- const tasksPath = paths.tasksFile;
1316
- const sessionId = randomUUID();
1317
- bindDiagnosticContext({
1318
- sessionId,
1319
- issue: issueNumber,
1320
- executionId: null,
1321
- phase: null,
1322
- story: null,
1323
- harness: null,
1324
- model: null
1325
- });
1326
- const [initialBranch, initialCommit] = await Promise.all([
1327
- getCurrentBranch().catch(() => ""),
1328
- getHeadCommit()
1329
- ]);
1330
- let publishedIssueNumber = toIssueNumber(issueNumber);
1331
- const publishSessionStart = (phases, at, info) => {
1332
- publisher.publish({
1333
- type: "session:start",
1334
- at,
1335
- sessionId,
1336
- issueNumber: publishedIssueNumber,
1337
- issueUrl: info?.issueUrl,
1338
- branch: info?.branch,
1339
- branchCreated: info?.branchCreated,
1340
- startCommit: info?.startCommit,
1341
- phases: [...phases],
1342
- configuration: configurationSnapshot,
1343
- environment: {
1344
- node: process.version,
1345
- platform: process.platform,
1346
- agent: agentSummary.defaultProvider,
1347
- model: agentSummary.defaultModel,
1348
- cliVersion: getPackageVersion()
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()
1349
2161
  }
1350
2162
  });
1351
- };
1352
- const agentSummary = await describeRunAgents(
1353
- prReview ? ["prd", "plan", "execute", "review", "pr", "pr-review"] : ["prd", "plan", "execute", "review", "pr"]
1354
- );
1355
- const fallbacks = getActiveResilienceConfig().providers?.chain ?? [];
1356
- const configurationSnapshot = {
1357
- precedence: ["default", "global", "project", "env", "cli", "step override"],
1358
- defaultProvider: {
1359
- value: agentSummary.defaultProvider,
1360
- source: agentSummary.defaultOrigin.provider
1361
- },
1362
- defaultModel: {
1363
- value: agentSummary.defaultModel,
1364
- source: agentSummary.defaultOrigin.model
1365
- },
1366
- phases: Object.entries(agentSummary.byPhase).map(([phase, resolved]) => ({
1367
- phase,
1368
- provider: { value: resolved.provider, source: resolved.origin.provider },
1369
- model: { value: resolved.model, source: resolved.origin.model }
1370
- })),
1371
- fallbacks,
1372
- overrides: Object.entries(agentSummary.byPhase).filter(
1373
- ([, resolved]) => resolved.provider !== agentSummary.defaultProvider || resolved.model !== agentSummary.defaultModel
1374
- ).map(
1375
- ([phase, resolved]) => `${phase}: ${resolved.provider}${resolved.model ? ` \xB7 ${resolved.model}` : ""}`
1376
- )
1377
- };
1378
- printInfo(
1379
- `Issue Flow v${getPackageVersion()} \xB7 starting pipeline for issue #${issueNumber} (mode: ${mode}, agent: ${agentSummary.label})`
1380
- );
1381
- const issuesConfig = await loadIssuesConfig();
1382
- const sessionStartedAt = isoNow();
1383
- if (queue?.preChecked !== true) {
1384
- if (isVerbose()) {
1385
- printInfo("Running prerequisite checks...");
1386
- }
1387
- const initCode = await runInit(issuesConfig.preferredProvider, { compact: !isVerbose() });
1388
- if (initCode !== 0) {
1389
- publishSessionStart(PIPELINE_PHASES, sessionStartedAt);
1390
- publisher.publish({ type: "phase:start", at: sessionStartedAt, phase: "init" });
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);
1391
2198
  publisher.publish({
1392
- type: "phase:end",
2199
+ type: "log",
1393
2200
  at: isoNow(),
1394
- phase: "init",
1395
- success: false,
1396
- error: "Prerequisites not met"
2201
+ level: "warn",
2202
+ message: `Interrupted during issue #${issueNumber}. A checkpoint was saved; resume with \`issue-flow resume ${issueNumber}\`.`
1397
2203
  });
1398
- printError("Prerequisites not met. Fix the issues above and try again.");
1399
- return failure(1);
2204
+ publisher.publish({ type: "session:end", at: isoNow(), status: "failed" });
1400
2205
  }
1401
- }
1402
- const resolution = await resolveCommandIssue(issueNumber, queue?.resolved, {
1403
- config: issuesConfig
1404
2206
  });
1405
- if (!resolution.ok) {
1406
- return failure(resolution.code);
1407
- }
1408
- const resolvedIssue = resolution.resolved;
1409
- publishedIssueNumber = resolvedIssue.issue.number;
1410
- if (queue === void 0) {
1411
- const decision = await decideQueue({
1412
- requested: input.requested ?? [issueNumber],
1413
- resolved: resolvedIssue,
1414
- noBranch: noBranch ?? false,
1415
- prReview: prReview ?? false,
1416
- runOptions: input.runOptions ?? {}
1417
- });
1418
- if (decision.kind === "stop") {
1419
- return failure(decision.code);
1420
- }
1421
- if (decision.kind === "queue") {
1422
- return {
1423
- ...failure(0),
1424
- queue: { plan: decision.plan, resumed: decision.resumed, resolved: resolvedIssue }
1425
- };
2207
+ const releaseClose = onShutdown({
2208
+ phase: "close",
2209
+ run: async () => {
2210
+ await publisher.close();
1426
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
+ });
1427
2225
  }
1428
- let effectiveNoBranch = noBranch ?? false;
1429
- let effectivePrReview = prReview ?? false;
1430
- let planIssueUrl;
1431
- let planBranch;
1432
- let planStories = [];
1433
- try {
1434
- const existingPlan = await loadTaskPlan(tasksPath);
1435
- const persistedNoBranch = existingPlan.noBranch ?? false;
1436
- planIssueUrl = existingPlan.issueUrl || void 0;
1437
- planBranch = existingPlan.branchName || void 0;
1438
- planStories = existingPlan.userStories;
1439
- effectivePrReview = prReview ?? existingPlan.prReview?.enabled ?? false;
1440
- if (noBranch !== void 0 && noBranch !== persistedNoBranch) {
1441
- if (persistedNoBranch) {
1442
- printWarning(
1443
- "This pipeline was started with --no-branch. Ignoring current flag; using persisted mode."
1444
- );
1445
- } else {
1446
- printWarning(
1447
- "This pipeline was started without --no-branch. Ignoring current flag; using persisted mode."
1448
- );
1449
- }
1450
- }
1451
- effectiveNoBranch = persistedNoBranch;
1452
- } catch {
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
+ });
1453
2244
  }
1454
- if (effectiveNoBranch && effectivePrReview) {
1455
- printWarning(
1456
- "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 }
1457
2254
  );
1458
- effectivePrReview = false;
1459
2255
  }
1460
- if (effectivePrReview) {
1461
- try {
1462
- const plan = await loadTaskPlan(tasksPath);
1463
- if (plan.prReview?.enabled !== true) {
1464
- plan.prReview = { ...plan.prReview, enabled: true, rounds: plan.prReview?.rounds ?? 0 };
1465
- await saveTaskPlan(tasksPath, plan);
1466
- }
1467
- } catch {
1468
- }
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 {
1469
2274
  }
1470
- const inQueue = queue !== void 0;
1471
- const finalPr = queue?.finalPr;
1472
- const activePhases = finalPr ? effectivePrReview ? QUEUE_PR_PHASES_WITH_REVIEW : QUEUE_PR_PHASES : inQueue ? PIPELINE_PHASES_NO_BRANCH : effectiveNoBranch ? PIPELINE_PHASES_NO_BRANCH : effectivePrReview ? PIPELINE_PHASES_WITH_PR_REVIEW : PIPELINE_PHASES;
1473
- const phaseOrder = finalPr ? effectivePrReview ? RUNNABLE_QUEUE_PR_PHASES_WITH_REVIEW : RUNNABLE_QUEUE_PR_PHASES : inQueue ? RUNNABLE_PHASES_NO_BRANCH : effectiveNoBranch ? RUNNABLE_PHASES_NO_BRANCH : effectivePrReview ? RUNNABLE_PHASES_WITH_PR_REVIEW : RUNNABLE_PHASES;
1474
- const queueCommitScope = queue === void 0 ? void 0 : `issue-${issueNumber}`;
1475
- let producedBranch = null;
1476
- let plannedExecutionBranch = planBranch ?? null;
1477
- let branchExistedBeforeExecution = effectiveNoBranch || planBranch === void 0 || planBranch === null ? null : await localBranchExists(planBranch).catch(() => null);
1478
- if (queue !== void 0 && finalPr === void 0 && !effectiveNoBranch) {
1479
- producedBranch = await adoptQueueBranch(tasksPath, queue.plan.branchName);
1480
- if (producedBranch !== null) {
1481
- planBranch = producedBranch;
1482
- }
2275
+ const resilience = await initResilienceConfig();
2276
+ const configuredInactivity = resilience.watchdog?.inactivityTimeoutMs;
2277
+ if (configuredInactivity !== void 0 && getInactivityTimeout() === void 0) {
2278
+ setInactivityTimeout(configuredInactivity);
1483
2279
  }
1484
- publishSessionStart(activePhases, sessionStartedAt, {
1485
- issueUrl: planIssueUrl ?? resolvedIssue.issue.remoteRef ?? void 0,
1486
- branch: effectiveNoBranch ? initialBranch : planBranch,
1487
- branchCreated: effectiveNoBranch ? false : branchExistedBeforeExecution === true ? false : null,
1488
- startCommit: initialCommit
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
1489
2289
  });
1490
- publishIssueDetails(publisher, resolvedIssue.issue, sessionStartedAt);
1491
- publishStorySeed(publisher, planStories, sessionStartedAt);
1492
- publisher.publish({ type: "phase:start", at: sessionStartedAt, phase: "init" });
1493
- publisher.publish({ type: "phase:end", at: isoNow(), phase: "init", success: true });
1494
- await publishGitState(publisher);
1495
- let startPhase = "prd";
1496
- if (from) {
1497
- if (!activePhases.includes(from)) {
1498
- const validPhases = activePhases.filter((p) => p !== "init").join(", ");
1499
- if (effectiveNoBranch && PIPELINE_PHASES.includes(from)) {
1500
- printError(
1501
- `The '${from}' phase is not available in --no-branch mode. Valid phases: ${validPhases}`
1502
- );
1503
- } else {
1504
- printError(`Invalid phase: ${from}. Valid phases: ${validPhases}`);
1505
- }
1506
- return failure(1);
1507
- }
1508
- startPhase = from;
1509
- } else {
1510
- try {
1511
- const plan = await loadTaskPlan(tasksPath);
1512
- const mgr = new PipelineManager(plan, tasksPath, activePhases);
1513
- const nextPhase = mgr.getNextPhase();
1514
- if (nextPhase && nextPhase !== "init") {
1515
- startPhase = nextPhase;
1516
- printInfo(`Resuming from phase: ${startPhase}`);
1517
- }
1518
- } catch {
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);
1519
2313
  }
2314
+ return result;
2315
+ } finally {
2316
+ await closeIssueSession({ releaseCheckpoint, releaseClose, result, publisher });
1520
2317
  }
1521
- 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
+ )();
1522
2344
  try {
1523
2345
  const plan = await loadTaskPlan(tasksPath);
1524
- const mgr = new PipelineManager(plan, tasksPath, activePhases);
1525
- if (!mgr.canResume(startPhase)) {
1526
- printError(`Cannot resume from ${startPhase}: prerequisite phases not complete`);
1527
- return failure(1);
1528
- }
1529
- } catch {
1530
- if (startPhase !== "prd") {
1531
- printError(`Cannot resume from ${startPhase}: no pipeline state found`);
1532
- return failure(1);
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
+ });
1533
2356
  }
1534
- }
1535
- }
1536
- const startIdx = Math.max(phaseOrder.indexOf(startPhase), 0);
1537
- const makeRunner = (fn, phase) => async () => {
1538
- await ensureRepositoryWritable(phase);
1539
- const code = await fn();
1540
- if (code !== 0) {
1541
- throw new Error(`Phase ${phase} failed with exit code ${code}`);
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);
1542
2369
  }
1543
2370
  };
1544
- const runners = {
1545
- prd: makeRunner(() => runPrd(issueNumber, resolvedIssue), "prd"),
1546
- plan: async () => {
1547
- await makeRunner(
1548
- () => runPlan(issueNumber, resolvedIssue, {
1549
- continueFlag: continueNumbering,
1550
- startUs,
1551
- ...effectiveNoBranch && initialBranch ? { branchName: initialBranch } : {}
1552
- }),
1553
- "plan"
1554
- )();
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 });
1555
2387
  try {
1556
2388
  const plan = await loadTaskPlan(tasksPath);
1557
- if (!effectiveNoBranch) {
1558
- plannedExecutionBranch = plan.branchName;
1559
- branchExistedBeforeExecution = await localBranchExists(plan.branchName).catch(() => null);
1560
- publisher.publish({
1561
- type: "git:update",
1562
- at: isoNow(),
1563
- branchCreated: branchExistedBeforeExecution === true ? false : null
1564
- });
1565
- }
1566
- publishStorySeed(publisher, plan.userStories, isoNow());
1567
- if (effectiveNoBranch || effectivePrReview) {
1568
- if (effectiveNoBranch) plan.noBranch = true;
1569
- if (effectivePrReview) {
1570
- plan.prReview = { ...plan.prReview, enabled: true, rounds: plan.prReview?.rounds ?? 0 };
1571
- }
1572
- await saveTaskPlan(tasksPath, plan);
1573
- }
2389
+ plan.correctionCycle = cycle;
2390
+ await saveTaskPlan(tasksPath, plan);
1574
2391
  } catch {
1575
2392
  }
1576
- if (queue !== void 0 && !effectiveNoBranch) {
1577
- producedBranch = await adoptQueueBranch(tasksPath, queue.plan.branchName);
1578
- }
1579
- },
1580
- execute: makeRunner(
1581
- () => runExecute(void 0, {
2393
+ const execCode = await runExecute(void 0, {
1582
2394
  issue: issueNumber,
1583
2395
  commitScope: queueCommitScope,
1584
2396
  ...executeRetry
1585
- }),
1586
- "execute"
1587
- ),
1588
- review: async () => {
1589
- let maxCycles = 3;
1590
- try {
1591
- const plan = await loadTaskPlan(tasksPath);
1592
- maxCycles = plan.maxCorrectionCycles;
1593
- } catch {
1594
- }
1595
- let code = await runReview(issueNumber, resolvedIssue);
1596
- let cycle = 0;
1597
- while (code !== 0 && cycle < maxCycles) {
1598
- cycle++;
1599
- printWarning(`Review failed. Starting correction cycle ${cycle}/${maxCycles}...`);
1600
- publisher.publish({ type: "correction:cycle", at: isoNow(), cycle, maxCycles });
1601
- try {
1602
- const plan = await loadTaskPlan(tasksPath);
1603
- plan.correctionCycle = cycle;
1604
- await saveTaskPlan(tasksPath, plan);
1605
- } catch {
1606
- }
1607
- const execCode = await runExecute(void 0, {
1608
- issue: issueNumber,
1609
- commitScope: queueCommitScope,
1610
- ...executeRetry
1611
- });
1612
- if (execCode !== 0) {
1613
- throw new Error("Correction execution failed");
1614
- }
1615
- code = await runReview(issueNumber, resolvedIssue);
1616
- }
1617
- if (code !== 0) {
1618
- throw new Error(`Review failed after ${maxCycles} correction cycles`);
2397
+ });
2398
+ if (execCode !== 0) {
2399
+ throw new Error("Correction execution failed");
1619
2400
  }
1620
- },
1621
- pr: makeRunner(
1622
- // The options argument is omitted outside a queue so a standalone run
1623
- // calls `runPr` exactly as it always did.
1624
- () => finalPr === void 0 ? runPr(issueNumber, resolvedIssue) : runPr(issueNumber, resolvedIssue, { queue: finalPr }),
1625
- "pr"
1626
- )
2401
+ code = await runReview(issueNumber, resolvedIssue);
2402
+ }
2403
+ if (code !== 0) {
2404
+ throw new Error(`Review failed after ${maxCycles} correction cycles`);
2405
+ }
1627
2406
  };
1628
- const reviewBox = { outcome: null };
1629
- if (effectivePrReview) {
1630
- runners["pr-review"] = async () => {
1631
- const code = await runPrReview(void 0, { issue: issueNumber, yes: true });
1632
- if (code === 1) {
1633
- throw new Error("Phase pr-review failed with exit code 1");
1634
- }
1635
- reviewBox.outcome = await readPrReviewOutcome(issueNumber, tasksPath, code === 2);
1636
- };
1637
- }
1638
- const instrumentedRunners = Object.fromEntries(
2407
+ }
2408
+ function instrumentPhaseRunners(runners, input) {
2409
+ const { publisher, issueNumber, branchState } = input;
2410
+ return Object.fromEntries(
1639
2411
  Object.entries(runners).map(([phase, fn]) => [
1640
2412
  phase,
1641
2413
  async () => {
1642
2414
  publisher.publish({ type: "phase:start", at: isoNow(), phase });
1643
2415
  try {
1644
2416
  await fn();
1645
- 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) {
1646
2418
  publisher.publish({ type: "git:update", at: isoNow(), branchCreated: true });
1647
2419
  }
1648
2420
  await publishGitState(publisher);
@@ -1661,447 +2433,257 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
1661
2433
  }
1662
2434
  ])
1663
2435
  );
1664
- const phaseSuffixes = {};
1665
- for (const [phase, resolved] of Object.entries(agentSummary.byPhase)) {
1666
- if (resolved.provider !== agentSummary.defaultProvider) {
1667
- phaseSuffixes[phase] = resolved.model ? `${resolved.provider} \xB7 ${resolved.model}` : resolved.provider;
1668
- }
1669
- }
1670
- const result = await runPipelineWithRenderer({
1671
- phases: phaseOrder,
1672
- startIndex: startIdx,
1673
- verbose: isVerbose(),
1674
- runners: instrumentedRunners,
1675
- tasksPath,
1676
- phaseSuffixes
1677
- });
1678
- if (!result.success) {
1679
- printError(`Phase ${result.failedPhase} failed`);
1680
- return {
1681
- ...failure(1),
1682
- failedPhase: result.failedPhase ?? null,
1683
- branchName: producedBranch,
1684
- elapsedSeconds: result.overallElapsedSeconds
1685
- };
1686
- }
1687
- const review = reviewBox.outcome;
1688
- if (review?.requestedChanges) {
1689
- console.log("");
1690
- printWarning("PR review requested changes \u2014 the Pull Request is not ready to merge.");
1691
- if (review.reportPath !== null) {
1692
- console.log(` Report: ${review.reportPath}`);
1693
- }
1694
- }
1695
- if (review?.requestedChanges) {
1696
- printInfo("Issue left open until the review blockers are addressed.");
1697
- } else if (!inQueue) {
1698
- await closeIssue(issueNumber, resolvedIssue.source);
1699
- }
1700
- let branchName = "unknown";
1701
- try {
1702
- const proc = await execa("git", ["branch", "--show-current"], { reject: false });
1703
- branchName = proc.stdout?.toString().trim() ?? "unknown";
1704
- } catch {
1705
- }
1706
- let storyCount = 0;
1707
- let planPrUrl = null;
1708
- try {
1709
- const plan = await loadTaskPlan(tasksPath);
1710
- storyCount = plan.userStories.length;
1711
- planPrUrl = plan.pullRequest?.url ?? null;
1712
- plan.lastAttemptAt = isoNow();
1713
- if (!review?.requestedChanges) {
1714
- plan.issueStatus = "completed";
1715
- plan.completedAt = isoNow();
1716
- }
1717
- await saveTaskPlan(tasksPath, plan);
1718
- } catch {
1719
- }
1720
- if (inQueue) {
1721
- if (finalPr === void 0) {
1722
- printSuccess(`Issue #${issueNumber} completed (${storyCount} stories).`);
1723
- }
1724
- return {
1725
- code: 0,
1726
- failedPhase: null,
1727
- branchName: producedBranch ?? (branchName === "unknown" ? null : branchName),
1728
- storyCount,
1729
- elapsedSeconds: result.overallElapsedSeconds,
1730
- review
1731
- };
1732
- }
1733
- let prUrl = "unknown";
1734
- if (!effectiveNoBranch && resolvedIssue.source === "github") {
1735
- if (planPrUrl !== null) {
1736
- prUrl = planPrUrl;
1737
- } else if (branchName !== "unknown" && branchName !== "") {
1738
- try {
1739
- const latest = mostRecent(await listPullRequests(branchName));
1740
- if (latest !== null) {
1741
- prUrl = latest.url;
1742
- }
1743
- } catch {
1744
- }
1745
- }
1746
- }
1747
- printRunSummary({
1748
- issueNumber,
1749
- branchName,
1750
- noBranch: effectiveNoBranch,
1751
- storyCount,
1752
- elapsedSeconds: result.overallElapsedSeconds,
1753
- prUrl,
1754
- prReview: review,
1755
- // Process-owned counters: the session snapshot is empty whenever web
1756
- // monitoring is off, so it cannot be the source of these totals.
1757
- usage: getRunUsageTotals(),
1758
- verification: verificationForSummary(getSessionPublisher().snapshot().verification)
1759
- });
1760
- return {
1761
- code: 0,
1762
- failedPhase: null,
1763
- branchName: branchName === "unknown" ? null : branchName,
1764
- storyCount,
1765
- elapsedSeconds: result.overallElapsedSeconds
1766
- };
1767
- }
1768
- async function detachAfterConfirm(requested, noBranch, prReview, options) {
1769
- const childFlags = [...process.argv.slice(2)];
1770
- if (options.yes !== true && options.only !== true) {
1771
- try {
1772
- const issuesConfig = await loadIssuesConfig();
1773
- const resolution = await resolveCommandIssue(requested[0], void 0, {
1774
- config: issuesConfig
1775
- });
1776
- if (resolution.ok) {
1777
- const decision = await decideQueue({
1778
- requested,
1779
- resolved: resolution.resolved,
1780
- noBranch: noBranch ?? false,
1781
- prReview: prReview ?? false,
1782
- runOptions: options
1783
- });
1784
- if (decision.kind === "stop") return decision.code;
1785
- if (decision.kind === "queue" && !childFlags.includes("--yes")) childFlags.push("--yes");
1786
- if (decision.kind === "single" && !childFlags.includes("--only")) childFlags.push("--only");
1787
- }
1788
- } catch {
1789
- }
1790
- }
1791
- const paths = await resolveIssuePaths(requested[0]);
1792
- const { pid, logFile } = await spawnDetachedRun({ paths, argv: childFlags });
1793
- printSuccess(`Detached run started (pid ${pid}).`);
1794
- printInfo(`Log: ${logFile}`);
1795
- printInfo("Follow with: issue-flow ps");
1796
- printInfo(`Stop with: kill ${pid}`);
1797
- return 0;
1798
- }
1799
- async function decideQueue(input) {
1800
- const primary = input.requested[0];
1801
- const single = input.requested.length === 1;
1802
- if (single && input.runOptions.only === true) {
1803
- return { kind: "single" };
1804
- }
1805
- try {
1806
- const [project, queuePaths] = await Promise.all([
1807
- resolveProjectPaths(),
1808
- resolveQueuePaths(primary)
1809
- ]);
1810
- return await planQueue({
1811
- requested: input.requested,
1812
- source: input.resolved.source,
1813
- known: [input.resolved.issue],
1814
- projectId: project.projectId,
1815
- planFile: queuePaths.planFile,
1816
- noBranch: input.noBranch,
1817
- prReview: input.prReview,
1818
- confirm: {
1819
- yes: input.runOptions.yes,
1820
- only: input.runOptions.only,
1821
- cascade: input.runOptions.cascade
1822
- }
1823
- });
1824
- } catch (err) {
1825
- const message = err instanceof Error ? err.message : String(err);
1826
- if (single) {
1827
- printWarning(
1828
- `Could not inspect related issues (${message}). Running issue #${primary} alone.`
1829
- );
1830
- return { kind: "single" };
1831
- }
1832
- printError(`Could not plan the execution of ${input.requested.length} issues: ${message}`);
1833
- return { kind: "stop", code: 1 };
1834
- }
1835
2436
  }
1836
- async function runQueue(initialPlan, options, resolvedPrimary) {
1837
- const planFile = (await resolveQueuePaths(initialPlan.id)).planFile;
1838
- let plan = initialPlan;
1839
- const queueUsage = beginUsageScope();
1840
- const startedAtMs = Date.now();
1841
- const summaries = [];
1842
- let firstOfThisRun = true;
1843
- const resilience = getActiveResilienceConfig();
1844
- const failureMode = options.runOptions?.onIssueFailure ?? resilience.queue?.onIssueFailure ?? "stop";
1845
- const maxIssueAttempts = resilience.queue?.maxIssueAttempts ?? DEFAULT_MAX_ISSUE_ATTEMPTS;
1846
- const exhausted = /* @__PURE__ */ new Set();
1847
- const failedIds = /* @__PURE__ */ new Set();
1848
- try {
1849
- while (true) {
1850
- const entry = nextQueueIssue(plan, { exclude: exhausted });
1851
- if (entry === null) break;
1852
- plan = markQueueIssueInProgress(plan, entry.id);
1853
- await saveExecutionPlan(planFile, plan);
1854
- printInfo(
1855
- `Queue ${entry.position}/${plan.issues.length}: issue #${entry.id}${entry.title === "" ? "" : ` \u2014 ${entry.title}`}`
1856
- );
1857
- const issueUsage = beginUsageScope();
1858
- let result;
1859
- try {
1860
- result = await runIssueSession(entry.id, options.mode, {
1861
- // `--from` addresses the issue the queue is resuming, never the ones
1862
- // that come after it — those start from their own beginning.
1863
- from: firstOfThisRun ? options.from : void 0,
1864
- noBranch: options.noBranch,
1865
- // `--start-us` belongs to the first issue this invocation runs; the
1866
- // ones after it continue from the history those plans just wrote.
1867
- runOptions: {
1868
- ...options.runOptions,
1869
- startUs: firstOfThisRun ? options.runOptions?.startUs : void 0
1870
- },
1871
- queue: {
1872
- plan,
1873
- preChecked: true,
1874
- resolved: entry.id === initialPlan.id ? resolvedPrimary : void 0
1875
- }
1876
- });
1877
- } finally {
1878
- firstOfThisRun = false;
1879
- issueUsage.end();
1880
- }
1881
- const usage = issueUsage.totals();
1882
- if (result.code !== 0) {
1883
- const failure2 = {
1884
- phase: result.failedPhase,
1885
- error: {
1886
- category: "queue_issue_failed",
1887
- message: `Issue #${entry.id} failed${result.failedPhase === null ? "" : ` in phase ${result.failedPhase}`}`,
1888
- at: isoNow()
1889
- }
1890
- };
1891
- const where = result.failedPhase === null ? "" : ` (phase ${result.failedPhase})`;
1892
- if (failureMode === "stop") {
1893
- plan = markQueueIssueFailed(plan, entry.id, failure2);
1894
- await saveExecutionPlan(planFile, plan);
1895
- printError(
1896
- `Queue stopped at issue #${entry.id}${where}. The branch and every commit made so far were kept.`
1897
- );
1898
- printInfo(`Resume with: issue-flow run ${plan.requested.join(",")}`);
1899
- return result.code;
1900
- }
1901
- if (failureMode === "block") {
1902
- plan = markQueueIssueBlocked(
1903
- plan,
1904
- entry.id,
1905
- `Failed${where} and --on-issue-failure block was in force`,
1906
- failure2
1907
- );
1908
- await saveExecutionPlan(planFile, plan);
1909
- exhausted.add(entry.id);
1910
- failedIds.add(entry.id);
1911
- printWarning(
1912
- `Issue #${entry.id} failed${where} and is blocked for review. Continuing with the rest of the queue.`
1913
- );
1914
- continue;
1915
- }
1916
- const attempts = entry.attempts + 1;
1917
- if (attempts >= maxIssueAttempts) {
1918
- plan = markQueueIssueFailed(plan, entry.id, failure2);
1919
- exhausted.add(entry.id);
1920
- failedIds.add(entry.id);
1921
- printWarning(
1922
- `Issue #${entry.id} failed${where} after ${attempts} attempt(s). Continuing with the rest of the queue.`
1923
- );
1924
- } else {
1925
- plan = markQueueIssueSkipped(plan, entry.id, failure2);
1926
- printWarning(
1927
- `Issue #${entry.id} failed${where}. Skipping it for now and coming back at the end of the queue.`
1928
- );
1929
- }
1930
- await saveExecutionPlan(planFile, plan);
1931
- continue;
1932
- }
1933
- if (plan.branchName === null && result.branchName !== null) {
1934
- plan = setQueueBranch(plan, result.branchName);
1935
- }
1936
- plan = markQueueIssueCompleted(plan, entry.id);
1937
- await saveExecutionPlan(planFile, plan);
1938
- summaries.push({
1939
- id: entry.id,
1940
- title: entry.title,
1941
- storyCount: result.storyCount,
1942
- elapsedSeconds: result.elapsedSeconds,
1943
- usage
1944
- });
1945
- }
1946
- const code = await finishQueue(
1947
- plan,
1948
- planFile,
1949
- summaries,
1950
- startedAtMs,
1951
- queueUsage,
1952
- options,
1953
- resolvedPrimary
1954
- );
1955
- if (failedIds.size > 0) {
1956
- printError(
1957
- `${failedIds.size} issue(s) did not finish: ${[...failedIds].map((id) => `#${id}`).join(", ")}.`
1958
- );
1959
- printInfo(`Resume with: issue-flow run ${plan.requested.join(",")}`);
1960
- return code === 0 ? 1 : code;
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}`);
1961
2454
  }
1962
- return code;
1963
- } finally {
1964
- 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
+ };
1965
2484
  }
2485
+ const instrumentedRunners = instrumentPhaseRunners(runners, {
2486
+ publisher,
2487
+ issueNumber,
2488
+ branchState
2489
+ });
2490
+ return { instrumentedRunners, reviewBox };
1966
2491
  }
1967
- async function finishQueue(plan, planFile, summaries, startedAtMs, queueUsage, options, resolvedPrimary) {
1968
- let current = plan;
1969
- let review = null;
1970
- const alreadyOpened = current.pullRequest !== void 0 || await primaryPrCreated(current);
1971
- if (!current.noBranch && options.noBranch !== true && !alreadyOpened) {
1972
- const outcome = await runIssueSession(current.id, options.mode, {
1973
- prReview: options.prReview ?? current.prReview,
1974
- queue: {
1975
- plan: current,
1976
- preChecked: true,
1977
- resolved: resolvedPrimary,
1978
- finalPr: await buildPrQueueContext(current)
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}`);
1979
2506
  }
1980
- });
1981
- review = outcome.review ?? null;
1982
- if (outcome.code !== 0) {
1983
- printError("The consolidated Pull Request could not be created.");
1984
- await saveExecutionPlan(planFile, current);
1985
- return outcome.code;
1986
- }
1987
- const pullRequest = await propagatePullRequest(current);
1988
- if (pullRequest !== null) {
1989
- current = setQueuePullRequest(current, pullRequest);
2507
+ return { ok: false };
1990
2508
  }
1991
- }
1992
- await saveExecutionPlan(planFile, current);
1993
- if (review?.requestedChanges === true) {
1994
- printInfo("Issues left open until the review blockers are addressed.");
2509
+ startPhase = from;
1995
2510
  } else {
1996
- for (const entry of current.issues) {
1997
- await closeIssue(entry.id, entry.source);
1998
- }
1999
- }
2000
- printQueueSummary({
2001
- queueId: current.id,
2002
- branchName: current.branchName,
2003
- issues: summaries,
2004
- excluded: current.excluded.map((entry) => ({ id: entry.id, title: entry.title })),
2005
- elapsedSeconds: Math.round((Date.now() - startedAtMs) / 1e3),
2006
- prUrl: current.pullRequest?.url ?? null,
2007
- // Read here, after the consolidated Pull Request (and the optional
2008
- // pr-review) already ran: reading it before would report a queue total
2009
- // that leaves out the phase that just spent tokens.
2010
- usage: queueUsage.totals(),
2011
- prReview: review
2012
- });
2013
- return 0;
2014
- }
2015
- async function primaryPrCreated(plan) {
2016
- try {
2017
- const paths = await resolveIssuePaths(plan.id);
2018
- return (await loadTaskPlan(paths.tasksFile)).pipeline.prCreated;
2019
- } catch {
2020
- return false;
2021
- }
2022
- }
2023
- async function buildPrQueueContext(plan) {
2024
- const pending = [];
2025
- for (const entry of plan.issues) {
2026
2511
  try {
2027
- const paths = await resolveIssuePaths(entry.id);
2028
- const taskPlan = await loadTaskPlan(paths.tasksFile);
2029
- if (taskPlan.lastReviewFindings !== null && taskPlan.lastReviewFindings !== "") {
2030
- pending.push(`Issue #${entry.id} has unresolved review findings`);
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}`);
2031
2518
  }
2032
2519
  } catch {
2033
2520
  }
2034
2521
  }
2035
- return {
2036
- issues: plan.issues.map((entry) => ({
2037
- id: entry.id,
2038
- number: entry.number,
2039
- title: entry.title,
2040
- url: entry.url,
2041
- source: entry.source,
2042
- parent: entry.parent,
2043
- role: entry.role,
2044
- complete: entry.status === "completed"
2045
- })),
2046
- excluded: plan.excluded.map((entry) => ({
2047
- id: entry.id,
2048
- number: entry.number,
2049
- title: entry.title,
2050
- reason: entry.reason
2051
- })),
2052
- pending
2053
- };
2054
- }
2055
- async function propagatePullRequest(plan) {
2056
- let pullRequest = null;
2057
- try {
2058
- const primaryPaths = await resolveIssuePaths(plan.id);
2059
- pullRequest = (await loadTaskPlan(primaryPaths.tasksFile)).pullRequest ?? null;
2060
- } catch {
2061
- return null;
2062
- }
2063
- if (pullRequest === null) return null;
2064
- for (const entry of plan.issues) {
2065
- if (entry.id === plan.id) continue;
2522
+ if (from) {
2066
2523
  try {
2067
- const paths = await resolveIssuePaths(entry.id);
2068
- const taskPlan = await loadTaskPlan(paths.tasksFile);
2069
- taskPlan.pullRequest = pullRequest;
2070
- taskPlan.pipeline.prCreated = true;
2071
- await saveTaskPlan(paths.tasksFile, taskPlan);
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
+ }
2072
2530
  } catch {
2073
- 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
+ }
2074
2535
  }
2075
2536
  }
2076
- return pullRequest;
2537
+ return { ok: true, startPhase };
2077
2538
  }
2078
- function failure(code) {
2079
- 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
+ });
2080
2629
  }
2081
- async function closeIssue(issueNumber, source) {
2082
- try {
2083
- const provider = getProvider(source);
2084
- if (provider.close !== void 0) {
2085
- printInfo("Closing issue...");
2086
- await provider.close(issueNumber);
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;
2087
2635
  }
2088
- } catch {
2089
- printWarning("Failed to close issue automatically");
2090
2636
  }
2637
+ return phaseSuffixes;
2091
2638
  }
2092
- 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;
2093
2644
  try {
2094
- const plan = await loadTaskPlan(tasksPath);
2095
- if (queueBranch === null || queueBranch === "") {
2096
- 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;
2097
2655
  }
2098
- if (plan.branchName !== queueBranch) {
2099
- plan.branchName = queueBranch;
2100
- 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;
2101
2678
  }
2102
- return queueBranch;
2103
- } catch {
2104
- 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();
2105
2687
  }
2106
2688
  }
2107
2689
 
@@ -2110,4 +2692,4 @@ export {
2110
2692
  publishIssueDetails,
2111
2693
  runPipeline
2112
2694
  };
2113
- //# sourceMappingURL=chunk-OLUOZLU5.js.map
2695
+ //# sourceMappingURL=chunk-HJBF4WD2.js.map