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
@@ -0,0 +1,1111 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ decideRouting
4
+ } from "./chunk-3XLCNVMD.js";
5
+ import {
6
+ parseJournal
7
+ } from "./chunk-QCXPNSEL.js";
8
+ import {
9
+ ensureRunnersRegistered,
10
+ getActiveResilienceConfig,
11
+ getAgentCliOverrides,
12
+ getRegisteredProviders,
13
+ getShutdownSignal,
14
+ hasExplicitAgentSelection,
15
+ loadAgentConfig,
16
+ loadRoutingConfig,
17
+ peekHarnessVersion,
18
+ resolveAgentFor,
19
+ resolveAntigravityTimeoutMs,
20
+ runnerFor
21
+ } from "./chunk-CNBYCMSU.js";
22
+ import {
23
+ resolveProjectPaths
24
+ } from "./chunk-HQAHNIAR.js";
25
+ import {
26
+ ElapsedTimer,
27
+ createSpinner,
28
+ formatDuration,
29
+ getActivityCallback,
30
+ getIcons,
31
+ getInactivityTimeout,
32
+ getOutputCallback,
33
+ getSessionPublisher,
34
+ isVerbose,
35
+ useColor
36
+ } from "./chunk-IYV6KNVF.js";
37
+ import {
38
+ beginExecution,
39
+ endExecution
40
+ } from "./chunk-6FDFRNXZ.js";
41
+ import {
42
+ isoNow,
43
+ sumUsage
44
+ } from "./chunk-DIG5XKQQ.js";
45
+ import {
46
+ isAgentProviderId,
47
+ providersHealthSchema
48
+ } from "./chunk-NEZZ6KDR.js";
49
+ import {
50
+ ensureCursorStorageGrant
51
+ } from "./chunk-TVN2JVOF.js";
52
+ import {
53
+ abortableDelay,
54
+ resolvePolicy,
55
+ shouldFailover,
56
+ withRetry
57
+ } from "./chunk-KQHB7ZT3.js";
58
+ import {
59
+ classify
60
+ } from "./chunk-XEYH6ZZI.js";
61
+
62
+ // src/routing/budget.ts
63
+ var DEFAULT_CEILINGS = {
64
+ maxCostUsdPerIssue: null,
65
+ maxDurationMsPerIssue: null,
66
+ maxExecutionsPerIssue: null,
67
+ onCeiling: "block"
68
+ };
69
+ function evaluateCeilings(input) {
70
+ const ceilings = { ...DEFAULT_CEILINGS, ...input.ceilings };
71
+ const enforced = [];
72
+ if (ceilings.maxCostUsdPerIssue !== null && input.spent.costStatus === "reported") {
73
+ enforced.push("cost");
74
+ }
75
+ if (ceilings.maxDurationMsPerIssue !== null) enforced.push("duration");
76
+ if (ceilings.maxExecutionsPerIssue !== null) enforced.push("executions");
77
+ if (ceilings.maxCostUsdPerIssue !== null && input.spent.costStatus === "reported" && input.spent.reportedUsd >= ceilings.maxCostUsdPerIssue) {
78
+ return {
79
+ ok: false,
80
+ stopReason: "max_cost",
81
+ binding: "cost",
82
+ onCeiling: "block",
83
+ numbers: { spent: input.spent.reportedUsd, ceiling: ceilings.maxCostUsdPerIssue },
84
+ enforced
85
+ };
86
+ }
87
+ if (ceilings.maxDurationMsPerIssue !== null && input.spent.durationMs >= ceilings.maxDurationMsPerIssue) {
88
+ return {
89
+ ok: false,
90
+ stopReason: "max_duration",
91
+ binding: "duration",
92
+ onCeiling: "block",
93
+ numbers: { spent: input.spent.durationMs, ceiling: ceilings.maxDurationMsPerIssue },
94
+ enforced
95
+ };
96
+ }
97
+ if (ceilings.maxExecutionsPerIssue !== null && input.spent.executions >= ceilings.maxExecutionsPerIssue) {
98
+ return {
99
+ ok: false,
100
+ stopReason: "max_attempts",
101
+ binding: "executions",
102
+ onCeiling: "block",
103
+ numbers: { spent: input.spent.executions, ceiling: ceilings.maxExecutionsPerIssue },
104
+ enforced
105
+ };
106
+ }
107
+ return { ok: true, binding: null };
108
+ }
109
+ function spendFromCost(cost) {
110
+ if (cost.status === "reported") {
111
+ return { reportedUsd: cost.amount, costStatus: "reported" };
112
+ }
113
+ return { reportedUsd: 0, costStatus: cost.status };
114
+ }
115
+
116
+ // src/agents/health.ts
117
+ import { mkdir, readFile, rename, writeFile } from "fs/promises";
118
+ import { dirname } from "path";
119
+ var DEFAULT_PROVIDER_COOLDOWN_MS = 6e4;
120
+ var DEFAULT_PROVIDER_MAX_COOLDOWN_MS = 30 * 6e4;
121
+ var DEFAULT_PROVIDER_FAILURE_WINDOW_MS = 5 * 6e4;
122
+ var DEFAULT_PROVIDER_FAILURES_TO_TRIP = 3;
123
+ function resolveProviderHealthConfig(config = {}) {
124
+ return {
125
+ cooldownMs: Math.max(0, config.cooldownMs ?? DEFAULT_PROVIDER_COOLDOWN_MS),
126
+ maxCooldownMs: Math.max(
127
+ config.cooldownMs ?? DEFAULT_PROVIDER_COOLDOWN_MS,
128
+ config.maxCooldownMs ?? DEFAULT_PROVIDER_MAX_COOLDOWN_MS
129
+ ),
130
+ failureWindowMs: Math.max(1, config.failureWindowMs ?? DEFAULT_PROVIDER_FAILURE_WINDOW_MS),
131
+ failuresToTrip: Math.max(
132
+ 1,
133
+ Math.trunc(config.failuresToTrip ?? DEFAULT_PROVIDER_FAILURES_TO_TRIP)
134
+ )
135
+ };
136
+ }
137
+ function emptyHealth() {
138
+ return providersHealthSchema.parse({});
139
+ }
140
+ function emptyRecord() {
141
+ return providersHealthSchema.parse({ providers: { provider: {} } }).providers.provider;
142
+ }
143
+ async function readProvidersHealth(filePath) {
144
+ try {
145
+ return providersHealthSchema.parse(JSON.parse(await readFile(filePath, "utf-8")));
146
+ } catch {
147
+ return emptyHealth();
148
+ }
149
+ }
150
+ async function writeProvidersHealth(filePath, value) {
151
+ await mkdir(dirname(filePath), { recursive: true });
152
+ const tmp = `${filePath}.tmp-${process.pid}`;
153
+ await writeFile(tmp, `${JSON.stringify(value, null, 2)}
154
+ `, "utf-8");
155
+ await rename(tmp, filePath);
156
+ }
157
+ async function updateRecord(filePath, provider, update) {
158
+ const health = await readProvidersHealth(filePath);
159
+ const next = update(health.providers[provider] ?? emptyRecord());
160
+ await writeProvidersHealth(filePath, {
161
+ ...health,
162
+ providers: { ...health.providers, [provider]: next }
163
+ });
164
+ return next;
165
+ }
166
+ function cooldownDelay(level, config) {
167
+ return Math.min(config.maxCooldownMs, config.cooldownMs * 2 ** Math.max(0, level));
168
+ }
169
+ var BREAKER_KINDS = /* @__PURE__ */ new Set([
170
+ "timeout",
171
+ "stalled",
172
+ "provider_down",
173
+ "provider_crash"
174
+ ]);
175
+ async function recordProviderFailure(filePath, provider, failure, options = {}) {
176
+ const nowMs = options.now?.() ?? Date.now();
177
+ const at = new Date(nowMs).toISOString();
178
+ const config = resolveProviderHealthConfig(options.config);
179
+ return updateRecord(filePath, provider, (record) => {
180
+ const failures = [
181
+ ...record.failures.filter((entry) => {
182
+ const timestamp = Date.parse(entry.at);
183
+ return Number.isFinite(timestamp) && nowMs - timestamp <= config.failureWindowMs;
184
+ }),
185
+ { at, kind: failure.kind }
186
+ ];
187
+ const consecutiveFailures = record.consecutiveFailures + 1;
188
+ if (failure.kind === "authentication") {
189
+ return {
190
+ ...record,
191
+ status: "auth_failed",
192
+ failures,
193
+ consecutiveFailures,
194
+ cooldownUntil: null,
195
+ lastFailureKind: failure.kind,
196
+ lastFailureAt: at,
197
+ probeInFlight: false,
198
+ probeStartedAt: null
199
+ };
200
+ }
201
+ if (failure.kind === "rate_limit") {
202
+ const delay = failure.retryAfterMs ?? cooldownDelay(record.cooldownLevel, config);
203
+ return {
204
+ ...record,
205
+ status: "rate_limited",
206
+ failures,
207
+ consecutiveFailures,
208
+ cooldownLevel: failure.retryAfterMs === void 0 ? record.cooldownLevel + 1 : record.cooldownLevel,
209
+ cooldownUntil: new Date(nowMs + delay).toISOString(),
210
+ lastFailureKind: failure.kind,
211
+ lastFailureAt: at,
212
+ probeInFlight: false,
213
+ probeStartedAt: null
214
+ };
215
+ }
216
+ if (BREAKER_KINDS.has(failure.kind)) {
217
+ const recentProviderFailures = failures.filter(
218
+ (entry) => BREAKER_KINDS.has(entry.kind)
219
+ ).length;
220
+ const unavailable = record.status === "half_open" || recentProviderFailures >= config.failuresToTrip;
221
+ const delay = unavailable ? cooldownDelay(record.cooldownLevel, config) : 0;
222
+ return {
223
+ ...record,
224
+ status: unavailable ? "unavailable" : "degraded",
225
+ failures,
226
+ consecutiveFailures,
227
+ cooldownLevel: unavailable ? record.cooldownLevel + 1 : record.cooldownLevel,
228
+ cooldownUntil: unavailable ? new Date(nowMs + delay).toISOString() : null,
229
+ lastFailureKind: failure.kind,
230
+ lastFailureAt: at,
231
+ probeInFlight: false,
232
+ probeStartedAt: null
233
+ };
234
+ }
235
+ return {
236
+ ...record,
237
+ lastFailureKind: failure.kind,
238
+ lastFailureAt: at,
239
+ probeInFlight: false,
240
+ probeStartedAt: null
241
+ };
242
+ });
243
+ }
244
+ async function recordProviderSuccess(filePath, provider, options = {}) {
245
+ const at = new Date(options.now?.() ?? Date.now()).toISOString();
246
+ return updateRecord(filePath, provider, (record) => ({
247
+ ...record,
248
+ status: "healthy",
249
+ failures: [],
250
+ consecutiveFailures: 0,
251
+ cooldownLevel: 0,
252
+ cooldownUntil: null,
253
+ lastFailureKind: null,
254
+ lastSuccessAt: at,
255
+ probeInFlight: false,
256
+ probeStartedAt: null
257
+ }));
258
+ }
259
+ async function openProviderCircuit(filePath, provider, options = {}) {
260
+ const nowMs = options.now?.() ?? Date.now();
261
+ const config = resolveProviderHealthConfig(options.config);
262
+ return updateRecord(filePath, provider, (record) => {
263
+ if (record.status === "unavailable" || record.status === "rate_limited") return record;
264
+ const delay = cooldownDelay(record.cooldownLevel, config);
265
+ return {
266
+ ...record,
267
+ status: "unavailable",
268
+ cooldownLevel: record.cooldownLevel + 1,
269
+ cooldownUntil: new Date(nowMs + delay).toISOString(),
270
+ probeInFlight: false,
271
+ probeStartedAt: null
272
+ };
273
+ });
274
+ }
275
+ async function acquireHalfOpenProbe(filePath, provider, options = {}) {
276
+ const nowMs = options.now?.() ?? Date.now();
277
+ const at = new Date(nowMs).toISOString();
278
+ const config = resolveProviderHealthConfig(options.config);
279
+ let acquired = false;
280
+ const record = await updateRecord(filePath, provider, (current) => {
281
+ const cooldownUntil = current.cooldownUntil === null ? 0 : Date.parse(current.cooldownUntil);
282
+ if (Number.isFinite(cooldownUntil) && cooldownUntil > nowMs) return current;
283
+ const probeStarted = current.probeStartedAt === null ? 0 : Date.parse(current.probeStartedAt);
284
+ const probeIsFresh = current.probeInFlight && Number.isFinite(probeStarted) && nowMs - probeStarted < config.cooldownMs;
285
+ if (probeIsFresh) return current;
286
+ acquired = true;
287
+ return {
288
+ ...current,
289
+ status: "half_open",
290
+ cooldownUntil: null,
291
+ probeInFlight: true,
292
+ probeStartedAt: at
293
+ };
294
+ });
295
+ return { acquired, record };
296
+ }
297
+
298
+ // src/agents/select.ts
299
+ var AgentSelectionBlockedError = class extends Error {
300
+ provider;
301
+ kind;
302
+ constructor(provider, kind) {
303
+ super(
304
+ `Authentication failed for agent provider '${provider}' (${kind}); the run is blocked. Authenticate or repair that provider before resuming.`
305
+ );
306
+ this.name = "AgentSelectionBlockedError";
307
+ this.provider = provider;
308
+ this.kind = kind;
309
+ }
310
+ };
311
+ function remainingMs(until, nowMs) {
312
+ if (until === null) return null;
313
+ const parsed = Date.parse(until);
314
+ return Number.isFinite(parsed) ? Math.max(0, parsed - nowMs) : null;
315
+ }
316
+ function failoverIsDue(record, config) {
317
+ if (record?.lastFailureKind == null) return false;
318
+ return shouldFailover(resolvePolicy(record.lastFailureKind, config), record.consecutiveFailures);
319
+ }
320
+ async function verdictFor(provider, record, options) {
321
+ if (record === void 0 || record.status === "healthy") {
322
+ return { available: true, waitMs: null, cooldownUntil: null, reason: null, blocked: false };
323
+ }
324
+ const reason = record.lastFailureKind;
325
+ if (record.status === "auth_failed") {
326
+ const mayFailover = reason !== null && failoverIsDue(record, options.config);
327
+ return {
328
+ available: false,
329
+ waitMs: null,
330
+ cooldownUntil: null,
331
+ reason,
332
+ blocked: options.primary && !mayFailover
333
+ };
334
+ }
335
+ if (record.status === "degraded") {
336
+ const due = failoverIsDue(record, options.config);
337
+ const opened = due ? await openProviderCircuit(options.healthFile, provider, {
338
+ now: () => options.nowMs,
339
+ config: options.config.providers
340
+ }) : record;
341
+ return {
342
+ available: !due,
343
+ waitMs: due ? remainingMs(opened.cooldownUntil, options.nowMs) : null,
344
+ cooldownUntil: opened.cooldownUntil,
345
+ reason,
346
+ blocked: false
347
+ };
348
+ }
349
+ if (record.status === "unavailable" || record.status === "rate_limited" || record.status === "half_open") {
350
+ const waitMs = remainingMs(record.cooldownUntil, options.nowMs);
351
+ if (waitMs !== null && waitMs > 0) {
352
+ return {
353
+ available: false,
354
+ waitMs,
355
+ cooldownUntil: record.cooldownUntil,
356
+ reason,
357
+ blocked: false
358
+ };
359
+ }
360
+ const probe = await acquireHalfOpenProbe(options.healthFile, provider, {
361
+ now: () => options.nowMs,
362
+ config: options.config.providers
363
+ });
364
+ return {
365
+ available: probe.acquired,
366
+ waitMs: probe.acquired ? null : resolveProviderHealthConfig(options.config.providers).cooldownMs,
367
+ cooldownUntil: probe.record.cooldownUntil,
368
+ reason,
369
+ blocked: false
370
+ };
371
+ }
372
+ return { available: true, waitMs: null, cooldownUntil: null, reason, blocked: false };
373
+ }
374
+ function providerOrder(primary, config) {
375
+ ensureRunnersRegistered();
376
+ const registered = getRegisteredProviders();
377
+ const configured = (config.providers?.chain ?? []).filter(isAgentProviderId);
378
+ return [.../* @__PURE__ */ new Set([primary, ...configured, ...registered])];
379
+ }
380
+ function settingsFor(base, provider) {
381
+ if (provider === base.provider) return base;
382
+ return {
383
+ ...base,
384
+ provider,
385
+ // A model name belongs to the configured primary unless the selected
386
+ // provider is that primary. Passing a Claude alias to Codex (or vice
387
+ // versa) is less useful than letting the fallback use its own default.
388
+ model: null
389
+ };
390
+ }
391
+ async function selectAgentForInvocation(phase, options = {}) {
392
+ const config = options.config ?? getActiveResilienceConfig();
393
+ const base = await resolveAgentFor(phase);
394
+ const failover = config.providers?.failover === true;
395
+ if (!failover) {
396
+ return {
397
+ primary: base.provider,
398
+ provider: base.provider,
399
+ settings: base,
400
+ healthFile: null,
401
+ failover: false,
402
+ reason: null,
403
+ cooldownUntil: null
404
+ };
405
+ }
406
+ let healthFile;
407
+ try {
408
+ healthFile = (await resolveProjectPaths()).providersHealthFile;
409
+ } catch {
410
+ return {
411
+ primary: base.provider,
412
+ provider: base.provider,
413
+ settings: base,
414
+ healthFile: null,
415
+ failover: true,
416
+ reason: null,
417
+ cooldownUntil: null
418
+ };
419
+ }
420
+ const delay = options.delay ?? abortableDelay;
421
+ selection: for (; ; ) {
422
+ const nowMs = options.now?.() ?? Date.now();
423
+ const health = await readProvidersHealth(healthFile);
424
+ const waits = [];
425
+ let primaryReason = null;
426
+ let primaryCooldown = null;
427
+ for (const provider of providerOrder(base.provider, config)) {
428
+ const verdict = await verdictFor(provider, health.providers[provider], {
429
+ healthFile,
430
+ config,
431
+ primary: provider === base.provider,
432
+ nowMs
433
+ });
434
+ if (provider === base.provider) {
435
+ primaryReason = verdict.reason;
436
+ primaryCooldown = verdict.cooldownUntil;
437
+ }
438
+ if (verdict.blocked && verdict.reason !== null) {
439
+ throw new AgentSelectionBlockedError(provider, verdict.reason);
440
+ }
441
+ if (provider === base.provider && !verdict.available && verdict.waitMs !== null && !failoverIsDue(health.providers[provider], config)) {
442
+ if (!await delay(Math.max(1, verdict.waitMs), { signal: getShutdownSignal() })) {
443
+ throw new Error("Agent provider selection was interrupted while waiting for cooldown.");
444
+ }
445
+ continue selection;
446
+ }
447
+ if (verdict.available) {
448
+ return {
449
+ primary: base.provider,
450
+ provider,
451
+ settings: settingsFor(base, provider),
452
+ healthFile,
453
+ failover: provider !== base.provider,
454
+ reason: provider === base.provider ? null : primaryReason,
455
+ cooldownUntil: primaryCooldown
456
+ };
457
+ }
458
+ if (verdict.waitMs !== null) waits.push(verdict.waitMs);
459
+ }
460
+ const waitMs = waits.length > 0 ? Math.max(1, Math.min(...waits)) : null;
461
+ if (waitMs === null) {
462
+ throw new Error(
463
+ "No healthy agent provider is available and none has a cooldown to wait for."
464
+ );
465
+ }
466
+ if (!await delay(waitMs, { signal: getShutdownSignal() })) {
467
+ throw new Error("Agent provider selection was interrupted while waiting for cooldown.");
468
+ }
469
+ }
470
+ }
471
+
472
+ // src/agents/invoke.ts
473
+ async function selectionForForced(invocation) {
474
+ const settings = await resolveAgentFor(invocation.phase, {
475
+ cli: { forceProvider: invocation.forceProvider }
476
+ });
477
+ return {
478
+ primary: settings.provider,
479
+ provider: settings.provider,
480
+ settings,
481
+ healthFile: null,
482
+ failover: false,
483
+ reason: null,
484
+ cooldownUntil: null
485
+ };
486
+ }
487
+ var attempts = /* @__PURE__ */ new Map();
488
+ var lastFailure = /* @__PURE__ */ new Map();
489
+ var issueSpend = {
490
+ reportedUsd: 0,
491
+ durationMs: 0,
492
+ executions: 0,
493
+ sawReported: false
494
+ };
495
+ function declaredAgentIdentity(provider) {
496
+ switch (provider) {
497
+ case "claude":
498
+ return { harness: "claude-code", vendor: "anthropic" };
499
+ case "codex":
500
+ return { harness: "codex-cli", vendor: "openai" };
501
+ case "cursor":
502
+ return { harness: "cursor-cli", vendor: "cursor" };
503
+ case "antigravity":
504
+ return { harness: "antigravity-cli", vendor: "google" };
505
+ default: {
506
+ const _exhaustive = provider;
507
+ return _exhaustive;
508
+ }
509
+ }
510
+ }
511
+ function triggerOf(selection, attempt) {
512
+ if (selection.failover) return "fallback";
513
+ if (attempt > 1) return "retry";
514
+ return "initial";
515
+ }
516
+ function nextAttempt(phase) {
517
+ const attempt = (attempts.get(phase) ?? 0) + 1;
518
+ attempts.set(phase, attempt);
519
+ return attempt;
520
+ }
521
+ function resetAgentInvocationState() {
522
+ attempts.clear();
523
+ lastFailure.clear();
524
+ issueSpend.reportedUsd = 0;
525
+ issueSpend.durationMs = 0;
526
+ issueSpend.executions = 0;
527
+ issueSpend.sawReported = false;
528
+ }
529
+ async function invokeSelectedAgent(invocation) {
530
+ const config = getActiveResilienceConfig();
531
+ const selection = invocation.forceProvider === void 0 ? await selectAgentForInvocation(invocation.phase, { config }) : await selectionForForced(invocation);
532
+ const attempt = nextAttempt(invocation.phase);
533
+ const publisher = getSessionPublisher();
534
+ publisher.publish({
535
+ type: "agent:attempt",
536
+ at: isoNow(),
537
+ attempt,
538
+ provider: selection.provider,
539
+ model: selection.settings.model,
540
+ primaryProvider: selection.primary
541
+ });
542
+ if (selection.failover) {
543
+ publisher.publish({
544
+ type: "failover",
545
+ at: isoNow(),
546
+ from: selection.primary,
547
+ to: selection.provider,
548
+ reason: selection.reason,
549
+ cooldownUntil: selection.cooldownUntil
550
+ });
551
+ }
552
+ const identity = declaredAgentIdentity(selection.provider);
553
+ const requested = selection.settings.model;
554
+ const routingCfg = await loadRoutingConfig();
555
+ const agentCfg = await loadAgentConfig();
556
+ const routingDecision = decideRouting({
557
+ phase: invocation.phase,
558
+ actualHarness: identity.harness,
559
+ mode: routingCfg.mode,
560
+ profile: routingCfg.profile,
561
+ skipScore: hasExplicitAgentSelection(agentCfg, getAgentCliOverrides()),
562
+ requiresExtraDirectories: (invocation.addDirs?.length ?? 0) > 0
563
+ });
564
+ const executionId = await beginExecution({
565
+ purpose: invocation.purpose ?? invocation.phase,
566
+ attempt,
567
+ trigger: triggerOf(selection, attempt),
568
+ triggerReason: selection.failover ? selection.reason : attempt > 1 ? lastFailure.get(invocation.phase) ?? null : null,
569
+ harness: identity.harness,
570
+ provider: identity.vendor,
571
+ harnessVersion: peekHarnessVersion(selection.provider) ?? null,
572
+ modelRequested: requested,
573
+ modelResolved: null,
574
+ modelSource: requested ? "config" : "unavailable",
575
+ ...routingDecision === null ? {} : { routingDecision }
576
+ });
577
+ const runner = runnerFor(selection.provider);
578
+ const ceiling = evaluateCeilings({
579
+ ceilings: routingCfg.ceilings,
580
+ spent: {
581
+ reportedUsd: issueSpend.reportedUsd,
582
+ costStatus: issueSpend.sawReported ? "reported" : "unknown",
583
+ durationMs: issueSpend.durationMs,
584
+ executions: issueSpend.executions
585
+ }
586
+ });
587
+ if (!ceiling.ok) {
588
+ const detail = `blocked by ${ceiling.binding} ceiling (${ceiling.numbers.spent} \u2265 ${ceiling.numbers.ceiling}). Enforced by Issue Flow, not by a harness flag.`;
589
+ const run2 = {
590
+ success: false,
591
+ result: "",
592
+ rawOutput: detail,
593
+ exitCode: 1,
594
+ usage: null,
595
+ error: detail,
596
+ agent: { provider: selection.provider, model: selection.settings.model },
597
+ harnessVersion: peekHarnessVersion(selection.provider)
598
+ };
599
+ if (executionId !== null) {
600
+ await endExecution({
601
+ id: executionId,
602
+ status: "failed",
603
+ error: detail,
604
+ exitCode: 1,
605
+ stopReason: ceiling.stopReason
606
+ });
607
+ }
608
+ return {
609
+ run: run2,
610
+ failure: null,
611
+ selection,
612
+ attempt,
613
+ health: null,
614
+ blocked: { stopReason: ceiling.stopReason, detail, actor: "human" }
615
+ };
616
+ }
617
+ if (runner.capabilities.nativeTimeout && invocation.timeout === 0 && resolveAntigravityTimeoutMs(invocation, selection.settings) === null) {
618
+ const run2 = {
619
+ success: false,
620
+ result: "",
621
+ rawOutput: "configuration: provider has nativeTimeout and received timeout: 0 without a ceiling.",
622
+ exitCode: 1,
623
+ usage: null,
624
+ error: "configuration: provider has nativeTimeout and received timeout: 0 without a ceiling. Set agent.antigravity.executeTimeout.",
625
+ agent: { provider: selection.provider, model: selection.settings.model },
626
+ harnessVersion: peekHarnessVersion(selection.provider)
627
+ };
628
+ if (executionId !== null) {
629
+ await endExecution({
630
+ id: executionId,
631
+ status: "failed",
632
+ error: run2.error,
633
+ exitCode: 1
634
+ });
635
+ }
636
+ const failure2 = classify({ source: "agent", exitCode: 1, stdout: run2.error });
637
+ return { run: run2, failure: failure2, selection, attempt, health: null };
638
+ }
639
+ if ((invocation.addDirs?.length ?? 0) > 0 && runner.capabilities.extraDirectories === "permission-file") {
640
+ const grant = selection.settings.cursor.permissionsFile === "none" ? { skipped: true, reason: "none" } : await ensureCursorStorageGrant({
641
+ mode: selection.settings.cursor.permissionsFile ?? "global"
642
+ });
643
+ if ("skipped" in grant && selection.settings.cursor.permissionsFile !== "none") {
644
+ throw new Error(
645
+ `Phase '${invocation.phase}' needs extraDirectories on '${selection.provider}', which only grants them via a permission file. Run \`issue-flow agent use cursor\` or set agent.cursor.permissionsFile.`
646
+ );
647
+ }
648
+ } else if ((invocation.addDirs?.length ?? 0) > 0 && runner.capabilities.extraDirectories === "none") {
649
+ throw new Error(
650
+ `Phase '${invocation.phase}' needs extraDirectories, but '${selection.provider}' cannot grant them.`
651
+ );
652
+ }
653
+ let run;
654
+ const startedMs = Date.now();
655
+ try {
656
+ run = await runner.run(
657
+ {
658
+ ...invocation,
659
+ onLine: (line) => {
660
+ publisher.publish({
661
+ type: "agent:activity",
662
+ at: isoNow(),
663
+ provider: selection.provider
664
+ });
665
+ invocation.onLine?.(line);
666
+ }
667
+ },
668
+ selection.settings
669
+ );
670
+ } catch (err) {
671
+ if (executionId !== null) {
672
+ await endExecution({
673
+ id: executionId,
674
+ status: "failed",
675
+ error: err instanceof Error ? err.message : String(err)
676
+ });
677
+ }
678
+ throw err;
679
+ }
680
+ issueSpend.executions += 1;
681
+ issueSpend.durationMs += Math.max(0, Date.now() - startedMs);
682
+ if (run.usage?.costUsd !== void 0) {
683
+ issueSpend.reportedUsd += run.usage.costUsd;
684
+ issueSpend.sawReported = true;
685
+ }
686
+ const failure = run.success && run.exitCode === 0 ? null : classify({
687
+ source: "agent",
688
+ exitCode: run.exitCode,
689
+ stdout: run.rawOutput || run.error || ""
690
+ });
691
+ if (executionId !== null) {
692
+ await endExecution({
693
+ id: executionId,
694
+ status: failure === null ? "completed" : failure.kind === "timeout" ? "timeout" : "failed",
695
+ usage: run.usage,
696
+ error: failure === null ? null : run.error ?? run.rawOutput,
697
+ exitCode: run.exitCode,
698
+ modelResolved: run.agent.model,
699
+ modelSource: run.agent.model ? requested ? "config" : "provider" : requested ? "config" : "unavailable",
700
+ harnessVersion: run.harnessVersion ?? peekHarnessVersion(selection.provider) ?? null,
701
+ providerSessionId: run.sessionId ?? null
702
+ });
703
+ }
704
+ if (failure === null) lastFailure.delete(invocation.phase);
705
+ else lastFailure.set(invocation.phase, failure.kind);
706
+ let health = null;
707
+ if (selection.healthFile !== null) {
708
+ health = failure === null ? await recordProviderSuccess(selection.healthFile, selection.provider) : await recordProviderFailure(selection.healthFile, selection.provider, failure, {
709
+ config: config.providers
710
+ });
711
+ }
712
+ publisher.publish({
713
+ type: "agent:result",
714
+ at: isoNow(),
715
+ provider: selection.provider,
716
+ success: failure === null,
717
+ ...failure === null ? {} : { failureKind: failure.kind },
718
+ cooldownUntil: health?.cooldownUntil ?? null
719
+ });
720
+ if (failure === null) attempts.delete(invocation.phase);
721
+ return { run, failure, selection, attempt, health };
722
+ }
723
+
724
+ // src/core/decompose.ts
725
+ var DECOMPOSITION_THRESHOLDS = {
726
+ /** Timeouts on the same phase. Two is already a pattern, not bad luck. */
727
+ timeoutsPerPhase: 2,
728
+ /** Stories in the plan. Past this, the plan is a backlog. */
729
+ stories: 15,
730
+ /** Iterations in a row that completed no story. */
731
+ barrenIterations: 5,
732
+ /** Files touched on the branch. */
733
+ filesTouched: 40,
734
+ /** Characters in the issue body. */
735
+ issueBodyChars: 2e4
736
+ };
737
+ var DECOMPOSITION_MIN_SIGNALS = 2;
738
+ function timeoutsByPhase(journal) {
739
+ const counts = /* @__PURE__ */ new Map();
740
+ let phase = "(unknown)";
741
+ for (const { event } of parseJournal(journal)) {
742
+ if (event.type === "phase:start") {
743
+ phase = event.phase;
744
+ continue;
745
+ }
746
+ if (event.type !== "retry") continue;
747
+ const kind = event.kind;
748
+ const reason = event.reason ?? "";
749
+ const timedOut = kind === "timeout" || kind === "stalled" || /timed out|stalled/i.test(reason);
750
+ if (!timedOut) continue;
751
+ counts.set(phase, (counts.get(phase) ?? 0) + 1);
752
+ }
753
+ return counts;
754
+ }
755
+ function longestBarrenRun(journal) {
756
+ let passing = 0;
757
+ let current = 0;
758
+ let longest = 0;
759
+ for (const { event } of parseJournal(journal)) {
760
+ if (event.type === "iteration:start") {
761
+ current++;
762
+ longest = Math.max(longest, current);
763
+ continue;
764
+ }
765
+ if (event.type !== "stories:update") continue;
766
+ const nowPassing = event.stories.filter((story) => story.passes).length;
767
+ if (nowPassing > passing) current = 0;
768
+ passing = Math.max(passing, nowPassing);
769
+ }
770
+ return longest;
771
+ }
772
+ function assessDecomposition(input) {
773
+ const signals = [];
774
+ const journal = input.journal ?? "";
775
+ for (const [phase, count] of timeoutsByPhase(journal)) {
776
+ if (count >= DECOMPOSITION_THRESHOLDS.timeoutsPerPhase) {
777
+ signals.push({
778
+ id: "repeated-timeouts",
779
+ detail: `The '${phase}' phase timed out ${count} times (threshold: ${DECOMPOSITION_THRESHOLDS.timeoutsPerPhase}).`
780
+ });
781
+ }
782
+ }
783
+ const stories = input.plan?.userStories.length ?? 0;
784
+ if (stories > DECOMPOSITION_THRESHOLDS.stories) {
785
+ signals.push({
786
+ id: "story-count",
787
+ detail: `The plan has ${stories} user stories (threshold: ${DECOMPOSITION_THRESHOLDS.stories}).`
788
+ });
789
+ }
790
+ const barren = longestBarrenRun(journal);
791
+ if (barren >= DECOMPOSITION_THRESHOLDS.barrenIterations) {
792
+ signals.push({
793
+ id: "barren-iterations",
794
+ detail: `${barren} iterations in a row completed no story (threshold: ${DECOMPOSITION_THRESHOLDS.barrenIterations}).`
795
+ });
796
+ }
797
+ const files = input.filesTouched ?? 0;
798
+ if (files > DECOMPOSITION_THRESHOLDS.filesTouched) {
799
+ signals.push({
800
+ id: "files-touched",
801
+ detail: `${files} files were touched on this branch (threshold: ${DECOMPOSITION_THRESHOLDS.filesTouched}).`
802
+ });
803
+ }
804
+ const body = input.issueBody?.length ?? 0;
805
+ if (body > DECOMPOSITION_THRESHOLDS.issueBodyChars) {
806
+ signals.push({
807
+ id: "issue-size",
808
+ detail: `The issue body is ${body} characters (threshold: ${DECOMPOSITION_THRESHOLDS.issueBodyChars}).`
809
+ });
810
+ }
811
+ if (input.hitMaxIterations === true) {
812
+ signals.push({
813
+ id: "max-iterations",
814
+ detail: "The execute loop stopped because it ran out of iterations, not because it finished."
815
+ });
816
+ }
817
+ return { signals, oversized: signals.length >= DECOMPOSITION_MIN_SIGNALS };
818
+ }
819
+ var STORIES_PER_SUBISSUE = 5;
820
+ function proposeSubIssues(plan) {
821
+ const pending = [...plan?.userStories ?? []].filter((story) => !story.passes).sort((a, b) => a.priority - b.priority);
822
+ if (pending.length === 0) return [];
823
+ const chunks = [];
824
+ for (let index = 0; index < pending.length; index += STORIES_PER_SUBISSUE) {
825
+ const stories = pending.slice(index, index + STORIES_PER_SUBISSUE);
826
+ const first = stories[0];
827
+ const last = stories[stories.length - 1];
828
+ const range = first === void 0 || last === void 0 || first.id === last.id ? first?.id ?? "" : `${first.id}\u2013${last.id}`;
829
+ chunks.push({
830
+ title: `${plan?.description ?? "Issue"} \u2014 part ${chunks.length + 1} (${range})`,
831
+ stories,
832
+ dependsOn: chunks.length === 0 ? [] : [chunks[chunks.length - 1]?.title ?? ""]
833
+ });
834
+ }
835
+ return chunks;
836
+ }
837
+ function buildDecompositionReport(input) {
838
+ const { issueNumber, assessment, plan, at } = input;
839
+ const proposals = proposeSubIssues(plan);
840
+ const lines = [];
841
+ lines.push(`# Issue #${issueNumber} looks larger than one run`, "");
842
+ lines.push(`Detected at ${at}.`, "");
843
+ lines.push(
844
+ "This is a report, not an action. Nothing has been split, and nothing will be",
845
+ "without you asking for it.",
846
+ ""
847
+ );
848
+ lines.push("## What was detected", "");
849
+ for (const signal of assessment.signals) {
850
+ lines.push(`- **${signal.id}** \u2014 ${signal.detail}`);
851
+ }
852
+ lines.push("");
853
+ if (proposals.length === 0) {
854
+ lines.push("## Proposed split", "");
855
+ lines.push(
856
+ "No pending user stories were found, so there is nothing to cut here.",
857
+ "The signals above are about the run rather than about the plan.",
858
+ ""
859
+ );
860
+ return `${lines.join("\n")}
861
+ `;
862
+ }
863
+ lines.push("## Proposed split", "");
864
+ lines.push(
865
+ `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`,
866
+ "dependency shape that can be derived from the plan alone \u2014 read it as a starting",
867
+ "point, not as an answer.",
868
+ ""
869
+ );
870
+ for (const proposal of proposals) {
871
+ lines.push(`### ${proposal.title}`, "");
872
+ if (proposal.dependsOn.length > 0) {
873
+ lines.push(`Depends on: ${proposal.dependsOn.join(", ")}`, "");
874
+ }
875
+ for (const story of proposal.stories) {
876
+ lines.push(`- \`${story.id}\` ${story.title}`);
877
+ }
878
+ lines.push("");
879
+ }
880
+ lines.push("## What to do next", "");
881
+ lines.push(
882
+ "- Adjust the cut above until it matches how you would actually split the work;",
883
+ '- create the sub-issues (`issue-flow generate "\u2026"`) and run them as a queue;',
884
+ "- or decide the issue is fine as it is and re-run it \u2014 nothing here blocks that,",
885
+ " beyond the report you are reading.",
886
+ ""
887
+ );
888
+ return `${lines.join("\n")}
889
+ `;
890
+ }
891
+
892
+ // src/core/headless.ts
893
+ import { readFile as readFile2 } from "fs/promises";
894
+ import chalk from "chalk";
895
+ var DEFAULT_HEADLESS_TIMEOUT_MS = 9e5;
896
+ async function escalatedTimeout(timeoutMs, history) {
897
+ if (timeoutMs === 0 || history === void 0) return timeoutMs;
898
+ const journal = (await Promise.all(
899
+ history.journalFiles.map(async (file) => {
900
+ try {
901
+ return await readFile2(file, "utf-8");
902
+ } catch {
903
+ return "";
904
+ }
905
+ })
906
+ )).join("");
907
+ const timeouts = timeoutsByPhase(journal).get(history.phase) ?? 0;
908
+ if (timeouts < DECOMPOSITION_THRESHOLDS.timeoutsPerPhase) return timeoutMs;
909
+ return Math.min(timeoutMs * 2, Number.MAX_SAFE_INTEGER);
910
+ }
911
+ function printAgentEvent(event, onOutput) {
912
+ const icons = getIcons();
913
+ const colored = useColor();
914
+ const emit = onOutput ?? ((msg) => process.stdout.write(`${msg}
915
+ `));
916
+ if (event.kind === "text") {
917
+ const lines = event.text.split("\n");
918
+ for (const textLine of lines) {
919
+ if (!textLine.trim()) continue;
920
+ const prefix2 = colored ? chalk.dim(` ${icons.connector} `) : ` ${icons.connector} `;
921
+ const text = colored ? chalk.dim(textLine) : textLine;
922
+ emit(`${prefix2}${text}`);
923
+ }
924
+ return;
925
+ }
926
+ getSessionPublisher().publish({
927
+ type: "activity",
928
+ at: isoNow(),
929
+ tool: event.name,
930
+ detail: event.detail
931
+ });
932
+ const toolName = event.name.padEnd(12);
933
+ const prefix = colored ? chalk.dim(` ${icons.connector} `) : ` ${icons.connector} `;
934
+ const toolIcon = colored ? chalk.cyan(icons.tool) : icons.tool;
935
+ const toolLabel = colored ? chalk.cyan(toolName) : toolName;
936
+ const contextText = event.detail ? colored ? chalk.dim(event.detail) : event.detail : "";
937
+ emit(`${prefix}${toolIcon} ${toolLabel} ${contextText}`);
938
+ }
939
+ async function invokeHeadlessAgent(invocation) {
940
+ const config = getActiveResilienceConfig();
941
+ if (config.providers?.failover !== true) {
942
+ return { run: (await invokeSelectedAgent(invocation)).run, retryExhausted: false };
943
+ }
944
+ let usage = null;
945
+ const outcome = await withRetry(() => invokeSelectedAgent(invocation), {
946
+ policy: (failure) => resolvePolicy(failure.kind, config),
947
+ signal: getShutdownSignal(),
948
+ evaluate: (selected) => selected.failure,
949
+ onAttempt: ({ value, failure, willRetry, delayMs, attempt }) => {
950
+ if (value.run.usage !== null) usage = sumUsage(usage, value.run.usage);
951
+ if (failure === null || !willRetry) return;
952
+ getSessionPublisher().publish({
953
+ type: "retry",
954
+ at: isoNow(),
955
+ attempt,
956
+ delaySeconds: delayMs / 1e3,
957
+ reason: failure.message,
958
+ kind: failure.kind
959
+ });
960
+ }
961
+ });
962
+ return {
963
+ run: usage === null ? outcome.value.run : { ...outcome.value.run, usage },
964
+ retryExhausted: outcome.exhausted
965
+ };
966
+ }
967
+ async function runHeadless(options) {
968
+ const {
969
+ prompt,
970
+ maxTurns = 10,
971
+ timeout: configuredTimeout = DEFAULT_HEADLESS_TIMEOUT_MS,
972
+ outputFormat = "json",
973
+ allowedTools,
974
+ addDirs,
975
+ statusMessage,
976
+ onOutput,
977
+ phase = "analyze",
978
+ permission = "workspace",
979
+ forceProvider,
980
+ purpose
981
+ } = options;
982
+ const timeout = await escalatedTimeout(configuredTimeout, options.timeoutHistory);
983
+ const settings = await resolveAgentFor(phase);
984
+ const verbose = isVerbose();
985
+ const effectiveOnOutput = onOutput ?? getOutputCallback();
986
+ if (verbose) {
987
+ const icons = getIcons();
988
+ const colored = useColor();
989
+ const emit = effectiveOnOutput ?? ((msg) => process.stdout.write(`${msg}
990
+ `));
991
+ if (statusMessage) {
992
+ const msg = colored ? chalk.blue(`${icons.start} ${statusMessage}`) : `${icons.start} ${statusMessage}`;
993
+ emit(msg);
994
+ }
995
+ const connectorLine = colored ? chalk.dim(` ${icons.connector}`) : ` ${icons.connector}`;
996
+ emit(connectorLine);
997
+ const startTime = Date.now();
998
+ const { run, retryExhausted } = await invokeHeadlessAgent({
999
+ prompt,
1000
+ phase,
1001
+ addDirs,
1002
+ timeout,
1003
+ permission,
1004
+ maxTurns,
1005
+ allowedTools,
1006
+ ...forceProvider === void 0 ? {} : { forceProvider },
1007
+ ...purpose === void 0 ? {} : { purpose },
1008
+ onEvent: (event) => printAgentEvent(event, effectiveOnOutput)
1009
+ });
1010
+ const elapsedSec = Math.floor((Date.now() - startTime) / 1e3);
1011
+ const durationStr = formatDuration(elapsedSec);
1012
+ const doneLine = colored ? chalk.dim(` ${icons.connector} ${chalk.italic(`Done in ${durationStr}`)}`) : ` ${icons.connector} Done in ${durationStr}`;
1013
+ emit(doneLine);
1014
+ emit(connectorLine);
1015
+ return mapHeadlessResult(run, outputFormat, retryExhausted);
1016
+ }
1017
+ const spinner = statusMessage ? createSpinner(statusMessage).start() : null;
1018
+ let timer = null;
1019
+ if (spinner) {
1020
+ timer = new ElapsedTimer((elapsed) => {
1021
+ spinner.suffixText = useColor() ? chalk.dim(`(${elapsed})`) : `(${elapsed})`;
1022
+ }).start();
1023
+ }
1024
+ try {
1025
+ const { run, retryExhausted } = await invokeHeadlessAgent({
1026
+ prompt,
1027
+ phase,
1028
+ addDirs,
1029
+ timeout,
1030
+ permission,
1031
+ maxTurns,
1032
+ allowedTools,
1033
+ inactivityTimeoutMs: getInactivityTimeout(),
1034
+ ...forceProvider === void 0 ? {} : { forceProvider },
1035
+ ...purpose === void 0 ? {} : { purpose },
1036
+ onEvent: (event) => {
1037
+ if (event.kind === "text") return;
1038
+ getSessionPublisher().publish({
1039
+ type: "activity",
1040
+ at: isoNow(),
1041
+ tool: event.name,
1042
+ detail: event.detail
1043
+ });
1044
+ getActivityCallback()?.({ tool: event.name, detail: event.detail });
1045
+ }
1046
+ });
1047
+ const elapsed = timer?.stop() ?? 0;
1048
+ const dur = useColor() ? chalk.dim(` (${formatDuration(elapsed)})`) : ` (${formatDuration(elapsed)})`;
1049
+ if (!run.success) {
1050
+ spinner?.fail(`${statusMessage}${dur}`);
1051
+ return mapHeadlessResult(run, outputFormat, retryExhausted);
1052
+ }
1053
+ spinner?.succeed(`${statusMessage}${dur}`);
1054
+ return mapHeadlessResult(run, outputFormat, retryExhausted);
1055
+ } catch (err) {
1056
+ const message = err instanceof Error ? err.message : String(err);
1057
+ const catchElapsed = timer?.stop() ?? 0;
1058
+ const catchDur = useColor() ? chalk.dim(` (${formatDuration(catchElapsed)})`) : ` (${formatDuration(catchElapsed)})`;
1059
+ spinner?.fail(`${statusMessage}${catchDur}`);
1060
+ return {
1061
+ success: false,
1062
+ result: "",
1063
+ cost: null,
1064
+ error: message,
1065
+ agent: { provider: settings.provider, model: settings.model }
1066
+ };
1067
+ }
1068
+ }
1069
+ function mapHeadlessResult(run, outputFormat, retryExhausted = false) {
1070
+ if (outputFormat === "text") {
1071
+ return {
1072
+ success: run.success,
1073
+ result: run.success ? run.rawOutput || run.result : "",
1074
+ cost: null,
1075
+ error: run.error,
1076
+ agent: run.agent,
1077
+ ...retryExhausted ? { retryExhausted: true } : {}
1078
+ };
1079
+ }
1080
+ if (!run.success) {
1081
+ return {
1082
+ success: false,
1083
+ result: "",
1084
+ cost: null,
1085
+ error: run.error,
1086
+ agent: run.agent,
1087
+ ...retryExhausted ? { retryExhausted: true } : {}
1088
+ };
1089
+ }
1090
+ return {
1091
+ success: true,
1092
+ result: run.result,
1093
+ cost: run.usage,
1094
+ error: null,
1095
+ agent: run.agent,
1096
+ ...retryExhausted ? { retryExhausted: true } : {}
1097
+ };
1098
+ }
1099
+
1100
+ export {
1101
+ evaluateCeilings,
1102
+ spendFromCost,
1103
+ resetAgentInvocationState,
1104
+ invokeSelectedAgent,
1105
+ assessDecomposition,
1106
+ proposeSubIssues,
1107
+ buildDecompositionReport,
1108
+ DEFAULT_HEADLESS_TIMEOUT_MS,
1109
+ runHeadless
1110
+ };
1111
+ //# sourceMappingURL=chunk-ARFVHC2F.js.map