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