issue-flow 0.11.1 → 0.13.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 (180) hide show
  1. package/README.md +79 -274
  2. package/dist/agent-BXRIEKVM.js +238 -0
  3. package/dist/agent-BXRIEKVM.js.map +1 -0
  4. package/dist/{analyze-XV3K25K3.js → analyze-WS47UYGN.js} +32 -15
  5. package/dist/analyze-WS47UYGN.js.map +1 -0
  6. package/dist/{apply-WUJHDD7A.js → apply-OS62UWRN.js} +15 -5
  7. package/dist/{apply-WUJHDD7A.js.map → apply-OS62UWRN.js.map} +1 -1
  8. package/dist/bench-SA3BOME5.js +906 -0
  9. package/dist/bench-SA3BOME5.js.map +1 -0
  10. package/dist/chunk-37N2RJW2.js +120 -0
  11. package/dist/chunk-37N2RJW2.js.map +1 -0
  12. package/dist/chunk-3XLCNVMD.js +180 -0
  13. package/dist/chunk-3XLCNVMD.js.map +1 -0
  14. package/dist/chunk-5TB6KLC2.js +72 -0
  15. package/dist/chunk-5TB6KLC2.js.map +1 -0
  16. package/dist/{chunk-OBLVLIUS.js → chunk-664Y3NDX.js} +25 -13
  17. package/dist/chunk-664Y3NDX.js.map +1 -0
  18. package/dist/chunk-6FDFRNXZ.js +357 -0
  19. package/dist/chunk-6FDFRNXZ.js.map +1 -0
  20. package/dist/chunk-7KD66LIM.js +658 -0
  21. package/dist/chunk-7KD66LIM.js.map +1 -0
  22. package/dist/chunk-7Z4Y3VKM.js +26 -0
  23. package/dist/chunk-7Z4Y3VKM.js.map +1 -0
  24. package/dist/chunk-ARFVHC2F.js +1111 -0
  25. package/dist/chunk-ARFVHC2F.js.map +1 -0
  26. package/dist/{chunk-RKTEV6EG.js → chunk-BHJCGWQX.js} +141 -12
  27. package/dist/chunk-BHJCGWQX.js.map +1 -0
  28. package/dist/chunk-BQDQTIM7.js +74 -0
  29. package/dist/chunk-BQDQTIM7.js.map +1 -0
  30. package/dist/chunk-CNBYCMSU.js +2709 -0
  31. package/dist/chunk-CNBYCMSU.js.map +1 -0
  32. package/dist/chunk-CRGIQFUQ.js +83 -0
  33. package/dist/chunk-CRGIQFUQ.js.map +1 -0
  34. package/dist/chunk-DIG5XKQQ.js +298 -0
  35. package/dist/chunk-DIG5XKQQ.js.map +1 -0
  36. package/dist/chunk-FZDR7CGH.js +71 -0
  37. package/dist/chunk-FZDR7CGH.js.map +1 -0
  38. package/dist/chunk-HEF2RMKN.js +413 -0
  39. package/dist/chunk-HEF2RMKN.js.map +1 -0
  40. package/dist/{chunk-IVHZEYAX.js → chunk-HJGGNLWI.js} +25 -19
  41. package/dist/chunk-HJGGNLWI.js.map +1 -0
  42. package/dist/chunk-HQAHNIAR.js +283 -0
  43. package/dist/chunk-HQAHNIAR.js.map +1 -0
  44. package/dist/{chunk-GANSDJV7.js → chunk-IHIEXUTC.js} +46 -24
  45. package/dist/chunk-IHIEXUTC.js.map +1 -0
  46. package/dist/chunk-IV5SS4JN.js +84 -0
  47. package/dist/chunk-IV5SS4JN.js.map +1 -0
  48. package/dist/{chunk-HC2MT7BX.js → chunk-IYV6KNVF.js} +158 -592
  49. package/dist/chunk-IYV6KNVF.js.map +1 -0
  50. package/dist/{chunk-LVKQ2AKT.js → chunk-J62QSVP6.js} +10 -8
  51. package/dist/chunk-J62QSVP6.js.map +1 -0
  52. package/dist/chunk-KMMASOA6.js +134 -0
  53. package/dist/chunk-KMMASOA6.js.map +1 -0
  54. package/dist/chunk-KQHB7ZT3.js +559 -0
  55. package/dist/chunk-KQHB7ZT3.js.map +1 -0
  56. package/dist/chunk-L6HSKTQE.js +77 -0
  57. package/dist/chunk-L6HSKTQE.js.map +1 -0
  58. package/dist/{chunk-6N7LZZ5U.js → chunk-LZ6GFQAG.js} +474 -160
  59. package/dist/chunk-LZ6GFQAG.js.map +1 -0
  60. package/dist/{chunk-HQYXVIH6.js → chunk-MIVHK5YI.js} +5 -5
  61. package/dist/chunk-NEZZ6KDR.js +1217 -0
  62. package/dist/chunk-NEZZ6KDR.js.map +1 -0
  63. package/dist/{run-DBF2VWUN.js → chunk-OPNGPUKJ.js} +747 -594
  64. package/dist/chunk-OPNGPUKJ.js.map +1 -0
  65. package/dist/chunk-P3LRUNC6.js +16 -0
  66. package/dist/chunk-P3LRUNC6.js.map +1 -0
  67. package/dist/{chunk-BXCPXRBR.js → chunk-PIVGJM6K.js} +129 -32
  68. package/dist/chunk-PIVGJM6K.js.map +1 -0
  69. package/dist/{chunk-AAY6KM7N.js → chunk-PV4OX5TC.js} +167 -196
  70. package/dist/chunk-PV4OX5TC.js.map +1 -0
  71. package/dist/chunk-QCXPNSEL.js +141 -0
  72. package/dist/chunk-QCXPNSEL.js.map +1 -0
  73. package/dist/{chunk-34WNTK3K.js → chunk-QXDNBYQF.js} +53 -13
  74. package/dist/chunk-QXDNBYQF.js.map +1 -0
  75. package/dist/chunk-SNTTOQAD.js +30 -0
  76. package/dist/chunk-SNTTOQAD.js.map +1 -0
  77. package/dist/{chunk-EY6WMHV2.js → chunk-TM67EHSV.js} +67 -23
  78. package/dist/chunk-TM67EHSV.js.map +1 -0
  79. package/dist/chunk-TVN2JVOF.js +77 -0
  80. package/dist/chunk-TVN2JVOF.js.map +1 -0
  81. package/dist/chunk-TX5WIVYP.js +129 -0
  82. package/dist/chunk-TX5WIVYP.js.map +1 -0
  83. package/dist/{chunk-KQ4JQN7T.js → chunk-UJ7XWK7J.js} +83 -21
  84. package/dist/chunk-UJ7XWK7J.js.map +1 -0
  85. package/dist/{chunk-47VIN33V.js → chunk-UOCPC2Y2.js} +596 -17
  86. package/dist/chunk-UOCPC2Y2.js.map +1 -0
  87. package/dist/chunk-XEYH6ZZI.js +289 -0
  88. package/dist/chunk-XEYH6ZZI.js.map +1 -0
  89. package/dist/cli.js +390 -40
  90. package/dist/cli.js.map +1 -1
  91. package/dist/config-MEM2IWWN.js +76 -0
  92. package/dist/contract-JH2WBIO6.js +8 -0
  93. package/dist/conventions-WV4NDXGH.js +143 -0
  94. package/dist/conventions-WV4NDXGH.js.map +1 -0
  95. package/dist/execute-NITIHHCK.js +31 -0
  96. package/dist/{generate-TBVTLMOP.js → generate-MVQI7A45.js} +39 -19
  97. package/dist/{generate-TBVTLMOP.js.map → generate-MVQI7A45.js.map} +1 -1
  98. package/dist/git-YRVRUN3C.js +31 -0
  99. package/dist/init-OKWEC3EV.js +21 -0
  100. package/dist/operations-LU6JA3BX.js +374 -0
  101. package/dist/operations-LU6JA3BX.js.map +1 -0
  102. package/dist/permissions-7NMDFU27.js +18 -0
  103. package/dist/plan-LEJJF3DE.js +31 -0
  104. package/dist/{policy-XQW5NUIE.js → policy-JSYEK5B2.js} +17 -4
  105. package/dist/policy-JSYEK5B2.js.map +1 -0
  106. package/dist/pr-HPVUZ7S3.js +36 -0
  107. package/dist/pr-review-R2QZBT3F.js +29 -0
  108. package/dist/pr-review-R2QZBT3F.js.map +1 -0
  109. package/dist/prd-5O6GYCC6.js +30 -0
  110. package/dist/prd-5O6GYCC6.js.map +1 -0
  111. package/dist/ps-TNE44R5V.js +90 -0
  112. package/dist/ps-TNE44R5V.js.map +1 -0
  113. package/dist/recorder-XGF55WXI.js +31 -0
  114. package/dist/recorder-XGF55WXI.js.map +1 -0
  115. package/dist/registry-LGPE53QD.js +15 -0
  116. package/dist/registry-LGPE53QD.js.map +1 -0
  117. package/dist/resume-QMEPMSZW.js +238 -0
  118. package/dist/resume-QMEPMSZW.js.map +1 -0
  119. package/dist/review-YKWD6SNN.js +32 -0
  120. package/dist/review-YKWD6SNN.js.map +1 -0
  121. package/dist/routing-2ZUMZPJB.js +91 -0
  122. package/dist/routing-2ZUMZPJB.js.map +1 -0
  123. package/dist/run-YU4TR62R.js +50 -0
  124. package/dist/run-YU4TR62R.js.map +1 -0
  125. package/dist/state-manager-X3PT3FGT.js +36 -0
  126. package/dist/state-manager-X3PT3FGT.js.map +1 -0
  127. package/dist/usage-VCFLDA5Y.js +174 -0
  128. package/dist/usage-VCFLDA5Y.js.map +1 -0
  129. package/dist/{web-ANQQTTRD.js → web-XVHLX62B.js} +43 -27
  130. package/dist/web-XVHLX62B.js.map +1 -0
  131. package/package.json +6 -2
  132. package/prompts/execute.md +7 -8
  133. package/prompts/plan.md +4 -5
  134. package/prompts/pr.md +13 -6
  135. package/web/AGENTS.md +3 -3
  136. package/web/public/app.css +38 -0
  137. package/web/public/app.js +132 -10
  138. package/web/public/index.html +24 -0
  139. package/dist/analyze-XV3K25K3.js.map +0 -1
  140. package/dist/chunk-34WNTK3K.js.map +0 -1
  141. package/dist/chunk-3BOHTH4V.js +0 -58
  142. package/dist/chunk-3BOHTH4V.js.map +0 -1
  143. package/dist/chunk-47VIN33V.js.map +0 -1
  144. package/dist/chunk-6N7LZZ5U.js.map +0 -1
  145. package/dist/chunk-AAY6KM7N.js.map +0 -1
  146. package/dist/chunk-BXCPXRBR.js.map +0 -1
  147. package/dist/chunk-EWDJENFW.js +0 -303
  148. package/dist/chunk-EWDJENFW.js.map +0 -1
  149. package/dist/chunk-EY6WMHV2.js.map +0 -1
  150. package/dist/chunk-GANSDJV7.js.map +0 -1
  151. package/dist/chunk-HC2MT7BX.js.map +0 -1
  152. package/dist/chunk-HDV3YDTG.js +0 -52
  153. package/dist/chunk-HDV3YDTG.js.map +0 -1
  154. package/dist/chunk-IVHZEYAX.js.map +0 -1
  155. package/dist/chunk-KQ4JQN7T.js.map +0 -1
  156. package/dist/chunk-LVKQ2AKT.js.map +0 -1
  157. package/dist/chunk-OBLVLIUS.js.map +0 -1
  158. package/dist/chunk-RKTEV6EG.js.map +0 -1
  159. package/dist/chunk-VQ5VN7DF.js +0 -1018
  160. package/dist/chunk-VQ5VN7DF.js.map +0 -1
  161. package/dist/config-4THHOXNN.js +0 -41
  162. package/dist/execute-5OEKLM5X.js +0 -15
  163. package/dist/init-PRMOTQ7N.js +0 -9
  164. package/dist/plan-Y2BYZD24.js +0 -17
  165. package/dist/policy-XQW5NUIE.js.map +0 -1
  166. package/dist/pr-H6GLOSXF.js +0 -21
  167. package/dist/pr-review-CK5FR6WX.js +0 -17
  168. package/dist/prd-4PWLCGH5.js +0 -17
  169. package/dist/review-QGVAHEZF.js +0 -15
  170. package/dist/run-DBF2VWUN.js.map +0 -1
  171. package/dist/web-ANQQTTRD.js.map +0 -1
  172. /package/dist/{chunk-HQYXVIH6.js.map → chunk-MIVHK5YI.js.map} +0 -0
  173. /package/dist/{config-4THHOXNN.js.map → config-MEM2IWWN.js.map} +0 -0
  174. /package/dist/{execute-5OEKLM5X.js.map → contract-JH2WBIO6.js.map} +0 -0
  175. /package/dist/{init-PRMOTQ7N.js.map → execute-NITIHHCK.js.map} +0 -0
  176. /package/dist/{plan-Y2BYZD24.js.map → git-YRVRUN3C.js.map} +0 -0
  177. /package/dist/{pr-H6GLOSXF.js.map → init-OKWEC3EV.js.map} +0 -0
  178. /package/dist/{pr-review-CK5FR6WX.js.map → permissions-7NMDFU27.js.map} +0 -0
  179. /package/dist/{prd-4PWLCGH5.js.map → plan-LEJJF3DE.js.map} +0 -0
  180. /package/dist/{review-QGVAHEZF.js.map → pr-HPVUZ7S3.js.map} +0 -0
@@ -1,580 +1,209 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- mostRecent,
4
- prReviewDir,
5
- readPrReviewIndex,
6
- runPrReview
7
- } from "./chunk-KQ4JQN7T.js";
8
- import {
9
- runPrd
10
- } from "./chunk-IVHZEYAX.js";
3
+ formatPhaseLine,
4
+ loadPhaseTiming,
5
+ snapshotTimingFields
6
+ } from "./chunk-37N2RJW2.js";
11
7
  import {
12
8
  runReview
13
- } from "./chunk-34WNTK3K.js";
9
+ } from "./chunk-QXDNBYQF.js";
14
10
  import {
15
- ensureWebMonitor
16
- } from "./chunk-GANSDJV7.js";
11
+ PIPELINE_PHASES,
12
+ PIPELINE_PHASES_NO_BRANCH,
13
+ PIPELINE_PHASES_WITH_PR_REVIEW,
14
+ PipelineManager,
15
+ buildDependencyGraph,
16
+ buildExecutionPlan,
17
+ collectCascadeIds,
18
+ computeExecutionOrder,
19
+ describeCycles,
20
+ isQueueComplete,
21
+ loadExecutionPlan,
22
+ markQueueIssueBlocked,
23
+ markQueueIssueCompleted,
24
+ markQueueIssueFailed,
25
+ markQueueIssueInProgress,
26
+ markQueueIssueSkipped,
27
+ nextQueueIssue,
28
+ saveExecutionPlan,
29
+ setQueueBranch,
30
+ setQueuePullRequest
31
+ } from "./chunk-7KD66LIM.js";
17
32
  import {
18
33
  runInit
19
- } from "./chunk-RKTEV6EG.js";
34
+ } from "./chunk-BHJCGWQX.js";
20
35
  import {
21
36
  printQueueSummary,
22
37
  printRunSummary,
23
- runExecute
24
- } from "./chunk-6N7LZZ5U.js";
38
+ runExecute,
39
+ stripMarkdown
40
+ } from "./chunk-LZ6GFQAG.js";
25
41
  import {
26
- listPullRequests,
27
- publishGitState
28
- } from "./chunk-LVKQ2AKT.js";
42
+ ensureWebMonitor
43
+ } from "./chunk-IHIEXUTC.js";
29
44
  import {
30
45
  runPlan
31
- } from "./chunk-EY6WMHV2.js";
46
+ } from "./chunk-TM67EHSV.js";
32
47
  import {
33
48
  runPr
34
- } from "./chunk-BXCPXRBR.js";
35
- import "./chunk-3BOHTH4V.js";
49
+ } from "./chunk-PIVGJM6K.js";
50
+ import {
51
+ mostRecent,
52
+ prReviewDir,
53
+ readPrReviewIndex,
54
+ runPrReview
55
+ } from "./chunk-UJ7XWK7J.js";
56
+ import {
57
+ listPullRequests,
58
+ publishGitState
59
+ } from "./chunk-J62QSVP6.js";
60
+ import {
61
+ runPrd
62
+ } from "./chunk-HJGGNLWI.js";
63
+ import {
64
+ resolveCommandIssue
65
+ } from "./chunk-FZDR7CGH.js";
36
66
  import {
37
- emptyRelations,
38
- getProvider,
39
- resolveCommandIssue,
40
- uniqueIds
41
- } from "./chunk-AAY6KM7N.js";
42
- import "./chunk-EWDJENFW.js";
43
- import "./chunk-HDV3YDTG.js";
67
+ getProvider
68
+ } from "./chunk-PV4OX5TC.js";
44
69
  import {
45
70
  beginUsageScope,
46
71
  getRunUsageTotals
47
- } from "./chunk-OBLVLIUS.js";
48
- import "./chunk-HQYXVIH6.js";
49
- import "./chunk-47VIN33V.js";
72
+ } from "./chunk-664Y3NDX.js";
50
73
  import {
51
- executionPlanSchema,
74
+ assessDecomposition,
75
+ buildDecompositionReport,
76
+ proposeSubIssues,
77
+ resetAgentInvocationState
78
+ } from "./chunk-ARFVHC2F.js";
79
+ import {
80
+ JournalPublisher,
81
+ MultiPublisher
82
+ } from "./chunk-QCXPNSEL.js";
83
+ import {
84
+ describeRunAgents,
85
+ getActiveResilienceConfig,
86
+ initResilienceConfig,
52
87
  loadIssuesConfig,
53
88
  loadWebConfig,
89
+ onShutdown
90
+ } from "./chunk-CNBYCMSU.js";
91
+ import {
54
92
  resolveIssuePaths,
55
93
  resolveProjectPaths,
56
94
  resolveQueuePaths
57
- } from "./chunk-VQ5VN7DF.js";
95
+ } from "./chunk-HQAHNIAR.js";
58
96
  import {
59
97
  FilePublisher,
60
- NullPublisher,
98
+ MemoryPublisher,
99
+ formatDuration,
100
+ getIcons,
101
+ getInactivityTimeout,
102
+ getSessionPublisher,
61
103
  isVerbose,
62
- isoNow,
63
- loadTaskPlan,
64
104
  printError,
65
105
  printInfo,
66
106
  printSuccess,
67
107
  printWarning,
68
- saveTaskPlan,
108
+ setActivityCallback,
109
+ setInactivityTimeout,
69
110
  setOutputCallback,
70
111
  setSessionPublisher,
71
112
  setStoryStageCallback,
72
- setStoryUpdateCallback,
73
- writeFileAtomic
74
- } from "./chunk-HC2MT7BX.js";
113
+ setStoryUpdateCallback
114
+ } from "./chunk-IYV6KNVF.js";
115
+ import {
116
+ isoNow,
117
+ loadTaskPlan,
118
+ saveTaskPlan
119
+ } from "./chunk-DIG5XKQQ.js";
120
+ import {
121
+ acquireRunLock,
122
+ describeRunLockOwner
123
+ } from "./chunk-NEZZ6KDR.js";
124
+ import {
125
+ committedStoryIds,
126
+ describePreflight,
127
+ getBaseBranch,
128
+ preflightRepository,
129
+ run
130
+ } from "./chunk-KQHB7ZT3.js";
75
131
 
76
132
  // src/commands/run.ts
77
133
  import { randomUUID } from "crypto";
78
- import { mkdir as mkdir2 } from "fs/promises";
134
+ import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
79
135
  import { join } from "path";
80
136
  import { execa } from "execa";
81
137
 
82
- // src/core/pipeline.ts
83
- var PIPELINE_PHASES = ["init", "prd", "plan", "execute", "review", "pr"];
84
- var PIPELINE_PHASES_WITH_PR_REVIEW = [...PIPELINE_PHASES, "pr-review"];
85
- var PIPELINE_PHASES_NO_BRANCH = PIPELINE_PHASES.filter(
86
- (p) => p !== "pr"
87
- );
88
- var PHASE_TO_FIELD = {
89
- init: null,
90
- prd: "prdCompleted",
91
- plan: "jsonCompleted",
92
- execute: "executionCompleted",
93
- review: "reviewCompleted",
94
- pr: "prCreated",
95
- "pr-review": "prReviewCompleted"
96
- };
97
- var PipelineManager = class {
98
- tasksJsonPath;
99
- plan;
100
- activePhases;
101
- constructor(plan, tasksJsonPath, activePhases) {
102
- this.plan = plan;
103
- this.tasksJsonPath = tasksJsonPath;
104
- this.activePhases = activePhases ?? PIPELINE_PHASES;
105
- }
106
- /**
107
- * Reload state from disk.
108
- */
109
- async reload() {
110
- this.plan = await loadTaskPlan(this.tasksJsonPath);
111
- }
112
- /**
113
- * Check if a phase is complete.
114
- */
115
- isPhaseComplete(phase) {
116
- const field = PHASE_TO_FIELD[phase];
117
- if (field === null) return true;
118
- return this.plan.pipeline?.[field] ?? false;
119
- }
120
- /**
121
- * Get the first incomplete phase in the pipeline.
122
- * Returns null if all phases are complete.
123
- */
124
- getNextPhase() {
125
- for (const phase of this.activePhases) {
126
- if (!this.isPhaseComplete(phase)) {
127
- return phase;
128
- }
129
- }
130
- return null;
131
- }
132
- /**
133
- * Get the current phase (alias for getNextPhase).
134
- */
135
- getCurrentPhase() {
136
- return this.getNextPhase();
137
- }
138
- /**
139
- * Check whether we can resume from a specific phase.
140
- * All prerequisite phases must be complete.
141
- */
142
- canResume(fromPhase) {
143
- const idx = this.activePhases.indexOf(fromPhase);
144
- if (idx < 0) return false;
145
- for (let i = 0; i < idx; i++) {
146
- if (!this.isPhaseComplete(this.activePhases[i])) {
147
- return false;
148
- }
149
- }
150
- return true;
151
- }
152
- /**
153
- * Mark a phase as complete and persist to disk.
154
- */
155
- async markPhaseComplete(phase) {
156
- const field = PHASE_TO_FIELD[phase];
157
- if (field === null) return;
158
- this.plan = {
159
- ...this.plan,
160
- pipeline: {
161
- ...this.plan.pipeline ?? {
162
- prdCompleted: false,
163
- jsonCompleted: false,
164
- executionCompleted: false,
165
- reviewCompleted: false,
166
- prCreated: false
167
- },
168
- [field]: true
169
- }
170
- };
171
- await saveTaskPlan(this.tasksJsonPath, this.plan);
172
- }
173
- };
174
-
175
- // src/execution/plan.ts
176
- import { mkdir, readFile } from "fs/promises";
138
+ // src/execution/detach.ts
139
+ import { spawn } from "child_process";
140
+ import { mkdir, open } from "fs/promises";
177
141
  import { dirname } from "path";
178
- import { ZodError } from "zod";
179
142
 
180
- // src/issues/graph.ts
181
- var DEFAULT_MAX_NODES = 25;
182
- var DEFAULT_MAX_DEPTH = 3;
183
- function dependencyEdges(graph) {
184
- const edges = [];
185
- const seen = /* @__PURE__ */ new Set();
186
- const add = (from, to) => {
187
- if (from === to || !graph.nodes.has(from) || !graph.nodes.has(to)) return;
188
- const key = `${from}->${to}`;
189
- if (seen.has(key)) return;
190
- seen.add(key);
191
- edges.push({ from, to });
192
- };
193
- for (const node of graph.nodes.values()) {
194
- for (const blocker of node.relations.blockedBy) {
195
- add(blocker, node.id);
196
- }
197
- for (const blocked of node.relations.blocking) {
198
- add(node.id, blocked);
199
- }
200
- }
201
- return edges;
202
- }
203
- function findCycles(graph) {
204
- const adjacency = /* @__PURE__ */ new Map();
205
- for (const { from, to } of dependencyEdges(graph)) {
206
- const list = adjacency.get(from);
207
- if (list === void 0) {
208
- adjacency.set(from, [to]);
209
- } else {
210
- list.push(to);
211
- }
212
- }
213
- const cycles = [];
214
- const reported = /* @__PURE__ */ new Set();
215
- const state = /* @__PURE__ */ new Map();
216
- const stack = [];
217
- const record = (cycle) => {
218
- let pivot = 0;
219
- for (let i = 1; i < cycle.length; i++) {
220
- if (compareIds(cycle[i], cycle[pivot]) < 0) pivot = i;
221
- }
222
- const normalized = [...cycle.slice(pivot), ...cycle.slice(0, pivot)];
223
- const key = normalized.join("->");
224
- if (reported.has(key)) return;
225
- reported.add(key);
226
- cycles.push(normalized);
227
- };
228
- const visit = (id) => {
229
- state.set(id, "visiting");
230
- stack.push(id);
231
- for (const next of adjacency.get(id) ?? []) {
232
- const status = state.get(next);
233
- if (status === "visiting") {
234
- record(stack.slice(stack.indexOf(next)));
235
- continue;
236
- }
237
- if (status === void 0) {
238
- visit(next);
239
- }
240
- }
241
- stack.pop();
242
- state.set(id, "done");
243
- };
244
- for (const id of graph.nodes.keys()) {
245
- if (!state.has(id)) visit(id);
246
- }
247
- return cycles;
248
- }
249
- function compareIds(a, b) {
250
- const na = Number(a);
251
- const nb = Number(b);
252
- if (Number.isInteger(na) && Number.isInteger(nb)) {
253
- return na - nb;
143
+ // src/storage/run-log.ts
144
+ import { rename, stat } from "fs/promises";
145
+ var DEFAULT_RUN_LOG_MAX_BYTES = 5 * 1024 * 1024;
146
+ async function rotateRunLogIfNeeded(filePath, rotatedPath, maxBytes = DEFAULT_RUN_LOG_MAX_BYTES) {
147
+ if (maxBytes <= 0) return;
148
+ try {
149
+ const size = (await stat(filePath)).size;
150
+ if (size < maxBytes) return;
151
+ } catch {
152
+ return;
254
153
  }
255
- return a < b ? -1 : a > b ? 1 : 0;
256
- }
257
- function neighbours(relations) {
258
- return uniqueIds(
259
- [
260
- ...relations.parent === null ? [] : [relations.parent],
261
- ...relations.children,
262
- ...relations.blockedBy,
263
- ...relations.blocking
264
- ],
265
- relations.id
266
- );
267
- }
268
- async function buildDependencyGraph(roots, fetchRelations, options = {}) {
269
- const maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES;
270
- const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
271
- const warn = options.warn ?? (() => {
272
- });
273
- const known = /* @__PURE__ */ new Map();
274
- for (const issue of options.known ?? []) {
275
- known.set(issue.id, issue);
276
- }
277
- const rootIds = uniqueIds(roots);
278
- const nodes = /* @__PURE__ */ new Map();
279
- const queued = new Set(rootIds);
280
- let truncated = false;
281
- let frontier = rootIds.map((id) => ({ id, depth: 0 }));
282
- while (frontier.length > 0) {
283
- const next = [];
284
- for (const { id, depth } of frontier) {
285
- if (nodes.size >= maxNodes) {
286
- truncated = true;
287
- break;
288
- }
289
- let relations = null;
290
- try {
291
- relations = await fetchRelations(id);
292
- } catch (err) {
293
- warn(
294
- `Could not read the relations of issue '${id}': ${err instanceof Error ? err.message : String(err)}`
295
- );
296
- }
297
- let issue = known.get(id) ?? null;
298
- if (issue === null && options.fetchIssue !== void 0) {
299
- try {
300
- issue = await options.fetchIssue(id);
301
- } catch (err) {
302
- warn(`Could not read issue '${id}': ${err instanceof Error ? err.message : String(err)}`);
303
- }
304
- }
305
- const resolved = relations ?? emptyRelations(id);
306
- nodes.set(id, { id, relations: resolved, depth, issue, root: rootIds.includes(id) });
307
- if (depth >= maxDepth) {
308
- if (neighbours(resolved).some((neighbour) => !queued.has(neighbour))) {
309
- truncated = true;
310
- }
311
- continue;
312
- }
313
- for (const neighbour of neighbours(resolved)) {
314
- if (queued.has(neighbour)) continue;
315
- queued.add(neighbour);
316
- next.push({ id: neighbour, depth: depth + 1 });
317
- }
318
- }
319
- if (nodes.size >= maxNodes && next.length > 0) {
320
- truncated = true;
321
- break;
322
- }
323
- frontier = next;
324
- }
325
- const graph = {
326
- roots: rootIds,
327
- nodes,
328
- cycles: [],
329
- truncated,
330
- limits: { maxNodes, maxDepth }
331
- };
332
- linkReferencedBy(graph);
333
- graph.cycles = findCycles(graph);
334
- return graph;
335
- }
336
- function linkReferencedBy(graph) {
337
- for (const node of graph.nodes.values()) {
338
- for (const cited of node.relations.references) {
339
- const target = graph.nodes.get(cited);
340
- if (target === void 0 || target.id === node.id) continue;
341
- target.relations.referencedBy = uniqueIds(
342
- [...target.relations.referencedBy, node.id],
343
- target.id
344
- );
345
- }
154
+ try {
155
+ await rename(filePath, rotatedPath);
156
+ } catch {
346
157
  }
347
158
  }
348
159
 
349
- // src/execution/order.ts
350
- var PRIORITY_LABELS = {
351
- high: "high",
352
- medium: "medium",
353
- low: "low"
354
- };
355
- var PRIORITY_RANK = { high: 0, medium: 1, low: 2 };
356
- var NO_PRIORITY_RANK = 3;
357
- function priorityOf(labels) {
358
- for (const label of labels) {
359
- const normalized = label.trim().toLowerCase();
360
- const direct = PRIORITY_LABELS[normalized];
361
- if (direct !== void 0) return direct;
362
- const qualified = /^(?:priority|prio|p)\s*[:/-]\s*(high|medium|low)$/.exec(normalized);
363
- const matched = qualified?.[1];
364
- if (matched !== void 0) return PRIORITY_LABELS[matched];
160
+ // src/execution/detach.ts
161
+ function backgroundRejection(mode, env = process.env) {
162
+ if (mode === "manual") {
163
+ return "--background cannot be used with --mode manual: a detached run cannot answer prompts.";
365
164
  }
366
- return null;
367
- }
368
- function hierarchyDepth(id, parentOf, selected) {
369
- let depth = 0;
370
- let current = parentOf.get(id) ?? null;
371
- const seen = /* @__PURE__ */ new Set([id]);
372
- while (current !== null && selected.has(current) && !seen.has(current)) {
373
- depth++;
374
- seen.add(current);
375
- current = parentOf.get(current) ?? null;
376
- }
377
- return depth;
378
- }
379
- function computeExecutionOrder(graph, options = {}) {
380
- const selected = new Set(
381
- (options.include ?? [...graph.nodes.keys()]).filter((id) => graph.nodes.has(id))
382
- );
383
- const parentOf = /* @__PURE__ */ new Map();
384
- const priority = /* @__PURE__ */ new Map();
385
- for (const id of selected) {
386
- const node = graph.nodes.get(id);
387
- parentOf.set(id, node?.relations.parent ?? null);
388
- priority.set(id, priorityOf(node?.issue?.labels ?? []));
389
- }
390
- const incoming = /* @__PURE__ */ new Map();
391
- const outgoing = /* @__PURE__ */ new Map();
392
- for (const id of selected) {
393
- incoming.set(id, /* @__PURE__ */ new Set());
394
- outgoing.set(id, []);
395
- }
396
- for (const { from, to } of dependencyEdges(graph)) {
397
- if (!selected.has(from) || !selected.has(to) || from === to) continue;
398
- incoming.get(to)?.add(from);
399
- outgoing.get(from)?.push(to);
400
- }
401
- const depth = /* @__PURE__ */ new Map();
402
- for (const id of selected) {
403
- depth.set(id, hierarchyDepth(id, parentOf, selected));
404
- }
405
- const rank = (id) => {
406
- const value = priority.get(id) ?? null;
407
- return value === null ? NO_PRIORITY_RANK : PRIORITY_RANK[value];
408
- };
409
- const better = (a, b) => {
410
- const byHierarchy = (depth.get(a) ?? 0) - (depth.get(b) ?? 0);
411
- if (byHierarchy !== 0) return byHierarchy;
412
- const byPriority = rank(a) - rank(b);
413
- if (byPriority !== 0) return byPriority;
414
- return compareIds(a, b);
415
- };
416
- const ready = [...selected].filter((id) => (incoming.get(id)?.size ?? 0) === 0);
417
- const order = [];
418
- while (ready.length > 0) {
419
- ready.sort(better);
420
- const next = ready.shift();
421
- order.push(next);
422
- for (const dependent of outgoing.get(next) ?? []) {
423
- const pending = incoming.get(dependent);
424
- if (pending === void 0) continue;
425
- pending.delete(next);
426
- if (pending.size === 0) {
427
- ready.push(dependent);
428
- }
429
- }
430
- }
431
- if (order.length !== selected.size) {
432
- const cycles = graph.cycles.length > 0 ? graph.cycles : findCycles(graph);
433
- const relevant = cycles.filter((cycle) => cycle.every((id) => selected.has(id)));
434
- return { ok: false, cycles: relevant.length > 0 ? relevant : cycles };
165
+ const ci = env.CI;
166
+ const inCi = ci !== void 0 && ci !== "" && ci !== "0" && ci.toLowerCase() !== "false";
167
+ if (inCi || !process.stdout.isTTY) {
168
+ return "--background needs an interactive terminal. In CI, run in the foreground so the exit code is visible.";
435
169
  }
436
- return { ok: true, order };
437
- }
438
- function describeCycles(cycles) {
439
- return cycles.map((cycle) => [...cycle, cycle[0]].map((id) => `#${id}`).join(" \u2192 ")).join("; ");
440
- }
441
-
442
- // src/execution/plan.ts
443
- function describeIssue(issue, id) {
444
- const parsed = Number.parseInt(id, 10);
445
- return {
446
- number: issue?.number ?? (Number.isNaN(parsed) ? null : parsed),
447
- title: issue?.title ?? "",
448
- url: issue?.remoteRef ?? null,
449
- source: issue?.source ?? "github"
450
- };
170
+ return null;
451
171
  }
452
- function buildExecutionPlan(input) {
453
- const now = input.now ?? isoNow;
454
- const at = now();
455
- const scheduled = new Set(input.order);
456
- const requested = new Set(input.requested);
457
- const issues = input.order.map((id, index) => {
458
- const node = input.graph.nodes.get(id);
459
- const relations = node?.relations;
460
- const described = describeIssue(node?.issue ?? null, id);
461
- return {
462
- id,
463
- number: described.number,
464
- title: described.title,
465
- url: described.url,
466
- source: described.source,
467
- position: index + 1,
468
- status: "pending",
469
- origin: requested.has(id) ? "requested" : "discovered",
470
- // Only the constraints that survive inside the queue: a blocker left out
471
- // of it is reported in `excluded`, not enforced as a dependency.
472
- dependsOn: (relations?.blockedBy ?? []).filter((blocker) => scheduled.has(blocker)),
473
- parent: relations?.parent ?? null,
474
- priority: priorityOf(node?.issue?.labels ?? []),
475
- heuristic: (relations?.heuristic.length ?? 0) > 0,
476
- failedPhase: null,
477
- lastError: null,
478
- startedAt: null,
479
- completedAt: null
480
- };
481
- });
482
- const excluded = [];
483
- for (const node of input.graph.nodes.values()) {
484
- if (scheduled.has(node.id)) continue;
485
- const described = describeIssue(node.issue, node.id);
486
- excluded.push({
487
- id: node.id,
488
- number: described.number,
489
- title: described.title,
490
- url: described.url,
491
- reason: "Discovered in the hierarchy but not selected for this run"
492
- });
493
- }
494
- return {
495
- schemaVersion: 1,
496
- id: input.requested[0] ?? input.order[0],
497
- project: input.projectId,
498
- requested: [...input.requested],
499
- branchName: null,
500
- noBranch: input.noBranch ?? false,
501
- prReview: input.prReview ?? false,
502
- status: "pending",
503
- createdAt: at,
504
- updatedAt: at,
505
- truncated: input.graph.truncated,
506
- issues,
507
- excluded
508
- };
172
+ function childArgv(argv) {
173
+ const next = [];
174
+ for (const arg of argv) {
175
+ if (arg === "--background" || arg === "-d") continue;
176
+ next.push(arg);
177
+ }
178
+ next.push("--detached-child");
179
+ return next;
509
180
  }
510
- async function loadExecutionPlan(path) {
511
- const raw = JSON.parse(await readFile(path, "utf-8"));
181
+ async function spawnDetachedRun(input) {
182
+ const entryScript = input.entryScript ?? process.argv[1];
183
+ if (entryScript === void 0) {
184
+ throw new Error("Could not determine the CLI entry point to spawn a detached run.");
185
+ }
186
+ await mkdir(dirname(input.paths.runLogFile), { recursive: true });
187
+ await rotateRunLogIfNeeded(input.paths.runLogFile, input.paths.rotatedRunLogFile);
188
+ const log = await open(input.paths.runLogFile, "a");
512
189
  try {
513
- return executionPlanSchema.parse(raw);
514
- } catch (err) {
515
- if (err instanceof ZodError) {
516
- const issues = err.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
517
- throw new Error(`Invalid execution-plan.json at ${path}:
518
- ${issues}`);
190
+ const args = [entryScript, ...childArgv((input.argv ?? process.argv.slice(2)).slice())];
191
+ const spawnFn = input.spawnFn ?? spawn;
192
+ const child = spawnFn(input.execPath ?? process.execPath, args, {
193
+ detached: true,
194
+ stdio: ["ignore", log.fd, log.fd],
195
+ cwd: input.cwd ?? process.cwd(),
196
+ env: input.env ?? process.env
197
+ });
198
+ if (child.pid === void 0) {
199
+ throw new Error("The detached run started but reported no pid.");
519
200
  }
520
- throw err;
201
+ child.unref();
202
+ return { pid: child.pid, logFile: input.paths.runLogFile };
203
+ } finally {
204
+ await log.close();
521
205
  }
522
206
  }
523
- async function saveExecutionPlan(path, plan, now = isoNow) {
524
- await mkdir(dirname(path), { recursive: true });
525
- const stamped = { ...plan, updatedAt: now() };
526
- await writeFileAtomic(path, `${JSON.stringify(stamped, null, 2)}
527
- `);
528
- }
529
- function nextQueueIssue(plan) {
530
- return plan.issues.find((entry) => entry.status === "in_progress") ?? plan.issues.find((entry) => entry.status === "failed") ?? plan.issues.find((entry) => entry.status === "pending") ?? null;
531
- }
532
- function queueStatus(plan) {
533
- if (plan.issues.some((entry) => entry.status === "failed")) return "failed";
534
- if (plan.issues.every((entry) => entry.status === "completed")) return "completed";
535
- if (plan.issues.some((entry) => entry.status !== "pending")) return "in_progress";
536
- return "pending";
537
- }
538
- function isQueueComplete(plan) {
539
- return plan.issues.every((entry) => entry.status === "completed");
540
- }
541
- function updateIssue(plan, id, patch) {
542
- const issues = plan.issues.map((entry) => entry.id === id ? { ...entry, ...patch } : entry);
543
- const next = { ...plan, issues };
544
- return { ...next, status: queueStatus(next) };
545
- }
546
- function markQueueIssueInProgress(plan, id, now = isoNow) {
547
- const current = plan.issues.find((entry) => entry.id === id);
548
- return updateIssue(plan, id, {
549
- status: "in_progress",
550
- // Preserved across a resume: the queue reports when the work on this Issue
551
- // really began, not when it was last picked up.
552
- startedAt: current?.startedAt ?? now(),
553
- failedPhase: null,
554
- lastError: null
555
- });
556
- }
557
- function markQueueIssueCompleted(plan, id, now = isoNow) {
558
- return updateIssue(plan, id, {
559
- status: "completed",
560
- completedAt: now(),
561
- failedPhase: null,
562
- lastError: null
563
- });
564
- }
565
- function markQueueIssueFailed(plan, id, failure2) {
566
- return updateIssue(plan, id, {
567
- status: "failed",
568
- failedPhase: failure2.phase,
569
- lastError: failure2.error
570
- });
571
- }
572
- function setQueueBranch(plan, branchName) {
573
- return { ...plan, branchName };
574
- }
575
- function setQueuePullRequest(plan, pullRequest) {
576
- return { ...plan, pullRequest };
577
- }
578
207
 
579
208
  // src/execution/confirm.ts
580
209
  import { createInterface } from "readline";
@@ -615,16 +244,25 @@ function buildQueueSummaryLines(plan) {
615
244
  const primary = plan.issues.find((entry) => entry.id === plan.id) ?? plan.issues[0];
616
245
  const lines = [];
617
246
  if (primary !== void 0) {
618
- lines.push(` Main issue: ${issueLabel(primary)}${primary.title ? ` ${primary.title}` : ""}`);
247
+ const role = primary.role === "container" ? " (container)" : "";
248
+ lines.push(
249
+ ` Main issue: ${issueLabel(primary)}${primary.title ? ` ${primary.title}` : ""}${role}`
250
+ );
619
251
  }
620
- lines.push(` Total issues: ${plan.issues.length}`);
252
+ const executables = plan.issues.filter((entry) => entry.role !== "container").length;
253
+ const containers = plan.issues.length - executables;
254
+ lines.push(
255
+ containers > 0 ? ` Total issues: ${executables} executable + ${containers} container` : ` Total issues: ${plan.issues.length}`
256
+ );
621
257
  lines.push(" Suggested order:");
622
258
  for (const entry of plan.issues) {
623
259
  const reason = reasonFor(entry);
624
260
  const flag = entry.heuristic ? " ~" : "";
261
+ const external = entry.externalDependencies.length > 0 ? ` \u26A0 depends on ${entry.externalDependencies.map((id) => `#${id}`).join(", ")} (outside this scope)` : "";
262
+ const role = entry.role === "container" ? " (container)" : "";
625
263
  const title = entry.title === "" ? "" : ` ${entry.title}`;
626
264
  lines.push(
627
- ` ${entry.position}. ${issueLabel(entry)}${title}${reason === "" ? "" : ` (${reason})`}${flag}`
265
+ ` ${entry.position}. ${issueLabel(entry)}${title}${role}${reason === "" ? "" : ` (${reason})`}${flag}${external}`
628
266
  );
629
267
  }
630
268
  if (plan.issues.some((entry) => entry.heuristic)) {
@@ -635,6 +273,23 @@ function buildQueueSummaryLines(plan) {
635
273
  }
636
274
  return lines;
637
275
  }
276
+ function buildScopeSummaryLine(plan, choice) {
277
+ if (choice === "cancel") return null;
278
+ const primary = plan.requested[0] ?? plan.id;
279
+ if (choice === "requested") {
280
+ return `Scope: ${plan.requested.length} requested issue(s).`;
281
+ }
282
+ if (choice === "cascade") {
283
+ const executables = plan.issues.filter((entry) => entry.role !== "container").length;
284
+ return `Scope: hierarchy of #${primary} (${executables} executable, container not implemented).`;
285
+ }
286
+ return `Scope: ${plan.issues.length} issues from the hierarchy of #${primary}.`;
287
+ }
288
+ function requestedIsContainer(plan) {
289
+ return plan.issues.some(
290
+ (entry) => plan.requested.includes(entry.id) && entry.role === "container"
291
+ );
292
+ }
638
293
  async function confirmQueue(plan, requestedOnlyCount, options = {}) {
639
294
  const info = options.info ?? printInfo;
640
295
  const warn = options.warn ?? printWarning;
@@ -643,19 +298,41 @@ async function confirmQueue(plan, requestedOnlyCount, options = {}) {
643
298
  (options.stdout ?? process.stdout).write(`${line}
644
299
  `);
645
300
  }
301
+ const container = requestedIsContainer(plan);
646
302
  if (options.only === true) {
303
+ if (container) {
304
+ warn(
305
+ `${plan.requested.map((id) => `#${id}`).join(", ")} is a container. --only will run it as an executable issue.`
306
+ );
307
+ }
647
308
  info(`--only: running just the ${requestedOnlyCount} issue(s) you asked for.`);
309
+ info(buildScopeSummaryLine(plan, "requested") ?? "");
648
310
  return "requested";
649
311
  }
312
+ if (options.cascade === true || options.yes === true && container) {
313
+ info(
314
+ `${options.cascade === true ? "--cascade" : "--yes"}: running the hierarchy of the container, without implementing it.`
315
+ );
316
+ info(buildScopeSummaryLine(plan, "cascade") ?? "");
317
+ return "cascade";
318
+ }
650
319
  if (options.yes === true) {
651
320
  info(`--yes: running the whole hierarchy (${plan.issues.length} issues).`);
321
+ info(buildScopeSummaryLine(plan, "all") ?? "");
652
322
  return "all";
653
323
  }
654
324
  const interactive = options.interactive ?? isInteractiveByDefault();
325
+ if (!interactive && container) {
326
+ throw new QueueConfirmationError(
327
+ `${plan.requested.map((id) => `#${id}`).join(", ")} is a container (umbrella) and the terminal is not interactive. Re-run with --cascade to execute its children, or --only to execute just that issue.`
328
+ );
329
+ }
655
330
  if (!interactive && options.singleRequest === true) {
656
331
  warn(
657
332
  `A larger structure was found, but the terminal is not interactive: running just issue ${plan.requested.map((id) => `#${id}`).join(", ")}. Re-run with --yes to execute the whole hierarchy.`
658
333
  );
334
+ const summary2 = buildScopeSummaryLine(plan, "requested");
335
+ if (summary2) info(summary2);
659
336
  return "requested";
660
337
  }
661
338
  if (!interactive) {
@@ -663,10 +340,19 @@ async function confirmQueue(plan, requestedOnlyCount, options = {}) {
663
340
  "This run involves more than one issue and the terminal is not interactive. Re-run with --yes to execute the whole hierarchy, or --only to execute just the issues you informed."
664
341
  );
665
342
  }
666
- const query = `Which scope should run? [1] Only the issues informed (${requestedOnlyCount}) [2] The whole hierarchy (${plan.issues.length}) [3] Cancel: `;
667
- return await ask(query, options.stdin ?? process.stdin, options.stdout ?? process.stdout, warn);
343
+ const query = container ? `Which scope should run? [1] The hierarchy of #${plan.id} (${plan.issues.filter((entry) => entry.role !== "container").length} issues) [2] Include dependencies outside it [3] Just #${plan.id} itself [4] Cancel: ` : `Which scope should run? [1] Only the issues informed (${requestedOnlyCount}) [2] The whole hierarchy (${plan.issues.length}) [3] Cancel: `;
344
+ const choice = await ask(
345
+ query,
346
+ options.stdin ?? process.stdin,
347
+ options.stdout ?? process.stdout,
348
+ warn,
349
+ container ? "container" : "plain"
350
+ );
351
+ const summary = buildScopeSummaryLine(plan, choice);
352
+ if (summary) info(summary);
353
+ return choice;
668
354
  }
669
- async function ask(query, stdin, stdout, warn) {
355
+ async function ask(query, stdin, stdout, warn, mode = "plain") {
670
356
  const rl = createInterface({ input: stdin, output: stdout });
671
357
  const queued = [];
672
358
  let pending = null;
@@ -708,6 +394,14 @@ async function ask(query, stdin, stdout, warn) {
708
394
  return "cancel";
709
395
  }
710
396
  const choice = answer.trim();
397
+ if (mode === "container") {
398
+ if (choice === "" || choice === "1") return "cascade";
399
+ if (choice === "2") return "all";
400
+ if (choice === "3") return "requested";
401
+ if (choice === "4") return "cancel";
402
+ warn(`Invalid choice: '${choice}'. Enter 1, 2, 3 or 4.`);
403
+ continue;
404
+ }
711
405
  if (choice === "1") return "requested";
712
406
  if (choice === "" || choice === "2") return "all";
713
407
  if (choice === "3") return "cancel";
@@ -750,6 +444,14 @@ async function discoverIssueGraph(roots, options = {}) {
750
444
  }
751
445
 
752
446
  // src/execution/queue.ts
447
+ function forceRequestedExecutable(plan) {
448
+ return {
449
+ ...plan,
450
+ issues: plan.issues.map(
451
+ (entry) => plan.requested.includes(entry.id) ? { ...entry, role: "executable" } : entry
452
+ )
453
+ };
454
+ }
753
455
  async function loadResumableQueue(planFile, requested) {
754
456
  let plan;
755
457
  try {
@@ -834,7 +536,30 @@ async function planQueue(input) {
834
536
  return { kind: "stop", code: 1 };
835
537
  }
836
538
  let plan = suggestedPlan;
837
- if (choice === "requested") {
539
+ if (choice === "cascade") {
540
+ const include = collectCascadeIds(graph, input.requested);
541
+ const cascadeOrder = computeExecutionOrder(graph, { include });
542
+ if (!cascadeOrder.ok) {
543
+ warn(`Dependency cycle between issues: ${describeCycles(cascadeOrder.cycles)}.`);
544
+ return { kind: "stop", code: 1 };
545
+ }
546
+ if (cascadeOrder.order.filter(
547
+ (id) => suggestedPlan.issues.find((entry) => entry.id === id)?.role !== "container"
548
+ ).length === 0) {
549
+ warn(
550
+ `Issue #${input.requested[0]} is a container with no executable children in scope. Re-run with --only if you really want to implement it.`
551
+ );
552
+ return { kind: "stop", code: 1 };
553
+ }
554
+ plan = buildExecutionPlan({
555
+ projectId: input.projectId,
556
+ requested: input.requested,
557
+ graph,
558
+ order: cascadeOrder.order,
559
+ noBranch: input.noBranch,
560
+ prReview: input.prReview
561
+ });
562
+ } else if (choice === "requested") {
838
563
  if (!requestedOrder.ok) {
839
564
  warn(
840
565
  `Dependency cycle between the issues you informed: ${describeCycles(requestedOrder.cycles)}.`
@@ -851,14 +576,16 @@ async function planQueue(input) {
851
576
  }
852
577
  return { kind: "single" };
853
578
  }
854
- plan = buildExecutionPlan({
855
- projectId: input.projectId,
856
- requested: input.requested,
857
- graph,
858
- order: requestedOrder.order,
859
- noBranch: input.noBranch,
860
- prReview: input.prReview
861
- });
579
+ plan = forceRequestedExecutable(
580
+ buildExecutionPlan({
581
+ projectId: input.projectId,
582
+ requested: input.requested,
583
+ graph,
584
+ order: requestedOrder.order,
585
+ noBranch: input.noBranch,
586
+ prReview: input.prReview
587
+ })
588
+ );
862
589
  }
863
590
  await saveExecutionPlan(input.planFile, plan);
864
591
  return { kind: "queue", plan, resumed: false };
@@ -902,6 +629,34 @@ function parseIssueArguments(values) {
902
629
 
903
630
  // src/ui/pipeline-renderer.ts
904
631
  import { Listr, PRESET_TIMER, PRESET_TIMESTAMP } from "listr2";
632
+
633
+ // src/ui/status-view.ts
634
+ function renderExecuteFocus(snapshot, options = {}) {
635
+ const icons = options.icons ?? getIcons();
636
+ const story = activeStory(snapshot);
637
+ if (story === void 0 && snapshot.currentActivity === null) return [];
638
+ const lines = [];
639
+ if (story !== void 0) {
640
+ const duration = story.durationSeconds !== null && story.durationSeconds !== void 0 ? ` ${formatDuration(story.durationSeconds)}` : "";
641
+ lines.push(` ${icons.pending} ${story.id} ${stripMarkdown(story.title)}${duration}`);
642
+ }
643
+ const activity = snapshot.currentActivity;
644
+ if (activity?.tool) {
645
+ const detail = activity.detail ? ` ${stripMarkdown(activity.detail)}` : "";
646
+ lines.push(` ${activity.tool}${detail}`);
647
+ }
648
+ return lines;
649
+ }
650
+ function activeStory(snapshot) {
651
+ const fromActivity = snapshot.currentActivity?.story;
652
+ if (fromActivity) {
653
+ const match = snapshot.stories.find((story) => story.id === fromActivity);
654
+ if (match) return match;
655
+ }
656
+ return snapshot.stories.find((story) => story.stage === "executing");
657
+ }
658
+
659
+ // src/ui/pipeline-renderer.ts
905
660
  var PHASE_LABELS = {
906
661
  prd: "PRD",
907
662
  plan: "Plan",
@@ -950,6 +705,13 @@ function createActiveStoryTracker(stories, storyTasks) {
950
705
  }
951
706
  };
952
707
  }
708
+ function executeExpandsStories(verbose) {
709
+ return verbose;
710
+ }
711
+ function paintExecuteFocus(task) {
712
+ const lines = renderExecuteFocus(getSessionPublisher().snapshot());
713
+ if (lines.length > 0) task.output = lines.join("\n");
714
+ }
953
715
  function buildExecutePhaseTask(runner, tasksPath, verbose) {
954
716
  return async (_ctx, task) => {
955
717
  let stories;
@@ -970,6 +732,31 @@ function buildExecutePhaseTask(runner, tasksPath, verbose) {
970
732
  const totalStories = stories.length;
971
733
  const initialPassed = stories.filter((s) => s.passes).length;
972
734
  task.title = `Execute (${initialPassed}/${totalStories} stories passing)`;
735
+ if (!executeExpandsStories(verbose)) {
736
+ setStoryUpdateCallback((updatedStories) => {
737
+ const passed = updatedStories.filter((s) => s.passes).length;
738
+ task.title = `Execute (${passed}/${totalStories} stories passing)`;
739
+ paintExecuteFocus(task);
740
+ });
741
+ setStoryStageCallback(() => {
742
+ paintExecuteFocus(task);
743
+ });
744
+ setActivityCallback(() => {
745
+ paintExecuteFocus(task);
746
+ });
747
+ setOutputCallback((line) => {
748
+ task.output = line;
749
+ });
750
+ try {
751
+ await runner();
752
+ } finally {
753
+ setOutputCallback(void 0);
754
+ setStoryUpdateCallback(void 0);
755
+ setStoryStageCallback(void 0);
756
+ setActivityCallback(void 0);
757
+ }
758
+ return;
759
+ }
973
760
  const resolvers = /* @__PURE__ */ new Map();
974
761
  setStoryUpdateCallback((updatedStories) => {
975
762
  const passed = updatedStories.filter((s) => s.passes).length;
@@ -1036,14 +823,14 @@ function buildExecutePhaseTask(runner, tasksPath, verbose) {
1036
823
  };
1037
824
  }
1038
825
  async function runPipelineWithRenderer(options) {
1039
- const { phases, startIndex, verbose, runners, tasksPath } = options;
826
+ const { phases, startIndex, verbose, runners, tasksPath, phaseSuffixes } = options;
1040
827
  const overallStart = Date.now();
1041
828
  let currentPhase;
1042
829
  const renderer = selectRenderer(verbose);
1043
830
  const isSimple = renderer === "simple";
1044
831
  const tasks = new Listr(
1045
832
  phases.map((phase, index) => {
1046
- const label = phaseLabel(phase);
833
+ const label = phaseSuffixes?.[phase] ? `${phaseLabel(phase)} (${phaseSuffixes[phase]})` : phaseLabel(phase);
1047
834
  const isExecutePhase = phase === "execute" && tasksPath;
1048
835
  return {
1049
836
  title: label,
@@ -1096,6 +883,7 @@ async function runPipelineWithRenderer(options) {
1096
883
  setOutputCallback(void 0);
1097
884
  setStoryUpdateCallback(void 0);
1098
885
  setStoryStageCallback(void 0);
886
+ setActivityCallback(void 0);
1099
887
  }
1100
888
  const overallElapsedSeconds = Math.floor((Date.now() - overallStart) / 1e3);
1101
889
  return {
@@ -1110,6 +898,35 @@ function toIssueNumber(id) {
1110
898
  const parsed = Number.parseInt(id, 10);
1111
899
  return Number.isNaN(parsed) ? null : parsed;
1112
900
  }
901
+ async function publishInstrumentedPhaseEnd(publisher, phase, issueNumber, success, error) {
902
+ const at = isoNow();
903
+ const startedAt = publisher.snapshot().phases.find((entry) => entry.name === phase)?.startedAt;
904
+ const wallMs = startedAt === null || startedAt === void 0 ? null : Math.max(0, Date.parse(at) - Date.parse(startedAt));
905
+ const timing = await loadPhaseTiming(phase, wallMs);
906
+ publisher.publish({
907
+ type: "phase:end",
908
+ at,
909
+ phase,
910
+ success,
911
+ ...error === void 0 ? {} : { error },
912
+ ...snapshotTimingFields(timing)
913
+ });
914
+ if (isVerbose()) {
915
+ printInfo(
916
+ formatPhaseLine({
917
+ issueNumber: toIssueNumber(issueNumber) ?? issueNumber,
918
+ phase,
919
+ iteration: timing.iteration,
920
+ wallMs,
921
+ cliDurationMs: timing.cliDurationMs,
922
+ harnessStartupMs: timing.harnessStartupMs,
923
+ ttftMs: timing.ttftMs,
924
+ numTurns: timing.numTurns,
925
+ outputTokens: timing.outputTokens
926
+ })
927
+ );
928
+ }
929
+ }
1113
930
  function publishStorySeed(publisher, stories, at) {
1114
931
  if (stories.length === 0) return false;
1115
932
  publisher.publish({ type: "stories:update", at, stories: [...stories] });
@@ -1140,6 +957,10 @@ var QUEUE_PR_PHASES_WITH_REVIEW = [
1140
957
  ];
1141
958
  var RUNNABLE_QUEUE_PR_PHASES = ["pr"];
1142
959
  var RUNNABLE_QUEUE_PR_PHASES_WITH_REVIEW = ["pr", "pr-review"];
960
+ function verificationForSummary(value) {
961
+ if (value === null || value.verdict === null) return null;
962
+ return { verdict: value.verdict, level: value.level };
963
+ }
1143
964
  async function readPrReviewOutcome(issue, tasksPath, requestedChanges) {
1144
965
  const outcome = { requestedChanges, recommendation: null, reportPath: null };
1145
966
  try {
@@ -1162,6 +983,7 @@ async function readPrReviewOutcome(issue, tasksPath, requestedChanges) {
1162
983
  }
1163
984
  return outcome;
1164
985
  }
986
+ var DEFAULT_MAX_ISSUE_ATTEMPTS = 2;
1165
987
  async function runPipeline(issue, mode, from, noBranch, prReview, options = {}) {
1166
988
  let requested;
1167
989
  try {
@@ -1170,34 +992,112 @@ async function runPipeline(issue, mode, from, noBranch, prReview, options = {})
1170
992
  printError(err instanceof Error ? err.message : String(err));
1171
993
  return 1;
1172
994
  }
1173
- const first = await runIssueSession(requested[0], mode, {
1174
- from,
1175
- noBranch,
1176
- prReview,
1177
- requested,
1178
- runOptions: options
1179
- });
1180
- if (first.queue === void 0) {
1181
- return first.code;
995
+ if (options.background === true && options.detachedChild !== true) {
996
+ const reason = backgroundRejection(mode);
997
+ if (reason !== null) {
998
+ printError(reason);
999
+ return 1;
1000
+ }
1001
+ return detachAfterConfirm(requested, noBranch, prReview, options);
1182
1002
  }
1183
- return runQueue(
1184
- first.queue.plan,
1185
- { mode, from, noBranch, prReview, runOptions: options },
1186
- first.queue.resolved
1187
- );
1003
+ const ownership = await claimRunOwnership(requested[0], options.detachedChild === true);
1004
+ if (!ownership.ok) {
1005
+ printError(
1006
+ `Another issue-flow run owns this project: ${describeRunLockOwner(ownership.owner)}.`
1007
+ );
1008
+ printInfo("Wait for it to finish, or stop that process before running again.");
1009
+ return 1;
1010
+ }
1011
+ try {
1012
+ const first = await runIssueSession(requested[0], mode, {
1013
+ from,
1014
+ noBranch,
1015
+ prReview,
1016
+ requested,
1017
+ runOptions: options,
1018
+ ...ownership.interruptedBy === null ? {} : { interruptedBy: ownership.interruptedBy }
1019
+ });
1020
+ if (first.queue === void 0) {
1021
+ return first.code;
1022
+ }
1023
+ return runQueue(
1024
+ first.queue.plan,
1025
+ { mode, from, noBranch, prReview, runOptions: options },
1026
+ first.queue.resolved
1027
+ );
1028
+ } finally {
1029
+ await ownership.release();
1030
+ }
1031
+ }
1032
+ var WRITING_PHASES = /* @__PURE__ */ new Set(["plan", "execute", "pr"]);
1033
+ async function ensureRepositoryWritable(phase) {
1034
+ if (!WRITING_PHASES.has(phase)) return;
1035
+ const preflight = await preflightRepository({ intent: "resume-same-phase" });
1036
+ if (preflight.ok) return;
1037
+ for (const line of describePreflight(preflight)) {
1038
+ printError(line);
1039
+ }
1040
+ throw new Error(`The repository is not in a state the ${phase} phase can write to`);
1041
+ }
1042
+ async function claimRunOwnership(target, detached = false) {
1043
+ let lockFile;
1044
+ try {
1045
+ lockFile = (await resolveProjectPaths()).runLockFile;
1046
+ } catch {
1047
+ return { ok: true, interruptedBy: null, release: async () => {
1048
+ } };
1049
+ }
1050
+ const result = await acquireRunLock(lockFile, { target, detached });
1051
+ if (!result.ok) return result;
1052
+ return {
1053
+ ok: true,
1054
+ interruptedBy: result.handle.reclaimedFrom,
1055
+ release: () => result.handle.release()
1056
+ };
1188
1057
  }
1189
1058
  async function runIssueSession(issueNumber, mode, input) {
1059
+ resetAgentInvocationState();
1190
1060
  const paths = await resolveIssuePaths(issueNumber);
1061
+ const resilience = await initResilienceConfig();
1062
+ const configuredInactivity = resilience.watchdog?.inactivityTimeoutMs;
1063
+ if (configuredInactivity !== void 0 && getInactivityTimeout() === void 0) {
1064
+ setInactivityTimeout(configuredInactivity);
1065
+ }
1191
1066
  const webConfig = await loadWebConfig();
1192
- let publisher = new NullPublisher();
1193
- if (webConfig.enabled) {
1067
+ const surfaces = [];
1068
+ const journalEnabled = resilience.journal?.enabled === true;
1069
+ const persistSnapshot = webConfig.enabled || input.runOptions?.detachedChild === true;
1070
+ if (persistSnapshot || journalEnabled) {
1194
1071
  await mkdir2(paths.issueDir, { recursive: true });
1195
- publisher = new FilePublisher(paths.sessionFile, {
1196
- logLimit: webConfig.logLimit,
1197
- includeLogs: webConfig.includeLogs
1198
- });
1199
1072
  }
1073
+ if (persistSnapshot) {
1074
+ surfaces.push(
1075
+ new FilePublisher(paths.sessionFile, {
1076
+ logLimit: webConfig.logLimit,
1077
+ includeLogs: webConfig.includeLogs
1078
+ })
1079
+ );
1080
+ }
1081
+ if (journalEnabled) {
1082
+ surfaces.push(
1083
+ new JournalPublisher(paths.eventsFile, paths.rotatedEventsFile, {
1084
+ logLimit: webConfig.logLimit,
1085
+ includeLogs: webConfig.includeLogs,
1086
+ ...resilience.journal?.maxFileBytes === void 0 ? {} : { maxFileBytes: resilience.journal.maxFileBytes }
1087
+ })
1088
+ );
1089
+ }
1090
+ const publisher = surfaces.length === 0 ? new MemoryPublisher() : surfaces.length === 1 ? surfaces[0] : new MultiPublisher(surfaces);
1200
1091
  setSessionPublisher(publisher);
1092
+ if (input.interruptedBy !== void 0) {
1093
+ const previous = input.interruptedBy;
1094
+ publisher.publish({
1095
+ type: "log",
1096
+ at: isoNow(),
1097
+ level: "warn",
1098
+ message: `Previous run interrupted: ${describeRunLockOwner(previous)}. Its lock was stale and has been taken over.`
1099
+ });
1100
+ }
1201
1101
  if (webConfig.enabled) {
1202
1102
  await ensureWebMonitor({
1203
1103
  publisher,
@@ -1213,10 +1113,34 @@ async function runIssueSession(issueNumber, mode, input) {
1213
1113
  storyCount: 0,
1214
1114
  elapsedSeconds: 0
1215
1115
  };
1116
+ const releaseCheckpoint = onShutdown({
1117
+ phase: "checkpoint",
1118
+ run: async () => {
1119
+ await pauseIssue(paths.tasksFile, issueNumber);
1120
+ publisher.publish({
1121
+ type: "log",
1122
+ at: isoNow(),
1123
+ level: "warn",
1124
+ message: `Interrupted during issue #${issueNumber}. A checkpoint was saved; resume with \`issue-flow resume ${issueNumber}\`.`
1125
+ });
1126
+ publisher.publish({ type: "session:end", at: isoNow(), status: "failed" });
1127
+ }
1128
+ });
1129
+ const releaseClose = onShutdown({
1130
+ phase: "close",
1131
+ run: async () => {
1132
+ await publisher.close();
1133
+ }
1134
+ });
1216
1135
  try {
1217
1136
  result = await runPipelinePhases(issueNumber, paths, mode, publisher, input);
1137
+ if (result.code !== 0) {
1138
+ await reportIfOversized(issueNumber, paths, result);
1139
+ }
1218
1140
  return result;
1219
1141
  } finally {
1142
+ releaseCheckpoint();
1143
+ releaseClose();
1220
1144
  if (result.queue === void 0) {
1221
1145
  publisher.publish({
1222
1146
  type: "session:end",
@@ -1228,10 +1152,127 @@ async function runIssueSession(issueNumber, mode, input) {
1228
1152
  setSessionPublisher(void 0);
1229
1153
  }
1230
1154
  }
1155
+ async function reportIfOversized(issueNumber, paths, result) {
1156
+ try {
1157
+ const journal = `${await readFileIfPresent(paths.rotatedEventsFile)}${await readFileIfPresent(paths.eventsFile)}`;
1158
+ const plan = await loadTaskPlan(paths.tasksFile).catch(() => null);
1159
+ const assessment = assessDecomposition({
1160
+ journal,
1161
+ plan,
1162
+ filesTouched: await countChangedFiles(),
1163
+ hitMaxIterations: result.failedPhase === "execute" && plan?.issueStatus !== "completed"
1164
+ });
1165
+ if (!assessment.oversized) return;
1166
+ const report = buildDecompositionReport({
1167
+ issueNumber,
1168
+ assessment,
1169
+ plan,
1170
+ at: isoNow()
1171
+ });
1172
+ const reportFile = paths.decompositionFile;
1173
+ await mkdir2(paths.issueDir, { recursive: true });
1174
+ await writeFile(reportFile, report, "utf-8");
1175
+ printWarning(`Issue #${issueNumber} looks larger than one run. Report: ${reportFile}`);
1176
+ for (const signal of assessment.signals) {
1177
+ printWarning(` ${signal.detail}`);
1178
+ }
1179
+ await maybeCreateSubIssues(issueNumber, plan, reportFile);
1180
+ if (plan !== null) {
1181
+ await saveTaskPlan(paths.tasksFile, {
1182
+ ...plan,
1183
+ runState: {
1184
+ currentPhase: plan.runState?.currentPhase ?? result.failedPhase,
1185
+ attempt: plan.runState?.attempt ?? 0,
1186
+ owner: null,
1187
+ status: "blocked",
1188
+ blockedReason: `Looks larger than one run; see ${reportFile}`,
1189
+ lastHeartbeatAt: isoNow()
1190
+ }
1191
+ });
1192
+ }
1193
+ } catch {
1194
+ }
1195
+ }
1196
+ async function maybeCreateSubIssues(issueNumber, plan, reportFile) {
1197
+ if (getActiveResilienceConfig().decompose?.auto !== true) {
1198
+ printInfo(`Nothing was split. Read ${reportFile} and decide.`);
1199
+ return;
1200
+ }
1201
+ const committed = await committedStoryIds(await getBaseBranch()).catch(() => /* @__PURE__ */ new Set());
1202
+ if (committed.size > 0) {
1203
+ printWarning(
1204
+ `--auto-decompose did not run: ${committed.size} story(ies) are already committed on this branch. Splitting on top of committed work needs a person.`
1205
+ );
1206
+ return;
1207
+ }
1208
+ const proposals = proposeSubIssues(plan);
1209
+ if (proposals.length === 0) {
1210
+ printInfo("Nothing left to split: no pending story.");
1211
+ return;
1212
+ }
1213
+ const { runGenerate } = await import("./generate-MVQI7A45.js");
1214
+ for (const proposal of proposals) {
1215
+ const instruction = [
1216
+ `Create a sub-issue of #${issueNumber}: ${proposal.title}.`,
1217
+ proposal.dependsOn.length === 0 ? "" : `It depends on: ${proposal.dependsOn.join(", ")}.`,
1218
+ "It covers exactly these user stories, and nothing else:",
1219
+ ...proposal.stories.map((story) => `- ${story.id} ${story.title}: ${story.description}`)
1220
+ ].filter((line) => line !== "").join("\n");
1221
+ const code = await runGenerate(instruction);
1222
+ if (code !== 0) {
1223
+ printWarning(
1224
+ `Could not create "${proposal.title}". The report at ${reportFile} still stands.`
1225
+ );
1226
+ return;
1227
+ }
1228
+ }
1229
+ }
1230
+ async function countChangedFiles() {
1231
+ try {
1232
+ const base = await getBaseBranch();
1233
+ const result = await run("git", ["diff", "--name-only", `${base}...HEAD`]);
1234
+ if (result.exitCode !== 0) return 0;
1235
+ return result.stdout.split("\n").filter((line) => line.trim() !== "").length;
1236
+ } catch {
1237
+ return 0;
1238
+ }
1239
+ }
1240
+ async function readFileIfPresent(path) {
1241
+ try {
1242
+ return await readFile(path, "utf-8");
1243
+ } catch {
1244
+ return "";
1245
+ }
1246
+ }
1247
+ async function pauseIssue(tasksFile, issueNumber) {
1248
+ try {
1249
+ const plan = await loadTaskPlan(tasksFile);
1250
+ await saveTaskPlan(tasksFile, {
1251
+ ...plan,
1252
+ runState: {
1253
+ ...plan.runState ?? {
1254
+ currentPhase: null,
1255
+ attempt: 0,
1256
+ lastHeartbeatAt: null,
1257
+ blockedReason: null,
1258
+ owner: null
1259
+ },
1260
+ status: "paused",
1261
+ lastHeartbeatAt: isoNow()
1262
+ }
1263
+ });
1264
+ } catch {
1265
+ printWarning(`Could not write a checkpoint for issue #${issueNumber}.`);
1266
+ }
1267
+ }
1231
1268
  async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
1232
1269
  const { from, noBranch, prReview, queue } = input;
1233
1270
  const continueNumbering = input.runOptions?.continueNumbering;
1234
1271
  const startUs = input.runOptions?.startUs;
1272
+ const executeRetry = {
1273
+ retryLimit: input.runOptions?.retryLimit,
1274
+ retryForever: input.runOptions?.retryForever
1275
+ };
1235
1276
  const tasksPath = paths.tasksFile;
1236
1277
  const sessionId = randomUUID();
1237
1278
  let publishedIssueNumber = toIssueNumber(issueNumber);
@@ -1244,15 +1285,27 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
1244
1285
  issueUrl: info?.issueUrl,
1245
1286
  branch: info?.branch,
1246
1287
  phases: [...phases],
1247
- environment: { node: process.version, platform: process.platform }
1288
+ environment: {
1289
+ node: process.version,
1290
+ platform: process.platform,
1291
+ agent: agentSummary.defaultProvider,
1292
+ model: agentSummary.defaultModel
1293
+ }
1248
1294
  });
1249
1295
  };
1250
- printInfo(`Starting pipeline for issue #${issueNumber} (mode: ${mode})`);
1296
+ const agentSummary = await describeRunAgents(
1297
+ prReview ? ["prd", "plan", "execute", "review", "pr", "pr-review"] : ["prd", "plan", "execute", "review", "pr"]
1298
+ );
1299
+ printInfo(
1300
+ `Starting pipeline for issue #${issueNumber} (mode: ${mode}, agent: ${agentSummary.label})`
1301
+ );
1251
1302
  const issuesConfig = await loadIssuesConfig();
1252
1303
  const sessionStartedAt = isoNow();
1253
1304
  if (queue?.preChecked !== true) {
1254
- printInfo("Running prerequisite checks...");
1255
- const initCode = await runInit(issuesConfig.preferredProvider);
1305
+ if (isVerbose()) {
1306
+ printInfo("Running prerequisite checks...");
1307
+ }
1308
+ const initCode = await runInit(issuesConfig.preferredProvider, { compact: !isVerbose() });
1256
1309
  if (initCode !== 0) {
1257
1310
  publishSessionStart(PIPELINE_PHASES, sessionStartedAt);
1258
1311
  publisher.publish({ type: "phase:start", at: sessionStartedAt, phase: "init" });
@@ -1399,6 +1452,7 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
1399
1452
  }
1400
1453
  const startIdx = Math.max(phaseOrder.indexOf(startPhase), 0);
1401
1454
  const makeRunner = (fn, phase) => async () => {
1455
+ await ensureRepositoryWritable(phase);
1402
1456
  const code = await fn();
1403
1457
  if (code !== 0) {
1404
1458
  throw new Error(`Phase ${phase} failed with exit code ${code}`);
@@ -1411,25 +1465,28 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
1411
1465
  () => runPlan(issueNumber, resolvedIssue, { continueFlag: continueNumbering, startUs }),
1412
1466
  "plan"
1413
1467
  )();
1414
- if (effectiveNoBranch || effectivePrReview) {
1415
- try {
1416
- const plan = await loadTaskPlan(tasksPath);
1417
- if (effectiveNoBranch) {
1418
- plan.noBranch = true;
1419
- }
1468
+ try {
1469
+ const plan = await loadTaskPlan(tasksPath);
1470
+ publishStorySeed(publisher, plan.userStories, isoNow());
1471
+ if (effectiveNoBranch || effectivePrReview) {
1472
+ if (effectiveNoBranch) plan.noBranch = true;
1420
1473
  if (effectivePrReview) {
1421
1474
  plan.prReview = { ...plan.prReview, enabled: true, rounds: plan.prReview?.rounds ?? 0 };
1422
1475
  }
1423
1476
  await saveTaskPlan(tasksPath, plan);
1424
- } catch {
1425
1477
  }
1478
+ } catch {
1426
1479
  }
1427
1480
  if (queue !== void 0 && !effectiveNoBranch) {
1428
1481
  producedBranch = await adoptQueueBranch(tasksPath, queue.plan.branchName);
1429
1482
  }
1430
1483
  },
1431
1484
  execute: makeRunner(
1432
- () => runExecute(void 0, { issue: issueNumber, commitScope: queueCommitScope }),
1485
+ () => runExecute(void 0, {
1486
+ issue: issueNumber,
1487
+ commitScope: queueCommitScope,
1488
+ ...executeRetry
1489
+ }),
1433
1490
  "execute"
1434
1491
  ),
1435
1492
  review: async () => {
@@ -1453,7 +1510,8 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
1453
1510
  }
1454
1511
  const execCode = await runExecute(void 0, {
1455
1512
  issue: issueNumber,
1456
- commitScope: queueCommitScope
1513
+ commitScope: queueCommitScope,
1514
+ ...executeRetry
1457
1515
  });
1458
1516
  if (execCode !== 0) {
1459
1517
  throw new Error("Correction execution failed");
@@ -1489,27 +1547,34 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
1489
1547
  try {
1490
1548
  await fn();
1491
1549
  await publishGitState(publisher);
1492
- publisher.publish({ type: "phase:end", at: isoNow(), phase, success: true });
1550
+ await publishInstrumentedPhaseEnd(publisher, phase, issueNumber, true);
1493
1551
  } catch (err) {
1494
1552
  await publishGitState(publisher);
1495
- publisher.publish({
1496
- type: "phase:end",
1497
- at: isoNow(),
1553
+ await publishInstrumentedPhaseEnd(
1554
+ publisher,
1498
1555
  phase,
1499
- success: false,
1500
- error: err instanceof Error ? err.message : String(err)
1501
- });
1556
+ issueNumber,
1557
+ false,
1558
+ err instanceof Error ? err.message : String(err)
1559
+ );
1502
1560
  throw err;
1503
1561
  }
1504
1562
  }
1505
1563
  ])
1506
1564
  );
1565
+ const phaseSuffixes = {};
1566
+ for (const [phase, resolved] of Object.entries(agentSummary.byPhase)) {
1567
+ if (resolved.provider !== agentSummary.defaultProvider) {
1568
+ phaseSuffixes[phase] = resolved.model ? `${resolved.provider} \xB7 ${resolved.model}` : resolved.provider;
1569
+ }
1570
+ }
1507
1571
  const result = await runPipelineWithRenderer({
1508
1572
  phases: phaseOrder,
1509
1573
  startIndex: startIdx,
1510
1574
  verbose: isVerbose(),
1511
1575
  runners: instrumentedRunners,
1512
- tasksPath
1576
+ tasksPath,
1577
+ phaseSuffixes
1513
1578
  });
1514
1579
  if (!result.success) {
1515
1580
  printError(`Phase ${result.failedPhase} failed`);
@@ -1590,7 +1655,8 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
1590
1655
  prReview: review,
1591
1656
  // Process-owned counters: the session snapshot is empty whenever web
1592
1657
  // monitoring is off, so it cannot be the source of these totals.
1593
- usage: getRunUsageTotals()
1658
+ usage: getRunUsageTotals(),
1659
+ verification: verificationForSummary(getSessionPublisher().snapshot().verification)
1594
1660
  });
1595
1661
  return {
1596
1662
  code: 0,
@@ -1600,6 +1666,37 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
1600
1666
  elapsedSeconds: result.overallElapsedSeconds
1601
1667
  };
1602
1668
  }
1669
+ async function detachAfterConfirm(requested, noBranch, prReview, options) {
1670
+ const childFlags = [...process.argv.slice(2)];
1671
+ if (options.yes !== true && options.only !== true) {
1672
+ try {
1673
+ const issuesConfig = await loadIssuesConfig();
1674
+ const resolution = await resolveCommandIssue(requested[0], void 0, {
1675
+ config: issuesConfig
1676
+ });
1677
+ if (resolution.ok) {
1678
+ const decision = await decideQueue({
1679
+ requested,
1680
+ resolved: resolution.resolved,
1681
+ noBranch: noBranch ?? false,
1682
+ prReview: prReview ?? false,
1683
+ runOptions: options
1684
+ });
1685
+ if (decision.kind === "stop") return decision.code;
1686
+ if (decision.kind === "queue" && !childFlags.includes("--yes")) childFlags.push("--yes");
1687
+ if (decision.kind === "single" && !childFlags.includes("--only")) childFlags.push("--only");
1688
+ }
1689
+ } catch {
1690
+ }
1691
+ }
1692
+ const paths = await resolveIssuePaths(requested[0]);
1693
+ const { pid, logFile } = await spawnDetachedRun({ paths, argv: childFlags });
1694
+ printSuccess(`Detached run started (pid ${pid}).`);
1695
+ printInfo(`Log: ${logFile}`);
1696
+ printInfo("Follow with: issue-flow ps");
1697
+ printInfo(`Stop with: kill ${pid}`);
1698
+ return 0;
1699
+ }
1603
1700
  async function decideQueue(input) {
1604
1701
  const primary = input.requested[0];
1605
1702
  const single = input.requested.length === 1;
@@ -1619,7 +1716,11 @@ async function decideQueue(input) {
1619
1716
  planFile: queuePaths.planFile,
1620
1717
  noBranch: input.noBranch,
1621
1718
  prReview: input.prReview,
1622
- confirm: { yes: input.runOptions.yes, only: input.runOptions.only }
1719
+ confirm: {
1720
+ yes: input.runOptions.yes,
1721
+ only: input.runOptions.only,
1722
+ cascade: input.runOptions.cascade
1723
+ }
1623
1724
  });
1624
1725
  } catch (err) {
1625
1726
  const message = err instanceof Error ? err.message : String(err);
@@ -1640,9 +1741,14 @@ async function runQueue(initialPlan, options, resolvedPrimary) {
1640
1741
  const startedAtMs = Date.now();
1641
1742
  const summaries = [];
1642
1743
  let firstOfThisRun = true;
1744
+ const resilience = getActiveResilienceConfig();
1745
+ const failureMode = options.runOptions?.onIssueFailure ?? resilience.queue?.onIssueFailure ?? "stop";
1746
+ const maxIssueAttempts = resilience.queue?.maxIssueAttempts ?? DEFAULT_MAX_ISSUE_ATTEMPTS;
1747
+ const exhausted = /* @__PURE__ */ new Set();
1748
+ const failedIds = /* @__PURE__ */ new Set();
1643
1749
  try {
1644
1750
  while (true) {
1645
- const entry = nextQueueIssue(plan);
1751
+ const entry = nextQueueIssue(plan, { exclude: exhausted });
1646
1752
  if (entry === null) break;
1647
1753
  plan = markQueueIssueInProgress(plan, entry.id);
1648
1754
  await saveExecutionPlan(planFile, plan);
@@ -1675,20 +1781,55 @@ async function runQueue(initialPlan, options, resolvedPrimary) {
1675
1781
  }
1676
1782
  const usage = issueUsage.totals();
1677
1783
  if (result.code !== 0) {
1678
- plan = markQueueIssueFailed(plan, entry.id, {
1784
+ const failure2 = {
1679
1785
  phase: result.failedPhase,
1680
1786
  error: {
1681
1787
  category: "queue_issue_failed",
1682
1788
  message: `Issue #${entry.id} failed${result.failedPhase === null ? "" : ` in phase ${result.failedPhase}`}`,
1683
1789
  at: isoNow()
1684
1790
  }
1685
- });
1791
+ };
1792
+ const where = result.failedPhase === null ? "" : ` (phase ${result.failedPhase})`;
1793
+ if (failureMode === "stop") {
1794
+ plan = markQueueIssueFailed(plan, entry.id, failure2);
1795
+ await saveExecutionPlan(planFile, plan);
1796
+ printError(
1797
+ `Queue stopped at issue #${entry.id}${where}. The branch and every commit made so far were kept.`
1798
+ );
1799
+ printInfo(`Resume with: issue-flow run ${plan.requested.join(",")}`);
1800
+ return result.code;
1801
+ }
1802
+ if (failureMode === "block") {
1803
+ plan = markQueueIssueBlocked(
1804
+ plan,
1805
+ entry.id,
1806
+ `Failed${where} and --on-issue-failure block was in force`,
1807
+ failure2
1808
+ );
1809
+ await saveExecutionPlan(planFile, plan);
1810
+ exhausted.add(entry.id);
1811
+ failedIds.add(entry.id);
1812
+ printWarning(
1813
+ `Issue #${entry.id} failed${where} and is blocked for review. Continuing with the rest of the queue.`
1814
+ );
1815
+ continue;
1816
+ }
1817
+ const attempts = entry.attempts + 1;
1818
+ if (attempts >= maxIssueAttempts) {
1819
+ plan = markQueueIssueFailed(plan, entry.id, failure2);
1820
+ exhausted.add(entry.id);
1821
+ failedIds.add(entry.id);
1822
+ printWarning(
1823
+ `Issue #${entry.id} failed${where} after ${attempts} attempt(s). Continuing with the rest of the queue.`
1824
+ );
1825
+ } else {
1826
+ plan = markQueueIssueSkipped(plan, entry.id, failure2);
1827
+ printWarning(
1828
+ `Issue #${entry.id} failed${where}. Skipping it for now and coming back at the end of the queue.`
1829
+ );
1830
+ }
1686
1831
  await saveExecutionPlan(planFile, plan);
1687
- printError(
1688
- `Queue stopped at issue #${entry.id}${result.failedPhase === null ? "" : ` (phase ${result.failedPhase})`}. The branch and every commit made so far were kept.`
1689
- );
1690
- printInfo(`Resume with: issue-flow run ${plan.requested.join(",")}`);
1691
- return result.code;
1832
+ continue;
1692
1833
  }
1693
1834
  if (plan.branchName === null && result.branchName !== null) {
1694
1835
  plan = setQueueBranch(plan, result.branchName);
@@ -1703,7 +1844,7 @@ async function runQueue(initialPlan, options, resolvedPrimary) {
1703
1844
  usage
1704
1845
  });
1705
1846
  }
1706
- return await finishQueue(
1847
+ const code = await finishQueue(
1707
1848
  plan,
1708
1849
  planFile,
1709
1850
  summaries,
@@ -1712,6 +1853,14 @@ async function runQueue(initialPlan, options, resolvedPrimary) {
1712
1853
  options,
1713
1854
  resolvedPrimary
1714
1855
  );
1856
+ if (failedIds.size > 0) {
1857
+ printError(
1858
+ `${failedIds.size} issue(s) did not finish: ${[...failedIds].map((id) => `#${id}`).join(", ")}.`
1859
+ );
1860
+ printInfo(`Resume with: issue-flow run ${plan.requested.join(",")}`);
1861
+ return code === 0 ? 1 : code;
1862
+ }
1863
+ return code;
1715
1864
  } finally {
1716
1865
  queueUsage.end();
1717
1866
  }
@@ -1790,7 +1939,10 @@ async function buildPrQueueContext(plan) {
1790
1939
  number: entry.number,
1791
1940
  title: entry.title,
1792
1941
  url: entry.url,
1793
- source: entry.source
1942
+ source: entry.source,
1943
+ parent: entry.parent,
1944
+ role: entry.role,
1945
+ complete: entry.status === "completed"
1794
1946
  })),
1795
1947
  excluded: plan.excluded.map((entry) => ({
1796
1948
  id: entry.id,
@@ -1853,9 +2005,10 @@ async function adoptQueueBranch(tasksPath, queueBranch) {
1853
2005
  return queueBranch;
1854
2006
  }
1855
2007
  }
2008
+
1856
2009
  export {
1857
- publishIssueDetails,
1858
2010
  publishStorySeed,
2011
+ publishIssueDetails,
1859
2012
  runPipeline
1860
2013
  };
1861
- //# sourceMappingURL=run-DBF2VWUN.js.map
2014
+ //# sourceMappingURL=chunk-OPNGPUKJ.js.map