issue-flow 0.12.0 → 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 (185) 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-OEET7KGE.js → analyze-WS47UYGN.js} +28 -16
  5. package/dist/analyze-WS47UYGN.js.map +1 -0
  6. package/dist/{apply-ZIA3C52S.js → apply-OS62UWRN.js} +15 -7
  7. package/dist/{apply-ZIA3C52S.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-MJULIKQU.js → chunk-664Y3NDX.js} +23 -47
  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-56KS3TFQ.js → chunk-7KD66LIM.js} +115 -160
  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-NS6R37II.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-M5UCXTIT.js → chunk-HJGGNLWI.js} +22 -18
  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-FJP3UQGD.js → chunk-IHIEXUTC.js} +39 -19
  45. package/dist/chunk-IHIEXUTC.js.map +1 -0
  46. package/dist/{chunk-Y5ZDJ4PJ.js → chunk-IV5SS4JN.js} +18 -10
  47. package/dist/chunk-IV5SS4JN.js.map +1 -0
  48. package/dist/{chunk-BYZDPR7N.js → chunk-IYV6KNVF.js} +146 -13
  49. package/dist/chunk-IYV6KNVF.js.map +1 -0
  50. package/dist/{chunk-TSOLWK7Y.js → chunk-J62QSVP6.js} +8 -8
  51. package/dist/chunk-KQHB7ZT3.js +559 -0
  52. package/dist/chunk-KQHB7ZT3.js.map +1 -0
  53. package/dist/chunk-L6HSKTQE.js +77 -0
  54. package/dist/chunk-L6HSKTQE.js.map +1 -0
  55. package/dist/{chunk-3QHK4YQA.js → chunk-LZ6GFQAG.js} +334 -117
  56. package/dist/chunk-LZ6GFQAG.js.map +1 -0
  57. package/dist/{chunk-LAFNLLQP.js → chunk-MIVHK5YI.js} +5 -5
  58. package/dist/chunk-NEZZ6KDR.js +1217 -0
  59. package/dist/chunk-NEZZ6KDR.js.map +1 -0
  60. package/dist/{chunk-NTMMF5UD.js → chunk-OPNGPUKJ.js} +436 -254
  61. package/dist/chunk-OPNGPUKJ.js.map +1 -0
  62. package/dist/chunk-P3LRUNC6.js +16 -0
  63. package/dist/chunk-P3LRUNC6.js.map +1 -0
  64. package/dist/{chunk-FADTCQGM.js → chunk-PIVGJM6K.js} +94 -32
  65. package/dist/chunk-PIVGJM6K.js.map +1 -0
  66. package/dist/{chunk-73ATPIYX.js → chunk-PV4OX5TC.js} +36 -71
  67. package/dist/chunk-PV4OX5TC.js.map +1 -0
  68. package/dist/chunk-QCXPNSEL.js +141 -0
  69. package/dist/chunk-QCXPNSEL.js.map +1 -0
  70. package/dist/{chunk-XQUSIEKA.js → chunk-QXDNBYQF.js} +52 -14
  71. package/dist/chunk-QXDNBYQF.js.map +1 -0
  72. package/dist/chunk-SNTTOQAD.js +30 -0
  73. package/dist/chunk-SNTTOQAD.js.map +1 -0
  74. package/dist/{chunk-QCPGHBSZ.js → chunk-TM67EHSV.js} +63 -21
  75. package/dist/chunk-TM67EHSV.js.map +1 -0
  76. package/dist/chunk-TVN2JVOF.js +77 -0
  77. package/dist/chunk-TVN2JVOF.js.map +1 -0
  78. package/dist/chunk-TX5WIVYP.js +129 -0
  79. package/dist/chunk-TX5WIVYP.js.map +1 -0
  80. package/dist/{chunk-A4SLHOW7.js → chunk-UJ7XWK7J.js} +24 -18
  81. package/dist/chunk-UJ7XWK7J.js.map +1 -0
  82. package/dist/{chunk-EZATO3DW.js → chunk-UOCPC2Y2.js} +594 -17
  83. package/dist/chunk-UOCPC2Y2.js.map +1 -0
  84. package/dist/chunk-XEYH6ZZI.js +289 -0
  85. package/dist/chunk-XEYH6ZZI.js.map +1 -0
  86. package/dist/cli.js +264 -44
  87. package/dist/cli.js.map +1 -1
  88. package/dist/{config-PRJGWW7J.js → config-MEM2IWWN.js} +28 -5
  89. package/dist/contract-JH2WBIO6.js +8 -0
  90. package/dist/conventions-WV4NDXGH.js +143 -0
  91. package/dist/conventions-WV4NDXGH.js.map +1 -0
  92. package/dist/execute-NITIHHCK.js +31 -0
  93. package/dist/{generate-EGB4KV2B.js → generate-MVQI7A45.js} +34 -20
  94. package/dist/{generate-EGB4KV2B.js.map → generate-MVQI7A45.js.map} +1 -1
  95. package/dist/git-YRVRUN3C.js +31 -0
  96. package/dist/init-OKWEC3EV.js +21 -0
  97. package/dist/{operations-BYT626KQ.js → operations-LU6JA3BX.js} +24 -15
  98. package/dist/{operations-BYT626KQ.js.map → operations-LU6JA3BX.js.map} +1 -1
  99. package/dist/permissions-7NMDFU27.js +18 -0
  100. package/dist/plan-LEJJF3DE.js +31 -0
  101. package/dist/{policy-6S4GFC76.js → policy-JSYEK5B2.js} +17 -6
  102. package/dist/policy-JSYEK5B2.js.map +1 -0
  103. package/dist/pr-HPVUZ7S3.js +36 -0
  104. package/dist/pr-review-R2QZBT3F.js +29 -0
  105. package/dist/prd-5O6GYCC6.js +30 -0
  106. package/dist/ps-TNE44R5V.js +90 -0
  107. package/dist/ps-TNE44R5V.js.map +1 -0
  108. package/dist/recorder-XGF55WXI.js +31 -0
  109. package/dist/recorder-XGF55WXI.js.map +1 -0
  110. package/dist/registry-LGPE53QD.js +15 -0
  111. package/dist/registry-LGPE53QD.js.map +1 -0
  112. package/dist/{resume-PPCNQVVV.js → resume-QMEPMSZW.js} +51 -34
  113. package/dist/{resume-PPCNQVVV.js.map → resume-QMEPMSZW.js.map} +1 -1
  114. package/dist/review-YKWD6SNN.js +32 -0
  115. package/dist/review-YKWD6SNN.js.map +1 -0
  116. package/dist/routing-2ZUMZPJB.js +91 -0
  117. package/dist/routing-2ZUMZPJB.js.map +1 -0
  118. package/dist/run-YU4TR62R.js +50 -0
  119. package/dist/run-YU4TR62R.js.map +1 -0
  120. package/dist/{state-manager-2EDVDBAR.js → state-manager-X3PT3FGT.js} +4 -2
  121. package/dist/state-manager-X3PT3FGT.js.map +1 -0
  122. package/dist/usage-VCFLDA5Y.js +174 -0
  123. package/dist/usage-VCFLDA5Y.js.map +1 -0
  124. package/dist/{web-EF5WIIGG.js → web-XVHLX62B.js} +41 -30
  125. package/dist/web-XVHLX62B.js.map +1 -0
  126. package/package.json +6 -2
  127. package/prompts/execute.md +7 -8
  128. package/prompts/plan.md +4 -5
  129. package/prompts/pr.md +2 -2
  130. package/web/AGENTS.md +3 -3
  131. package/web/public/app.css +38 -0
  132. package/web/public/app.js +121 -11
  133. package/web/public/index.html +24 -0
  134. package/dist/analyze-OEET7KGE.js.map +0 -1
  135. package/dist/chunk-3QHK4YQA.js.map +0 -1
  136. package/dist/chunk-42PJFMAR.js +0 -605
  137. package/dist/chunk-42PJFMAR.js.map +0 -1
  138. package/dist/chunk-4JSOYE4G.js +0 -368
  139. package/dist/chunk-4JSOYE4G.js.map +0 -1
  140. package/dist/chunk-56KS3TFQ.js.map +0 -1
  141. package/dist/chunk-73ATPIYX.js.map +0 -1
  142. package/dist/chunk-A4SLHOW7.js.map +0 -1
  143. package/dist/chunk-BAUPQXPE.js +0 -173
  144. package/dist/chunk-BAUPQXPE.js.map +0 -1
  145. package/dist/chunk-BYZDPR7N.js.map +0 -1
  146. package/dist/chunk-CD5ULRVC.js +0 -615
  147. package/dist/chunk-CD5ULRVC.js.map +0 -1
  148. package/dist/chunk-DIDUVL6O.js +0 -1365
  149. package/dist/chunk-DIDUVL6O.js.map +0 -1
  150. package/dist/chunk-EZATO3DW.js.map +0 -1
  151. package/dist/chunk-FADTCQGM.js.map +0 -1
  152. package/dist/chunk-FJP3UQGD.js.map +0 -1
  153. package/dist/chunk-LWRSMTTZ.js +0 -18
  154. package/dist/chunk-LWRSMTTZ.js.map +0 -1
  155. package/dist/chunk-M5UCXTIT.js.map +0 -1
  156. package/dist/chunk-M66SJHQQ.js +0 -206
  157. package/dist/chunk-M66SJHQQ.js.map +0 -1
  158. package/dist/chunk-MJULIKQU.js.map +0 -1
  159. package/dist/chunk-NS6R37II.js.map +0 -1
  160. package/dist/chunk-NTMMF5UD.js.map +0 -1
  161. package/dist/chunk-QCPGHBSZ.js.map +0 -1
  162. package/dist/chunk-XQUSIEKA.js.map +0 -1
  163. package/dist/chunk-Y5ZDJ4PJ.js.map +0 -1
  164. package/dist/execute-XBNVL753.js +0 -18
  165. package/dist/init-AJHCXJBP.js +0 -10
  166. package/dist/plan-QOZ3RIO6.js +0 -21
  167. package/dist/policy-6S4GFC76.js.map +0 -1
  168. package/dist/pr-EBBHD2ZX.js +0 -26
  169. package/dist/pr-review-VVMW5PO4.js +0 -20
  170. package/dist/prd-JQSZF7PR.js +0 -21
  171. package/dist/review-3F6MARWH.js +0 -19
  172. package/dist/run-RXE5GDQO.js +0 -36
  173. package/dist/web-EF5WIIGG.js.map +0 -1
  174. /package/dist/{chunk-TSOLWK7Y.js.map → chunk-J62QSVP6.js.map} +0 -0
  175. /package/dist/{chunk-LAFNLLQP.js.map → chunk-MIVHK5YI.js.map} +0 -0
  176. /package/dist/{config-PRJGWW7J.js.map → config-MEM2IWWN.js.map} +0 -0
  177. /package/dist/{execute-XBNVL753.js.map → contract-JH2WBIO6.js.map} +0 -0
  178. /package/dist/{init-AJHCXJBP.js.map → execute-NITIHHCK.js.map} +0 -0
  179. /package/dist/{plan-QOZ3RIO6.js.map → git-YRVRUN3C.js.map} +0 -0
  180. /package/dist/{pr-EBBHD2ZX.js.map → init-OKWEC3EV.js.map} +0 -0
  181. /package/dist/{pr-review-VVMW5PO4.js.map → permissions-7NMDFU27.js.map} +0 -0
  182. /package/dist/{prd-JQSZF7PR.js.map → plan-LEJJF3DE.js.map} +0 -0
  183. /package/dist/{review-3F6MARWH.js.map → pr-HPVUZ7S3.js.map} +0 -0
  184. /package/dist/{run-RXE5GDQO.js.map → pr-review-R2QZBT3F.js.map} +0 -0
  185. /package/dist/{state-manager-2EDVDBAR.js.map → prd-5O6GYCC6.js.map} +0 -0
@@ -1,31 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- ensureWebMonitor
4
- } from "./chunk-FJP3UQGD.js";
5
- import {
6
- runPr
7
- } from "./chunk-FADTCQGM.js";
8
- import {
9
- mostRecent,
10
- prReviewDir,
11
- readPrReviewIndex,
12
- runPrReview
13
- } from "./chunk-A4SLHOW7.js";
14
- import {
15
- runPrd
16
- } from "./chunk-M5UCXTIT.js";
3
+ formatPhaseLine,
4
+ loadPhaseTiming,
5
+ snapshotTimingFields
6
+ } from "./chunk-37N2RJW2.js";
17
7
  import {
18
8
  runReview
19
- } from "./chunk-XQUSIEKA.js";
9
+ } from "./chunk-QXDNBYQF.js";
20
10
  import {
21
- JournalPublisher,
22
- MultiPublisher,
23
11
  PIPELINE_PHASES,
24
12
  PIPELINE_PHASES_NO_BRANCH,
25
13
  PIPELINE_PHASES_WITH_PR_REVIEW,
26
14
  PipelineManager,
27
15
  buildDependencyGraph,
28
16
  buildExecutionPlan,
17
+ collectCascadeIds,
29
18
  computeExecutionOrder,
30
19
  describeCycles,
31
20
  isQueueComplete,
@@ -36,250 +25,184 @@ import {
36
25
  markQueueIssueInProgress,
37
26
  markQueueIssueSkipped,
38
27
  nextQueueIssue,
39
- parseJournal,
40
28
  saveExecutionPlan,
41
29
  setQueueBranch,
42
30
  setQueuePullRequest
43
- } from "./chunk-56KS3TFQ.js";
44
- import {
45
- acquireRunLock,
46
- describeRunLockOwner
47
- } from "./chunk-BAUPQXPE.js";
31
+ } from "./chunk-7KD66LIM.js";
48
32
  import {
49
33
  runInit
50
- } from "./chunk-NS6R37II.js";
34
+ } from "./chunk-BHJCGWQX.js";
51
35
  import {
52
36
  printQueueSummary,
53
37
  printRunSummary,
54
- runExecute
55
- } from "./chunk-3QHK4YQA.js";
38
+ runExecute,
39
+ stripMarkdown
40
+ } from "./chunk-LZ6GFQAG.js";
41
+ import {
42
+ ensureWebMonitor
43
+ } from "./chunk-IHIEXUTC.js";
44
+ import {
45
+ runPlan
46
+ } from "./chunk-TM67EHSV.js";
47
+ import {
48
+ runPr
49
+ } from "./chunk-PIVGJM6K.js";
50
+ import {
51
+ mostRecent,
52
+ prReviewDir,
53
+ readPrReviewIndex,
54
+ runPrReview
55
+ } from "./chunk-UJ7XWK7J.js";
56
56
  import {
57
57
  listPullRequests,
58
58
  publishGitState
59
- } from "./chunk-TSOLWK7Y.js";
59
+ } from "./chunk-J62QSVP6.js";
60
60
  import {
61
- runPlan
62
- } from "./chunk-QCPGHBSZ.js";
61
+ runPrd
62
+ } from "./chunk-HJGGNLWI.js";
63
63
  import {
64
- getProvider,
65
64
  resolveCommandIssue
66
- } from "./chunk-73ATPIYX.js";
65
+ } from "./chunk-FZDR7CGH.js";
66
+ import {
67
+ getProvider
68
+ } from "./chunk-PV4OX5TC.js";
67
69
  import {
68
70
  beginUsageScope,
69
71
  getRunUsageTotals
70
- } from "./chunk-MJULIKQU.js";
72
+ } from "./chunk-664Y3NDX.js";
71
73
  import {
72
- onShutdown
73
- } from "./chunk-M66SJHQQ.js";
74
+ assessDecomposition,
75
+ buildDecompositionReport,
76
+ proposeSubIssues,
77
+ resetAgentInvocationState
78
+ } from "./chunk-ARFVHC2F.js";
74
79
  import {
80
+ JournalPublisher,
81
+ MultiPublisher
82
+ } from "./chunk-QCXPNSEL.js";
83
+ import {
84
+ describeRunAgents,
75
85
  getActiveResilienceConfig,
76
86
  initResilienceConfig,
77
87
  loadIssuesConfig,
78
- loadWebConfig
79
- } from "./chunk-42PJFMAR.js";
88
+ loadWebConfig,
89
+ onShutdown
90
+ } from "./chunk-CNBYCMSU.js";
80
91
  import {
81
- committedStoryIds,
82
- describePreflight,
83
- getBaseBranch,
84
- preflightRepository,
85
92
  resolveIssuePaths,
86
93
  resolveProjectPaths,
87
- resolveQueuePaths,
88
- run
89
- } from "./chunk-DIDUVL6O.js";
94
+ resolveQueuePaths
95
+ } from "./chunk-HQAHNIAR.js";
90
96
  import {
91
97
  FilePublisher,
92
- NullPublisher,
98
+ MemoryPublisher,
99
+ formatDuration,
100
+ getIcons,
93
101
  getInactivityTimeout,
102
+ getSessionPublisher,
94
103
  isVerbose,
95
104
  printError,
96
105
  printInfo,
97
106
  printSuccess,
98
107
  printWarning,
108
+ setActivityCallback,
99
109
  setInactivityTimeout,
100
110
  setOutputCallback,
101
111
  setSessionPublisher,
102
112
  setStoryStageCallback,
103
113
  setStoryUpdateCallback
104
- } from "./chunk-BYZDPR7N.js";
114
+ } from "./chunk-IYV6KNVF.js";
105
115
  import {
106
116
  isoNow,
107
117
  loadTaskPlan,
108
118
  saveTaskPlan
109
- } from "./chunk-CD5ULRVC.js";
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";
110
131
 
111
132
  // src/commands/run.ts
112
133
  import { randomUUID } from "crypto";
113
- import { mkdir, readFile, writeFile } from "fs/promises";
134
+ import { mkdir as mkdir2, readFile, writeFile } from "fs/promises";
114
135
  import { join } from "path";
115
136
  import { execa } from "execa";
116
137
 
117
- // src/core/decompose.ts
118
- var DECOMPOSITION_THRESHOLDS = {
119
- /** Timeouts on the same phase. Two is already a pattern, not bad luck. */
120
- timeoutsPerPhase: 2,
121
- /** Stories in the plan. Past this, the plan is a backlog. */
122
- stories: 15,
123
- /** Iterations in a row that completed no story. */
124
- barrenIterations: 5,
125
- /** Files touched on the branch. */
126
- filesTouched: 40,
127
- /** Characters in the issue body. */
128
- issueBodyChars: 2e4
129
- };
130
- var DECOMPOSITION_MIN_SIGNALS = 2;
131
- function timeoutsByPhase(journal) {
132
- const counts = /* @__PURE__ */ new Map();
133
- let phase = "(unknown)";
134
- for (const { event } of parseJournal(journal)) {
135
- if (event.type === "phase:start") {
136
- phase = event.phase;
137
- continue;
138
- }
139
- if (event.type !== "retry") continue;
140
- const kind = event.kind;
141
- const reason = event.reason ?? "";
142
- const timedOut = kind === "timeout" || kind === "stalled" || /timed out|stalled/i.test(reason);
143
- if (!timedOut) continue;
144
- counts.set(phase, (counts.get(phase) ?? 0) + 1);
145
- }
146
- return counts;
147
- }
148
- function longestBarrenRun(journal) {
149
- let passing = 0;
150
- let current = 0;
151
- let longest = 0;
152
- for (const { event } of parseJournal(journal)) {
153
- if (event.type === "iteration:start") {
154
- current++;
155
- longest = Math.max(longest, current);
156
- continue;
157
- }
158
- if (event.type !== "stories:update") continue;
159
- const nowPassing = event.stories.filter((story) => story.passes).length;
160
- if (nowPassing > passing) current = 0;
161
- passing = Math.max(passing, nowPassing);
162
- }
163
- return longest;
164
- }
165
- function assessDecomposition(input) {
166
- const signals = [];
167
- const journal = input.journal ?? "";
168
- for (const [phase, count] of timeoutsByPhase(journal)) {
169
- if (count >= DECOMPOSITION_THRESHOLDS.timeoutsPerPhase) {
170
- signals.push({
171
- id: "repeated-timeouts",
172
- detail: `The '${phase}' phase timed out ${count} times (threshold: ${DECOMPOSITION_THRESHOLDS.timeoutsPerPhase}).`
173
- });
174
- }
175
- }
176
- const stories = input.plan?.userStories.length ?? 0;
177
- if (stories > DECOMPOSITION_THRESHOLDS.stories) {
178
- signals.push({
179
- id: "story-count",
180
- detail: `The plan has ${stories} user stories (threshold: ${DECOMPOSITION_THRESHOLDS.stories}).`
181
- });
182
- }
183
- const barren = longestBarrenRun(journal);
184
- if (barren >= DECOMPOSITION_THRESHOLDS.barrenIterations) {
185
- signals.push({
186
- id: "barren-iterations",
187
- detail: `${barren} iterations in a row completed no story (threshold: ${DECOMPOSITION_THRESHOLDS.barrenIterations}).`
188
- });
138
+ // src/execution/detach.ts
139
+ import { spawn } from "child_process";
140
+ import { mkdir, open } from "fs/promises";
141
+ import { dirname } from "path";
142
+
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;
189
153
  }
190
- const files = input.filesTouched ?? 0;
191
- if (files > DECOMPOSITION_THRESHOLDS.filesTouched) {
192
- signals.push({
193
- id: "files-touched",
194
- detail: `${files} files were touched on this branch (threshold: ${DECOMPOSITION_THRESHOLDS.filesTouched}).`
195
- });
154
+ try {
155
+ await rename(filePath, rotatedPath);
156
+ } catch {
196
157
  }
197
- const body = input.issueBody?.length ?? 0;
198
- if (body > DECOMPOSITION_THRESHOLDS.issueBodyChars) {
199
- signals.push({
200
- id: "issue-size",
201
- detail: `The issue body is ${body} characters (threshold: ${DECOMPOSITION_THRESHOLDS.issueBodyChars}).`
202
- });
158
+ }
159
+
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.";
203
164
  }
204
- if (input.hitMaxIterations === true) {
205
- signals.push({
206
- id: "max-iterations",
207
- detail: "The execute loop stopped because it ran out of iterations, not because it finished."
208
- });
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.";
209
169
  }
210
- return { signals, oversized: signals.length >= DECOMPOSITION_MIN_SIGNALS };
170
+ return null;
211
171
  }
212
- var STORIES_PER_SUBISSUE = 5;
213
- function proposeSubIssues(plan) {
214
- const pending = [...plan?.userStories ?? []].filter((story) => !story.passes).sort((a, b) => a.priority - b.priority);
215
- if (pending.length === 0) return [];
216
- const chunks = [];
217
- for (let index = 0; index < pending.length; index += STORIES_PER_SUBISSUE) {
218
- const stories = pending.slice(index, index + STORIES_PER_SUBISSUE);
219
- const first = stories[0];
220
- const last = stories[stories.length - 1];
221
- const range = first === void 0 || last === void 0 || first.id === last.id ? first?.id ?? "" : `${first.id}\u2013${last.id}`;
222
- chunks.push({
223
- title: `${plan?.description ?? "Issue"} \u2014 part ${chunks.length + 1} (${range})`,
224
- stories,
225
- dependsOn: chunks.length === 0 ? [] : [chunks[chunks.length - 1]?.title ?? ""]
226
- });
227
- }
228
- return chunks;
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;
229
180
  }
230
- function buildDecompositionReport(input) {
231
- const { issueNumber, assessment, plan, at } = input;
232
- const proposals = proposeSubIssues(plan);
233
- const lines = [];
234
- lines.push(`# Issue #${issueNumber} looks larger than one run`, "");
235
- lines.push(`Detected at ${at}.`, "");
236
- lines.push(
237
- "This is a report, not an action. Nothing has been split, and nothing will be",
238
- "without you asking for it.",
239
- ""
240
- );
241
- lines.push("## What was detected", "");
242
- for (const signal of assessment.signals) {
243
- lines.push(`- **${signal.id}** \u2014 ${signal.detail}`);
244
- }
245
- lines.push("");
246
- if (proposals.length === 0) {
247
- lines.push("## Proposed split", "");
248
- lines.push(
249
- "No pending user stories were found, so there is nothing to cut here.",
250
- "The signals above are about the run rather than about the plan.",
251
- ""
252
- );
253
- return `${lines.join("\n")}
254
- `;
255
- }
256
- lines.push("## Proposed split", "");
257
- lines.push(
258
- `The ${proposals.reduce((total, proposal) => total + proposal.stories.length, 0)} pending stories, in priority order, cut into ${proposals.length} sub-issues of at most ${STORIES_PER_SUBISSUE}. Each depends on the one before it, which is the only`,
259
- "dependency shape that can be derived from the plan alone \u2014 read it as a starting",
260
- "point, not as an answer.",
261
- ""
262
- );
263
- for (const proposal of proposals) {
264
- lines.push(`### ${proposal.title}`, "");
265
- if (proposal.dependsOn.length > 0) {
266
- lines.push(`Depends on: ${proposal.dependsOn.join(", ")}`, "");
267
- }
268
- for (const story of proposal.stories) {
269
- lines.push(`- \`${story.id}\` ${story.title}`);
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");
189
+ try {
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.");
270
200
  }
271
- lines.push("");
201
+ child.unref();
202
+ return { pid: child.pid, logFile: input.paths.runLogFile };
203
+ } finally {
204
+ await log.close();
272
205
  }
273
- lines.push("## What to do next", "");
274
- lines.push(
275
- "- Adjust the cut above until it matches how you would actually split the work;",
276
- '- create the sub-issues (`issue-flow generate "\u2026"`) and run them as a queue;',
277
- "- or decide the issue is fine as it is and re-run it \u2014 nothing here blocks that,",
278
- " beyond the report you are reading.",
279
- ""
280
- );
281
- return `${lines.join("\n")}
282
- `;
283
206
  }
284
207
 
285
208
  // src/execution/confirm.ts
@@ -321,16 +244,25 @@ function buildQueueSummaryLines(plan) {
321
244
  const primary = plan.issues.find((entry) => entry.id === plan.id) ?? plan.issues[0];
322
245
  const lines = [];
323
246
  if (primary !== void 0) {
324
- 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
+ );
325
251
  }
326
- 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
+ );
327
257
  lines.push(" Suggested order:");
328
258
  for (const entry of plan.issues) {
329
259
  const reason = reasonFor(entry);
330
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)" : "";
331
263
  const title = entry.title === "" ? "" : ` ${entry.title}`;
332
264
  lines.push(
333
- ` ${entry.position}. ${issueLabel(entry)}${title}${reason === "" ? "" : ` (${reason})`}${flag}`
265
+ ` ${entry.position}. ${issueLabel(entry)}${title}${role}${reason === "" ? "" : ` (${reason})`}${flag}${external}`
334
266
  );
335
267
  }
336
268
  if (plan.issues.some((entry) => entry.heuristic)) {
@@ -341,6 +273,23 @@ function buildQueueSummaryLines(plan) {
341
273
  }
342
274
  return lines;
343
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
+ }
344
293
  async function confirmQueue(plan, requestedOnlyCount, options = {}) {
345
294
  const info = options.info ?? printInfo;
346
295
  const warn = options.warn ?? printWarning;
@@ -349,19 +298,41 @@ async function confirmQueue(plan, requestedOnlyCount, options = {}) {
349
298
  (options.stdout ?? process.stdout).write(`${line}
350
299
  `);
351
300
  }
301
+ const container = requestedIsContainer(plan);
352
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
+ }
353
308
  info(`--only: running just the ${requestedOnlyCount} issue(s) you asked for.`);
309
+ info(buildScopeSummaryLine(plan, "requested") ?? "");
354
310
  return "requested";
355
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
+ }
356
319
  if (options.yes === true) {
357
320
  info(`--yes: running the whole hierarchy (${plan.issues.length} issues).`);
321
+ info(buildScopeSummaryLine(plan, "all") ?? "");
358
322
  return "all";
359
323
  }
360
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
+ }
361
330
  if (!interactive && options.singleRequest === true) {
362
331
  warn(
363
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.`
364
333
  );
334
+ const summary2 = buildScopeSummaryLine(plan, "requested");
335
+ if (summary2) info(summary2);
365
336
  return "requested";
366
337
  }
367
338
  if (!interactive) {
@@ -369,10 +340,19 @@ async function confirmQueue(plan, requestedOnlyCount, options = {}) {
369
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."
370
341
  );
371
342
  }
372
- const query = `Which scope should run? [1] Only the issues informed (${requestedOnlyCount}) [2] The whole hierarchy (${plan.issues.length}) [3] Cancel: `;
373
- 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;
374
354
  }
375
- async function ask(query, stdin, stdout, warn) {
355
+ async function ask(query, stdin, stdout, warn, mode = "plain") {
376
356
  const rl = createInterface({ input: stdin, output: stdout });
377
357
  const queued = [];
378
358
  let pending = null;
@@ -414,6 +394,14 @@ async function ask(query, stdin, stdout, warn) {
414
394
  return "cancel";
415
395
  }
416
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
+ }
417
405
  if (choice === "1") return "requested";
418
406
  if (choice === "" || choice === "2") return "all";
419
407
  if (choice === "3") return "cancel";
@@ -456,6 +444,14 @@ async function discoverIssueGraph(roots, options = {}) {
456
444
  }
457
445
 
458
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
+ }
459
455
  async function loadResumableQueue(planFile, requested) {
460
456
  let plan;
461
457
  try {
@@ -540,7 +536,30 @@ async function planQueue(input) {
540
536
  return { kind: "stop", code: 1 };
541
537
  }
542
538
  let plan = suggestedPlan;
543
- 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") {
544
563
  if (!requestedOrder.ok) {
545
564
  warn(
546
565
  `Dependency cycle between the issues you informed: ${describeCycles(requestedOrder.cycles)}.`
@@ -557,14 +576,16 @@ async function planQueue(input) {
557
576
  }
558
577
  return { kind: "single" };
559
578
  }
560
- plan = buildExecutionPlan({
561
- projectId: input.projectId,
562
- requested: input.requested,
563
- graph,
564
- order: requestedOrder.order,
565
- noBranch: input.noBranch,
566
- prReview: input.prReview
567
- });
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
+ );
568
589
  }
569
590
  await saveExecutionPlan(input.planFile, plan);
570
591
  return { kind: "queue", plan, resumed: false };
@@ -608,6 +629,34 @@ function parseIssueArguments(values) {
608
629
 
609
630
  // src/ui/pipeline-renderer.ts
610
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
611
660
  var PHASE_LABELS = {
612
661
  prd: "PRD",
613
662
  plan: "Plan",
@@ -656,6 +705,13 @@ function createActiveStoryTracker(stories, storyTasks) {
656
705
  }
657
706
  };
658
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
+ }
659
715
  function buildExecutePhaseTask(runner, tasksPath, verbose) {
660
716
  return async (_ctx, task) => {
661
717
  let stories;
@@ -676,6 +732,31 @@ function buildExecutePhaseTask(runner, tasksPath, verbose) {
676
732
  const totalStories = stories.length;
677
733
  const initialPassed = stories.filter((s) => s.passes).length;
678
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
+ }
679
760
  const resolvers = /* @__PURE__ */ new Map();
680
761
  setStoryUpdateCallback((updatedStories) => {
681
762
  const passed = updatedStories.filter((s) => s.passes).length;
@@ -742,14 +823,14 @@ function buildExecutePhaseTask(runner, tasksPath, verbose) {
742
823
  };
743
824
  }
744
825
  async function runPipelineWithRenderer(options) {
745
- const { phases, startIndex, verbose, runners, tasksPath } = options;
826
+ const { phases, startIndex, verbose, runners, tasksPath, phaseSuffixes } = options;
746
827
  const overallStart = Date.now();
747
828
  let currentPhase;
748
829
  const renderer = selectRenderer(verbose);
749
830
  const isSimple = renderer === "simple";
750
831
  const tasks = new Listr(
751
832
  phases.map((phase, index) => {
752
- const label = phaseLabel(phase);
833
+ const label = phaseSuffixes?.[phase] ? `${phaseLabel(phase)} (${phaseSuffixes[phase]})` : phaseLabel(phase);
753
834
  const isExecutePhase = phase === "execute" && tasksPath;
754
835
  return {
755
836
  title: label,
@@ -802,6 +883,7 @@ async function runPipelineWithRenderer(options) {
802
883
  setOutputCallback(void 0);
803
884
  setStoryUpdateCallback(void 0);
804
885
  setStoryStageCallback(void 0);
886
+ setActivityCallback(void 0);
805
887
  }
806
888
  const overallElapsedSeconds = Math.floor((Date.now() - overallStart) / 1e3);
807
889
  return {
@@ -816,6 +898,35 @@ function toIssueNumber(id) {
816
898
  const parsed = Number.parseInt(id, 10);
817
899
  return Number.isNaN(parsed) ? null : parsed;
818
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
+ }
819
930
  function publishStorySeed(publisher, stories, at) {
820
931
  if (stories.length === 0) return false;
821
932
  publisher.publish({ type: "stories:update", at, stories: [...stories] });
@@ -846,6 +957,10 @@ var QUEUE_PR_PHASES_WITH_REVIEW = [
846
957
  ];
847
958
  var RUNNABLE_QUEUE_PR_PHASES = ["pr"];
848
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
+ }
849
964
  async function readPrReviewOutcome(issue, tasksPath, requestedChanges) {
850
965
  const outcome = { requestedChanges, recommendation: null, reportPath: null };
851
966
  try {
@@ -877,7 +992,15 @@ async function runPipeline(issue, mode, from, noBranch, prReview, options = {})
877
992
  printError(err instanceof Error ? err.message : String(err));
878
993
  return 1;
879
994
  }
880
- const ownership = await claimRunOwnership(requested[0]);
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);
1002
+ }
1003
+ const ownership = await claimRunOwnership(requested[0], options.detachedChild === true);
881
1004
  if (!ownership.ok) {
882
1005
  printError(
883
1006
  `Another issue-flow run owns this project: ${describeRunLockOwner(ownership.owner)}.`
@@ -916,7 +1039,7 @@ async function ensureRepositoryWritable(phase) {
916
1039
  }
917
1040
  throw new Error(`The repository is not in a state the ${phase} phase can write to`);
918
1041
  }
919
- async function claimRunOwnership(target) {
1042
+ async function claimRunOwnership(target, detached = false) {
920
1043
  let lockFile;
921
1044
  try {
922
1045
  lockFile = (await resolveProjectPaths()).runLockFile;
@@ -924,7 +1047,7 @@ async function claimRunOwnership(target) {
924
1047
  return { ok: true, interruptedBy: null, release: async () => {
925
1048
  } };
926
1049
  }
927
- const result = await acquireRunLock(lockFile, { target });
1050
+ const result = await acquireRunLock(lockFile, { target, detached });
928
1051
  if (!result.ok) return result;
929
1052
  return {
930
1053
  ok: true,
@@ -933,6 +1056,7 @@ async function claimRunOwnership(target) {
933
1056
  };
934
1057
  }
935
1058
  async function runIssueSession(issueNumber, mode, input) {
1059
+ resetAgentInvocationState();
936
1060
  const paths = await resolveIssuePaths(issueNumber);
937
1061
  const resilience = await initResilienceConfig();
938
1062
  const configuredInactivity = resilience.watchdog?.inactivityTimeoutMs;
@@ -942,10 +1066,11 @@ async function runIssueSession(issueNumber, mode, input) {
942
1066
  const webConfig = await loadWebConfig();
943
1067
  const surfaces = [];
944
1068
  const journalEnabled = resilience.journal?.enabled === true;
945
- if (webConfig.enabled || journalEnabled) {
946
- await mkdir(paths.issueDir, { recursive: true });
1069
+ const persistSnapshot = webConfig.enabled || input.runOptions?.detachedChild === true;
1070
+ if (persistSnapshot || journalEnabled) {
1071
+ await mkdir2(paths.issueDir, { recursive: true });
947
1072
  }
948
- if (webConfig.enabled) {
1073
+ if (persistSnapshot) {
949
1074
  surfaces.push(
950
1075
  new FilePublisher(paths.sessionFile, {
951
1076
  logLimit: webConfig.logLimit,
@@ -962,7 +1087,7 @@ async function runIssueSession(issueNumber, mode, input) {
962
1087
  })
963
1088
  );
964
1089
  }
965
- const publisher = surfaces.length === 0 ? new NullPublisher() : surfaces.length === 1 ? surfaces[0] : new MultiPublisher(surfaces);
1090
+ const publisher = surfaces.length === 0 ? new MemoryPublisher() : surfaces.length === 1 ? surfaces[0] : new MultiPublisher(surfaces);
966
1091
  setSessionPublisher(publisher);
967
1092
  if (input.interruptedBy !== void 0) {
968
1093
  const previous = input.interruptedBy;
@@ -1045,7 +1170,7 @@ async function reportIfOversized(issueNumber, paths, result) {
1045
1170
  at: isoNow()
1046
1171
  });
1047
1172
  const reportFile = paths.decompositionFile;
1048
- await mkdir(paths.issueDir, { recursive: true });
1173
+ await mkdir2(paths.issueDir, { recursive: true });
1049
1174
  await writeFile(reportFile, report, "utf-8");
1050
1175
  printWarning(`Issue #${issueNumber} looks larger than one run. Report: ${reportFile}`);
1051
1176
  for (const signal of assessment.signals) {
@@ -1085,7 +1210,7 @@ async function maybeCreateSubIssues(issueNumber, plan, reportFile) {
1085
1210
  printInfo("Nothing left to split: no pending story.");
1086
1211
  return;
1087
1212
  }
1088
- const { runGenerate } = await import("./generate-EGB4KV2B.js");
1213
+ const { runGenerate } = await import("./generate-MVQI7A45.js");
1089
1214
  for (const proposal of proposals) {
1090
1215
  const instruction = [
1091
1216
  `Create a sub-issue of #${issueNumber}: ${proposal.title}.`,
@@ -1160,15 +1285,27 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
1160
1285
  issueUrl: info?.issueUrl,
1161
1286
  branch: info?.branch,
1162
1287
  phases: [...phases],
1163
- 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
+ }
1164
1294
  });
1165
1295
  };
1166
- 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
+ );
1167
1302
  const issuesConfig = await loadIssuesConfig();
1168
1303
  const sessionStartedAt = isoNow();
1169
1304
  if (queue?.preChecked !== true) {
1170
- printInfo("Running prerequisite checks...");
1171
- const initCode = await runInit(issuesConfig.preferredProvider);
1305
+ if (isVerbose()) {
1306
+ printInfo("Running prerequisite checks...");
1307
+ }
1308
+ const initCode = await runInit(issuesConfig.preferredProvider, { compact: !isVerbose() });
1172
1309
  if (initCode !== 0) {
1173
1310
  publishSessionStart(PIPELINE_PHASES, sessionStartedAt);
1174
1311
  publisher.publish({ type: "phase:start", at: sessionStartedAt, phase: "init" });
@@ -1328,18 +1465,17 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
1328
1465
  () => runPlan(issueNumber, resolvedIssue, { continueFlag: continueNumbering, startUs }),
1329
1466
  "plan"
1330
1467
  )();
1331
- if (effectiveNoBranch || effectivePrReview) {
1332
- try {
1333
- const plan = await loadTaskPlan(tasksPath);
1334
- if (effectiveNoBranch) {
1335
- plan.noBranch = true;
1336
- }
1468
+ try {
1469
+ const plan = await loadTaskPlan(tasksPath);
1470
+ publishStorySeed(publisher, plan.userStories, isoNow());
1471
+ if (effectiveNoBranch || effectivePrReview) {
1472
+ if (effectiveNoBranch) plan.noBranch = true;
1337
1473
  if (effectivePrReview) {
1338
1474
  plan.prReview = { ...plan.prReview, enabled: true, rounds: plan.prReview?.rounds ?? 0 };
1339
1475
  }
1340
1476
  await saveTaskPlan(tasksPath, plan);
1341
- } catch {
1342
1477
  }
1478
+ } catch {
1343
1479
  }
1344
1480
  if (queue !== void 0 && !effectiveNoBranch) {
1345
1481
  producedBranch = await adoptQueueBranch(tasksPath, queue.plan.branchName);
@@ -1411,27 +1547,34 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
1411
1547
  try {
1412
1548
  await fn();
1413
1549
  await publishGitState(publisher);
1414
- publisher.publish({ type: "phase:end", at: isoNow(), phase, success: true });
1550
+ await publishInstrumentedPhaseEnd(publisher, phase, issueNumber, true);
1415
1551
  } catch (err) {
1416
1552
  await publishGitState(publisher);
1417
- publisher.publish({
1418
- type: "phase:end",
1419
- at: isoNow(),
1553
+ await publishInstrumentedPhaseEnd(
1554
+ publisher,
1420
1555
  phase,
1421
- success: false,
1422
- error: err instanceof Error ? err.message : String(err)
1423
- });
1556
+ issueNumber,
1557
+ false,
1558
+ err instanceof Error ? err.message : String(err)
1559
+ );
1424
1560
  throw err;
1425
1561
  }
1426
1562
  }
1427
1563
  ])
1428
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
+ }
1429
1571
  const result = await runPipelineWithRenderer({
1430
1572
  phases: phaseOrder,
1431
1573
  startIndex: startIdx,
1432
1574
  verbose: isVerbose(),
1433
1575
  runners: instrumentedRunners,
1434
- tasksPath
1576
+ tasksPath,
1577
+ phaseSuffixes
1435
1578
  });
1436
1579
  if (!result.success) {
1437
1580
  printError(`Phase ${result.failedPhase} failed`);
@@ -1512,7 +1655,8 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
1512
1655
  prReview: review,
1513
1656
  // Process-owned counters: the session snapshot is empty whenever web
1514
1657
  // monitoring is off, so it cannot be the source of these totals.
1515
- usage: getRunUsageTotals()
1658
+ usage: getRunUsageTotals(),
1659
+ verification: verificationForSummary(getSessionPublisher().snapshot().verification)
1516
1660
  });
1517
1661
  return {
1518
1662
  code: 0,
@@ -1522,6 +1666,37 @@ async function runPipelinePhases(issueNumber, paths, mode, publisher, input) {
1522
1666
  elapsedSeconds: result.overallElapsedSeconds
1523
1667
  };
1524
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
+ }
1525
1700
  async function decideQueue(input) {
1526
1701
  const primary = input.requested[0];
1527
1702
  const single = input.requested.length === 1;
@@ -1541,7 +1716,11 @@ async function decideQueue(input) {
1541
1716
  planFile: queuePaths.planFile,
1542
1717
  noBranch: input.noBranch,
1543
1718
  prReview: input.prReview,
1544
- 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
+ }
1545
1724
  });
1546
1725
  } catch (err) {
1547
1726
  const message = err instanceof Error ? err.message : String(err);
@@ -1760,7 +1939,10 @@ async function buildPrQueueContext(plan) {
1760
1939
  number: entry.number,
1761
1940
  title: entry.title,
1762
1941
  url: entry.url,
1763
- source: entry.source
1942
+ source: entry.source,
1943
+ parent: entry.parent,
1944
+ role: entry.role,
1945
+ complete: entry.status === "completed"
1764
1946
  })),
1765
1947
  excluded: plan.excluded.map((entry) => ({
1766
1948
  id: entry.id,
@@ -1829,4 +2011,4 @@ export {
1829
2011
  publishIssueDetails,
1830
2012
  runPipeline
1831
2013
  };
1832
- //# sourceMappingURL=chunk-NTMMF5UD.js.map
2014
+ //# sourceMappingURL=chunk-OPNGPUKJ.js.map