@wrongstack/core 0.313.0 → 0.313.1

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 (182) hide show
  1. package/dist/chronicle/index.js +84 -6391
  2. package/dist/chronicle/project-server.js +84 -4868
  3. package/dist/chunk-2BUUIMQB.js +225 -0
  4. package/dist/chunk-2FK4WTFF.js +207 -0
  5. package/dist/chunk-2FQL4PDC.js +292 -0
  6. package/dist/chunk-2KKODIQU.js +2256 -0
  7. package/dist/chunk-2YU4WVBW.js +134 -0
  8. package/dist/chunk-3E244UDA.js +356 -0
  9. package/dist/chunk-3XPVB7BA.js +135 -0
  10. package/dist/chunk-4LJJYBAU.js +388 -0
  11. package/dist/chunk-4VWO3ZXL.js +342 -0
  12. package/dist/chunk-4XV6IR37.js +187 -0
  13. package/dist/chunk-4ZHTKJHM.js +183 -0
  14. package/dist/chunk-52RTVH4M.js +361 -0
  15. package/dist/chunk-57GEWK5X.js +26 -0
  16. package/dist/chunk-5DSMEVA4.js +931 -0
  17. package/dist/chunk-5RP3QTRL.js +1223 -0
  18. package/dist/chunk-5S437PRC.js +50 -0
  19. package/dist/chunk-5XDUGTKN.js +511 -0
  20. package/dist/chunk-62V2AFVG.js +422 -0
  21. package/dist/chunk-65RDIOR2.js +2470 -0
  22. package/dist/chunk-6AKOTGLM.js +597 -0
  23. package/dist/chunk-7ZHZE5EO.js +28 -0
  24. package/dist/chunk-A455ATZB.js +140 -0
  25. package/dist/chunk-AD7ZFZCS.js +46 -0
  26. package/dist/chunk-AI4LLQ2U.js +856 -0
  27. package/dist/chunk-ASX6AW36.js +789 -0
  28. package/dist/chunk-AT4V4XGI.js +3522 -0
  29. package/dist/chunk-AWNQ6ED6.js +311 -0
  30. package/dist/chunk-B4AMLZPB.js +9 -0
  31. package/dist/chunk-BAKVJLEV.js +417 -0
  32. package/dist/chunk-BJHVULHO.js +55 -0
  33. package/dist/chunk-CB2HFKTP.js +9 -0
  34. package/dist/chunk-CE4ZOCOI.js +1640 -0
  35. package/dist/chunk-CQQL4ZOQ.js +151 -0
  36. package/dist/chunk-DEIQMBD2.js +65 -0
  37. package/dist/chunk-E6I5RXVK.js +58 -0
  38. package/dist/chunk-EFEHDQGY.js +1589 -0
  39. package/dist/chunk-EODDVJBM.js +82 -0
  40. package/dist/chunk-EPC3GZD4.js +308 -0
  41. package/dist/chunk-FAYRX2AD.js +13 -0
  42. package/dist/chunk-FFSX2QDQ.js +362 -0
  43. package/dist/chunk-FHAMQS7Q.js +5856 -0
  44. package/dist/chunk-FIV4IJL7.js +2263 -0
  45. package/dist/chunk-FNXVT7HF.js +52 -0
  46. package/dist/chunk-FSWCWSTS.js +395 -0
  47. package/dist/chunk-FW5LDDMJ.js +16464 -0
  48. package/dist/chunk-FXQLIX3K.js +211 -0
  49. package/dist/chunk-G5AAKEEZ.js +228 -0
  50. package/dist/chunk-G5S3S5ZN.js +38 -0
  51. package/dist/chunk-GAZEWQEZ.js +231 -0
  52. package/dist/chunk-GFJOMSHG.js +125 -0
  53. package/dist/chunk-GFXZTHNI.js +158 -0
  54. package/dist/chunk-GM54GJBU.js +960 -0
  55. package/dist/chunk-GPQ3NJPR.js +900 -0
  56. package/dist/chunk-GTCU6DZ3.js +1 -0
  57. package/dist/chunk-H5E77KUJ.js +15 -0
  58. package/dist/chunk-H72H4XDU.js +1023 -0
  59. package/dist/chunk-HA4ZYA6T.js +178 -0
  60. package/dist/chunk-I45KAW2N.js +5020 -0
  61. package/dist/chunk-IQ35HMQL.js +1396 -0
  62. package/dist/chunk-IYFBFX34.js +86 -0
  63. package/dist/chunk-JJPMWPB6.js +109 -0
  64. package/dist/chunk-JTHKOYWU.js +663 -0
  65. package/dist/chunk-JVGPR3TS.js +365 -0
  66. package/dist/chunk-JVKB2TBL.js +48 -0
  67. package/dist/chunk-JX7VBZDK.js +31 -0
  68. package/dist/chunk-K3G6JBUC.js +5232 -0
  69. package/dist/chunk-K4YUNKU7.js +589 -0
  70. package/dist/chunk-KBZGE6LN.js +710 -0
  71. package/dist/chunk-KDQJCT2J.js +45 -0
  72. package/dist/chunk-KI4L2DUL.js +560 -0
  73. package/dist/chunk-KJ2ZTQSL.js +47 -0
  74. package/dist/chunk-KSJ5UTP7.js +154 -0
  75. package/dist/chunk-LHOA5DWA.js +28 -0
  76. package/dist/chunk-LX5QWLBL.js +27 -0
  77. package/dist/chunk-LZDDLNCZ.js +279 -0
  78. package/dist/chunk-M2WF6AXG.js +115 -0
  79. package/dist/chunk-M7BS2K6U.js +1 -0
  80. package/dist/chunk-NDBIPFNY.js +102 -0
  81. package/dist/chunk-NEMIWEXC.js +1199 -0
  82. package/dist/chunk-NFAYHPRH.js +323 -0
  83. package/dist/chunk-O37BQ65S.js +21 -0
  84. package/dist/chunk-O74MJHXU.js +187 -0
  85. package/dist/chunk-OJZEX73F.js +112 -0
  86. package/dist/chunk-PAAWLLZX.js +74 -0
  87. package/dist/chunk-PFXFQQRT.js +339 -0
  88. package/dist/chunk-PFZO3C6H.js +14 -0
  89. package/dist/chunk-PHH7LD45.js +113 -0
  90. package/dist/chunk-QRTG4XTI.js +188 -0
  91. package/dist/chunk-QVOVMXJR.js +1351 -0
  92. package/dist/chunk-RCQYT2I4.js +757 -0
  93. package/dist/chunk-RD6DEE36.js +1178 -0
  94. package/dist/chunk-RJIZEJ42.js +30 -0
  95. package/dist/chunk-RPOA7IAE.js +150 -0
  96. package/dist/chunk-SN2DQRL6.js +1172 -0
  97. package/dist/chunk-SQJVFEBV.js +41 -0
  98. package/dist/chunk-SQP64RCS.js +5391 -0
  99. package/dist/chunk-SVT4RNTR.js +49 -0
  100. package/dist/chunk-SWUFN7XM.js +43 -0
  101. package/dist/chunk-SXMVYC5H.js +174 -0
  102. package/dist/chunk-SZ6AYUIZ.js +23 -0
  103. package/dist/chunk-T3KGZBU5.js +3171 -0
  104. package/dist/chunk-TBRL2RCC.js +9 -0
  105. package/dist/chunk-TRT2HRWZ.js +18 -0
  106. package/dist/chunk-TYW2GI5I.js +1077 -0
  107. package/dist/chunk-UIWONWVG.js +4959 -0
  108. package/dist/chunk-VB3H5WWB.js +151 -0
  109. package/dist/chunk-W4A6QA3D.js +258 -0
  110. package/dist/chunk-WBPVKEEE.js +227 -0
  111. package/dist/chunk-WEBMSQP2.js +123 -0
  112. package/dist/chunk-WOEZV3Q4.js +333 -0
  113. package/dist/chunk-WQQON3SV.js +473 -0
  114. package/dist/chunk-WQTOVMGV.js +119 -0
  115. package/dist/chunk-WZO23AWO.js +2779 -0
  116. package/dist/chunk-XBDP4QPW.js +7 -0
  117. package/dist/chunk-XEBHSJLT.js +103 -0
  118. package/dist/chunk-XQ2ECNK3.js +1 -0
  119. package/dist/chunk-XQ2XGIBF.js +2086 -0
  120. package/dist/chunk-Y2GE5TSI.js +355 -0
  121. package/dist/chunk-YBUJCZ57.js +351 -0
  122. package/dist/chunk-YNCVSFGF.js +149 -0
  123. package/dist/chunk-YUPMXOGP.js +282 -0
  124. package/dist/chunk-YY6APYUM.js +49 -0
  125. package/dist/chunk-Z2IJUO45.js +691 -0
  126. package/dist/chunk-ZEHUCJHY.js +129 -0
  127. package/dist/chunk-ZYU2YOJ6.js +1496 -0
  128. package/dist/chunk-ZZ4V3WIZ.js +163 -0
  129. package/dist/coordination/agents/index.js +145 -6260
  130. package/dist/coordination/index.js +365 -34324
  131. package/dist/coordination/mailbox-project-server.js +48 -3276
  132. package/dist/coordination/provider-status-tracker.d.ts +4 -0
  133. package/dist/core/agent.d.ts +14 -1
  134. package/dist/core/index.js +267 -12480
  135. package/dist/core/model-ref.js +5 -23
  136. package/dist/core/provider-runner.d.ts +36 -1
  137. package/dist/design/index.js +50 -1087
  138. package/dist/execution/index.d.ts +1 -1
  139. package/dist/execution/index.js +1599 -21888
  140. package/dist/execution/prompt-enhancer.js +16 -535
  141. package/dist/execution/retry-policy.d.ts +31 -1
  142. package/dist/extension/index.js +7 -311
  143. package/dist/goal/index.js +33 -3435
  144. package/dist/hooks/index.js +14 -1138
  145. package/dist/hq/index.js +112 -5139
  146. package/dist/hq/protocol.js +21 -668
  147. package/dist/index.js +1410 -92656
  148. package/dist/infrastructure/index.js +64 -4811
  149. package/dist/kernel/events/provider-events.d.ts +15 -0
  150. package/dist/kernel/index.js +14 -992
  151. package/dist/models/index.js +40 -1117
  152. package/dist/notifications/index.js +3 -108
  153. package/dist/observability/index.js +17 -279
  154. package/dist/plugin/index.js +584 -14835
  155. package/dist/prompts/index.js +14 -948
  156. package/dist/registry/index.js +9 -822
  157. package/dist/registry-R23UBE7H.js +14 -0
  158. package/dist/replay/index.js +6 -84
  159. package/dist/review-finding-store-DKAV3AS6.js +21 -0
  160. package/dist/review-report-store-OWY3EC3N.js +19 -0
  161. package/dist/security/index.js +114 -2202
  162. package/dist/session-catalog/index.js +36 -2103
  163. package/dist/session-catalog/project-server.js +42 -1435
  164. package/dist/skills/index.js +35 -1578
  165. package/dist/storage/index.js +182 -15600
  166. package/dist/tasking/index.js +10 -676
  167. package/dist/tools/index.js +52 -9590
  168. package/dist/types/config/autonomy.d.ts +5 -0
  169. package/dist/types/config/ui.d.ts +1 -1
  170. package/dist/types/errors.d.ts +18 -0
  171. package/dist/types/index.js +121 -1342
  172. package/dist/types/provider-runner.d.ts +8 -0
  173. package/dist/types/session-markers.js +8 -144
  174. package/dist/utils/error.js +3 -4
  175. package/dist/utils/expect-defined.js +3 -9
  176. package/dist/utils/heap-watchdog.js +6 -706
  177. package/dist/utils/index.js +269 -5685
  178. package/dist/utils/sage-output-block.js +5 -42
  179. package/dist/wiring/proxy-rewrite.d.ts +18 -0
  180. package/dist/wiring/proxy-rewrite.js +18 -121
  181. package/dist/worktree/index.js +6 -887
  182. package/package.json +4 -4
@@ -0,0 +1,2086 @@
1
+ import {
2
+ DefaultTaskStore,
3
+ TaskTracker
4
+ } from "./chunk-K4YUNKU7.js";
5
+ import {
6
+ noOpLogger
7
+ } from "./chunk-WBPVKEEE.js";
8
+ import {
9
+ readBundledInstructionText,
10
+ renderInstructionTemplate
11
+ } from "./chunk-5S437PRC.js";
12
+ import {
13
+ atomicWrite,
14
+ withFileLock
15
+ } from "./chunk-7ZHZE5EO.js";
16
+ import {
17
+ toErrorMessage
18
+ } from "./chunk-TBRL2RCC.js";
19
+
20
+ // src/goal/phase-graph-builder.ts
21
+ var PhaseGraphBuilder = class _PhaseGraphBuilder {
22
+ constructor(opts) {
23
+ this.opts = opts;
24
+ }
25
+ opts;
26
+ async build() {
27
+ const graphId = crypto.randomUUID();
28
+ const phases = /* @__PURE__ */ new Map();
29
+ const phaseIds = [];
30
+ for (let i = 0; i < this.opts.phases.length; i++) {
31
+ const tmpl = this.opts.phases[i] ?? { name: "", description: "", tasks: [], taskTemplates: [], parallelizable: false, priority: "medium", estimateHours: 0 };
32
+ const phaseId = crypto.randomUUID();
33
+ phaseIds.push(phaseId);
34
+ const store = this.opts.externalTaskStore ?? new DefaultTaskStore();
35
+ const tracker = new TaskTracker({ store });
36
+ const taskGraph = await tracker.createGraph(phaseId, `${tmpl.name} Tasks`);
37
+ if (tmpl.taskTemplates && tmpl.taskTemplates.length > 0) {
38
+ for (const tt of tmpl.taskTemplates) {
39
+ tracker.addNode({
40
+ title: tt.title,
41
+ description: tt.description,
42
+ type: tt.type,
43
+ priority: tt.priority,
44
+ status: "pending",
45
+ estimateHours: tt.estimateHours,
46
+ tags: tt.tags ?? []
47
+ });
48
+ }
49
+ }
50
+ const phase = {
51
+ id: phaseId,
52
+ name: tmpl.name,
53
+ description: tmpl.description,
54
+ status: "pending",
55
+ taskGraph,
56
+ dependsOn: i > 0 ? [phaseIds[i - 1] ?? ""] : [],
57
+ nextPhases: i < this.opts.phases.length - 1 ? [phaseIds[i + 1] ?? ""] : [],
58
+ parallelizable: tmpl.parallelizable,
59
+ priority: tmpl.priority,
60
+ estimateHours: tmpl.estimateHours,
61
+ assignedAgents: [],
62
+ createdAt: Date.now(),
63
+ updatedAt: Date.now()
64
+ };
65
+ phases.set(phaseId, phase);
66
+ }
67
+ const phaseArray = Array.from(phases.values());
68
+ for (let i = 0; i < phaseArray.length; i++) {
69
+ const phase = phaseArray[i];
70
+ if (!phase) continue;
71
+ phase.nextPhases = i < phaseArray.length - 1 ? [phaseArray[i + 1]?.id ?? ""] : [];
72
+ }
73
+ const graph = {
74
+ id: graphId,
75
+ title: this.opts.title,
76
+ description: this.opts.description ?? "",
77
+ phases,
78
+ rootPhaseIds: phaseIds.length > 0 ? [phaseIds[0] ?? ""] : [],
79
+ activePhaseIds: [],
80
+ completedPhaseIds: [],
81
+ failedPhaseIds: [],
82
+ autonomous: this.opts.autonomous ?? true,
83
+ stopOnComplete: true,
84
+ multiBoard: this.opts.multiBoard,
85
+ verifyTasks: this.opts.verifyTasks,
86
+ chimeraReview: this.opts.chimeraReview,
87
+ createdAt: Date.now(),
88
+ updatedAt: Date.now()
89
+ };
90
+ return graph;
91
+ }
92
+ /**
93
+ * Create phases from an existing TaskGraph.
94
+ * Groups tasks into phases using topological sort so dependencies
95
+ * stay in the same phase wherever possible.
96
+ */
97
+ static fromTaskGraph(taskGraph, options) {
98
+ const tasksPerPhase = options.tasksPerPhase ?? 5;
99
+ const nodes = Array.from(taskGraph.nodes.values());
100
+ const dependents = /* @__PURE__ */ new Map();
101
+ for (const edge of taskGraph.edges) {
102
+ const list = dependents.get(edge.from) ?? [];
103
+ list.push(edge.to);
104
+ dependents.set(edge.from, list);
105
+ }
106
+ const inDegree = /* @__PURE__ */ new Map();
107
+ for (const node of nodes) inDegree.set(node.id, 0);
108
+ for (const edge of taskGraph.edges) {
109
+ inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1);
110
+ }
111
+ const queue = [];
112
+ for (const [id, degree] of inDegree) {
113
+ if (degree === 0) queue.push(id);
114
+ }
115
+ const prioOrder = {
116
+ critical: 0,
117
+ high: 1,
118
+ medium: 2,
119
+ low: 3
120
+ };
121
+ const topoSorted = [];
122
+ while (queue.length > 0) {
123
+ queue.sort((a, b) => {
124
+ const na = taskGraph.nodes.get(a);
125
+ const nb = taskGraph.nodes.get(b);
126
+ const naPrio = na ? prioOrder[na.priority] : 4;
127
+ const nbPrio = nb ? prioOrder[nb.priority] : 4;
128
+ return naPrio - nbPrio;
129
+ });
130
+ const id = queue.shift();
131
+ topoSorted.push(id);
132
+ for (const depId of dependents.get(id) ?? []) {
133
+ const deg = (inDegree.get(depId) ?? 1) - 1;
134
+ inDegree.set(depId, deg);
135
+ if (deg === 0) queue.push(depId);
136
+ }
137
+ }
138
+ const groups = [];
139
+ let currentGroup = [];
140
+ const assigned = /* @__PURE__ */ new Set();
141
+ for (const id of topoSorted) {
142
+ if (assigned.has(id)) continue;
143
+ assigned.add(id);
144
+ currentGroup.push(id);
145
+ const deps = dependents.get(id) ?? [];
146
+ for (const depId of deps) {
147
+ if (!assigned.has(depId) && currentGroup.length < tasksPerPhase) {
148
+ assigned.add(depId);
149
+ currentGroup.push(depId);
150
+ }
151
+ }
152
+ if (currentGroup.length >= tasksPerPhase) {
153
+ groups.push(currentGroup);
154
+ currentGroup = [];
155
+ }
156
+ }
157
+ if (currentGroup.length > 0) groups.push(currentGroup);
158
+ const phaseTemplates = groups.map((groupIds, idx) => {
159
+ const group = groupIds.map((id) => taskGraph.nodes.get(id)).filter((n) => n != null);
160
+ const hasCritical = group.some((t) => t.priority === "critical");
161
+ const totalHours = group.reduce((sum, t) => sum + (t.estimateHours ?? 2), 0);
162
+ return {
163
+ name: `Phase ${idx + 1}: ${group[0]?.title.slice(0, 30) ?? "Tasks"}`,
164
+ description: group.map((t) => t.title).join(", "),
165
+ priority: hasCritical ? "critical" : "high",
166
+ estimateHours: totalHours,
167
+ parallelizable: false,
168
+ taskTemplates: group.map((t) => ({
169
+ title: t.title,
170
+ description: t.description,
171
+ type: t.type,
172
+ priority: t.priority,
173
+ estimateHours: t.estimateHours ?? 2,
174
+ tags: t.tags ?? []
175
+ }))
176
+ };
177
+ });
178
+ const builder = new _PhaseGraphBuilder({
179
+ ...options,
180
+ phases: phaseTemplates
181
+ });
182
+ return builder.build();
183
+ }
184
+ };
185
+
186
+ // src/goal/phase-board-mutations.ts
187
+ function findPhaseOfTaskInGraph(graph, taskId) {
188
+ for (const phase of graph.phases.values()) {
189
+ if (phase.taskGraph.nodes.has(taskId)) return phase;
190
+ }
191
+ return void 0;
192
+ }
193
+ function movePhaseTask(ctx, taskId, toPhaseId) {
194
+ const from = findPhaseOfTaskInGraph(ctx.graph, taskId);
195
+ const to = ctx.graph.phases.get(toPhaseId);
196
+ if (!from || !to || from.id === toPhaseId) return false;
197
+ const node = from.taskGraph.nodes.get(taskId);
198
+ if (!node) return false;
199
+ const dependents = from.taskGraph.edges.filter((e) => e.from === taskId).map((e) => e.to);
200
+ for (const depId of dependents) {
201
+ const depNode = from.taskGraph.nodes.get(depId);
202
+ if (depNode) {
203
+ depNode.parentId = void 0;
204
+ depNode.children = void 0;
205
+ }
206
+ from.taskGraph.edges = from.taskGraph.edges.filter(
207
+ (e) => !(e.from === taskId && e.to === depId)
208
+ );
209
+ if (!from.taskGraph.rootNodes.includes(depId)) {
210
+ from.taskGraph.rootNodes.push(depId);
211
+ }
212
+ }
213
+ from.taskGraph.nodes.delete(taskId);
214
+ from.taskGraph.rootNodes = from.taskGraph.rootNodes.filter((id) => id !== taskId);
215
+ from.taskGraph.edges = from.taskGraph.edges.filter((e) => e.from !== taskId && e.to !== taskId);
216
+ from.taskGraph.updatedAt = Date.now();
217
+ node.parentId = void 0;
218
+ node.children = void 0;
219
+ node.updatedAt = Date.now();
220
+ to.taskGraph.nodes.set(taskId, node);
221
+ to.taskGraph.rootNodes.push(taskId);
222
+ to.taskGraph.updatedAt = Date.now();
223
+ ctx.trackerCache.delete(from.id);
224
+ ctx.trackerCache.delete(to.id);
225
+ ctx.graph.updatedAt = Date.now();
226
+ ctx.emit("phase.taskMoved", { taskId, fromPhaseId: from.id, toPhaseId });
227
+ return true;
228
+ }
229
+ function setPhaseTaskAssignee(ctx, taskId, agentId, agentName) {
230
+ const phase = findPhaseOfTaskInGraph(ctx.graph, taskId);
231
+ if (!phase) return false;
232
+ const tracker = ctx.getTrackerForPhase(phase);
233
+ tracker.updateNode(taskId, { assignee: agentName ?? agentId ?? "" });
234
+ ctx.graph.updatedAt = Date.now();
235
+ ctx.emit("phase.taskAssigned", {
236
+ phaseId: phase.id,
237
+ taskId,
238
+ ...agentId !== void 0 ? { agentId } : {},
239
+ ...agentName !== void 0 ? { agentName } : {}
240
+ });
241
+ return true;
242
+ }
243
+ function addPhaseTask(ctx, phaseId, spec) {
244
+ const phase = ctx.graph.phases.get(phaseId);
245
+ if (!phase) return null;
246
+ const tracker = ctx.getTrackerForPhase(phase);
247
+ const node = tracker.addNode({
248
+ title: spec.title,
249
+ description: spec.description ?? "",
250
+ type: spec.type ?? "feature",
251
+ priority: spec.priority ?? "medium",
252
+ status: "pending"
253
+ });
254
+ ctx.graph.updatedAt = Date.now();
255
+ ctx.emit("phase.taskAdded", { phaseId, taskId: node.id, taskTitle: node.title });
256
+ return node.id;
257
+ }
258
+ function requeuePhaseTask(ctx, taskId) {
259
+ const phase = findPhaseOfTaskInGraph(ctx.graph, taskId);
260
+ if (!phase) return false;
261
+ const tracker = ctx.getTrackerForPhase(phase);
262
+ tracker.updateNodeStatus(taskId, "pending");
263
+ ctx.taskRetryCounts.delete(`${phase.id}:${taskId}`);
264
+ if (phase.status === "completed" || phase.status === "failed" || phase.status === "paused") {
265
+ ctx.graph.failedPhaseIds = ctx.graph.failedPhaseIds.filter((id) => id !== phase.id);
266
+ ctx.graph.completedPhaseIds = ctx.graph.completedPhaseIds.filter((id) => id !== phase.id);
267
+ ctx.graph.activePhaseIds = ctx.graph.activePhaseIds.filter((id) => id !== phase.id);
268
+ ctx.phaseWorktrees.delete(phase.id);
269
+ ctx.updatePhaseStatus(phase, "pending");
270
+ }
271
+ ctx.graph.updatedAt = Date.now();
272
+ return true;
273
+ }
274
+
275
+ // src/goal/phase-orchestrator-queries.ts
276
+ function getReadyPhases(graph) {
277
+ const ready = [];
278
+ for (const phase of graph.phases.values()) {
279
+ if (phase.status !== "pending") continue;
280
+ const depsDone = phase.dependsOn.every((depId) => {
281
+ const dep = graph.phases.get(depId);
282
+ return dep?.status === "completed" || dep?.status === "skipped";
283
+ });
284
+ if (depsDone) {
285
+ ready.push(phase);
286
+ }
287
+ }
288
+ const prioOrder = { critical: 0, high: 1, medium: 2, low: 3 };
289
+ ready.sort((a, b) => (prioOrder[a.priority] ?? 4) - (prioOrder[b.priority] ?? 4));
290
+ return ready;
291
+ }
292
+ function getActivePhases(graph) {
293
+ return Array.from(graph.phases.values()).filter((p) => p.status === "running");
294
+ }
295
+ function getExecutableTasks(trackerFor, phase) {
296
+ const tracker = trackerFor(phase);
297
+ return tracker.getAllNodes({ status: ["pending", "blocked"] }).filter(
298
+ (n) => n.status === "pending" && tracker.canStart(n.id)
299
+ ).sort((a, b) => {
300
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
301
+ return (priorityOrder[a.priority] ?? 4) - (priorityOrder[b.priority] ?? 4);
302
+ });
303
+ }
304
+ function getFailedTaskCount(trackerFor, phase) {
305
+ return trackerFor(phase).getAllNodes({ status: ["failed"] }).length;
306
+ }
307
+ function getCompletedTaskCount(trackerFor, phase) {
308
+ return trackerFor(phase).getAllNodes({ status: ["completed"] }).length;
309
+ }
310
+ function isGraphComplete(graph) {
311
+ const allPhases = Array.from(graph.phases.values());
312
+ return allPhases.every((p) => p.status === "completed" || p.status === "skipped");
313
+ }
314
+ function getProgress(graph, trackerFor) {
315
+ const phases = Array.from(graph.phases.values());
316
+ let pending = 0;
317
+ let ready = 0;
318
+ let running = 0;
319
+ let paused = 0;
320
+ let completed = 0;
321
+ let failed = 0;
322
+ let skipped = 0;
323
+ let totalTasks = 0;
324
+ let completedTasks = 0;
325
+ let failedTasks = 0;
326
+ let estimatedHours = 0;
327
+ let actualHours = 0;
328
+ for (const p of phases) {
329
+ switch (p.status) {
330
+ case "pending":
331
+ pending++;
332
+ break;
333
+ case "ready":
334
+ ready++;
335
+ break;
336
+ case "running":
337
+ running++;
338
+ break;
339
+ case "paused":
340
+ paused++;
341
+ break;
342
+ case "completed":
343
+ completed++;
344
+ break;
345
+ case "failed":
346
+ failed++;
347
+ break;
348
+ case "skipped":
349
+ skipped++;
350
+ break;
351
+ default:
352
+ break;
353
+ }
354
+ estimatedHours += p.estimateHours;
355
+ if (p.actualDurationMs) actualHours += p.actualDurationMs / 36e5;
356
+ const tracker = trackerFor(p);
357
+ const progress = tracker.getProgress();
358
+ totalTasks += progress.total;
359
+ completedTasks += progress.completed;
360
+ failedTasks += progress.failed;
361
+ }
362
+ const totalPhases = phases.length;
363
+ const done = completed + skipped;
364
+ return {
365
+ totalPhases,
366
+ pending,
367
+ ready,
368
+ running,
369
+ paused,
370
+ completed,
371
+ failed,
372
+ skipped,
373
+ percentComplete: totalPhases > 0 ? Math.min(100, Math.round(done / totalPhases * 100)) : 0,
374
+ totalTasks,
375
+ completedTasks,
376
+ failedTasks,
377
+ estimatedHours,
378
+ actualHours
379
+ };
380
+ }
381
+ function setIntegrationMetadata(graph, phase, status, details = {}) {
382
+ phase.metadata = {
383
+ ...phase.metadata,
384
+ integrationStatus: status,
385
+ integrationBranch: details.branch,
386
+ integrationWorktreeDir: details.worktreeDir,
387
+ integrationConflictFiles: details.conflictFiles,
388
+ integrationError: details.error,
389
+ integrationUpdatedAt: Date.now()
390
+ };
391
+ phase.updatedAt = Date.now();
392
+ graph.updatedAt = Date.now();
393
+ }
394
+ function truncate(text, max = 500) {
395
+ const t = text.trim();
396
+ return t.length <= max ? t : `${t.slice(0, max)}\u2026 (+${t.length - max} chars)`;
397
+ }
398
+
399
+ // src/goal/phase-orchestrator-integration.ts
400
+ function worktreeEnv(int, phase) {
401
+ const handle = int.phaseWorktrees.get(phase.id);
402
+ return handle ? { cwd: handle.dir, branch: handle.branch } : void 0;
403
+ }
404
+ async function failPhaseAfterTasks(int, phase, error) {
405
+ int.updatePhaseStatus(phase, "failed");
406
+ phase.completedAt = Date.now();
407
+ phase.actualDurationMs = Date.now() - (phase.startedAt ?? Date.now());
408
+ int.runningPhases.delete(phase.id);
409
+ int.graph.activePhaseIds = int.graph.activePhaseIds.filter((id) => id !== phase.id);
410
+ if (!int.graph.failedPhaseIds.includes(phase.id)) int.graph.failedPhaseIds.push(phase.id);
411
+ int.emit("phase.failed", { phaseId: phase.id, name: phase.name, error });
412
+ int.ctx.onPhaseFail?.(phase, new Error(error));
413
+ await keepWorktreeForReview(int, phase);
414
+ }
415
+ function markPhaseMergeFailed(int, phase, error) {
416
+ if (phase.status !== "failed") {
417
+ int.graph.completedPhaseIds = int.graph.completedPhaseIds.filter((id) => id !== phase.id);
418
+ int.updatePhaseStatus(phase, "failed");
419
+ }
420
+ if (!int.graph.failedPhaseIds.includes(phase.id)) int.graph.failedPhaseIds.push(phase.id);
421
+ int.emit("phase.failed", { phaseId: phase.id, name: phase.name, error });
422
+ int.ctx.onPhaseFail?.(phase, new Error(error));
423
+ }
424
+ async function commitAndEnqueueMerge(int, phase) {
425
+ const handle = int.phaseWorktrees.get(phase.id);
426
+ if (!int.worktrees || !handle) return;
427
+ try {
428
+ await int.worktrees.commitAll(handle, `goal(${phase.name}): ${phase.id}`);
429
+ } catch {
430
+ }
431
+ const depPromises = phase.dependsOn.map((d) => int.phaseMergePromise.get(d)).filter((p) => Boolean(p));
432
+ const merged = (async () => {
433
+ await Promise.allSettled(depPromises);
434
+ int.setMergeQueue(
435
+ int.getMergeQueue().then(() => mergeOne(int, phase, handle)).catch((err) => {
436
+ const msg = toErrorMessage(err);
437
+ int.logger.error(msg, { event: "orchestrator.merge_failed", phaseId: phase.id });
438
+ markPhaseMergeFailed(int, phase, msg);
439
+ })
440
+ );
441
+ await int.getMergeQueue();
442
+ })();
443
+ int.phaseMergePromise.set(phase.id, merged);
444
+ }
445
+ async function mergeOne(int, phase, handle) {
446
+ if (!int.worktrees) return;
447
+ try {
448
+ const resolve2 = int.ctx.resolveConflict ? async (info) => {
449
+ const shouldResolve = await shouldAttemptConflictResolution(int, phase, info);
450
+ if (!shouldResolve) return false;
451
+ int.emit("phase.conflictResolving", {
452
+ phaseId: phase.id,
453
+ name: phase.name,
454
+ files: info.conflictFiles
455
+ });
456
+ const resolved = await int.ctx.resolveConflict?.(phase, info);
457
+ return resolved ?? false;
458
+ } : void 0;
459
+ const mergeOpts = {
460
+ squash: true
461
+ };
462
+ if (resolve2 !== void 0) mergeOpts.resolve = resolve2;
463
+ const result = await int.worktrees.merge(handle, mergeOpts);
464
+ if (result.resolved) {
465
+ int.emit("phase.conflictResolved", { phaseId: phase.id, name: phase.name });
466
+ }
467
+ setIntegrationMetadata(int.graph, phase, result.ok ? "merged" : "needs_review", {
468
+ branch: handle.branch,
469
+ worktreeDir: handle.dir,
470
+ conflictFiles: result.conflictFiles
471
+ });
472
+ await int.worktrees.release(handle, { keep: !result.ok });
473
+ } catch (err) {
474
+ setIntegrationMetadata(int.graph, phase, "merge_failed", {
475
+ branch: handle.branch,
476
+ worktreeDir: handle.dir,
477
+ error: toErrorMessage(err)
478
+ });
479
+ markPhaseMergeFailed(int, phase, `worktree merge failed: ${toErrorMessage(err)}`);
480
+ }
481
+ }
482
+ async function shouldAttemptConflictResolution(int, phase, info) {
483
+ if (!int.ctx.brain) return true;
484
+ const decision = await int.ctx.brain.decide({
485
+ id: `goal-conflict-${phase.id}`,
486
+ source: "goal",
487
+ question: `Should Goal try to resolve merge conflicts for phase "${phase.name}" automatically?`,
488
+ context: [
489
+ `Phase id: ${phase.id}`,
490
+ `Conflicted files: ${info.conflictFiles.join(", ") || "(unknown)"}`,
491
+ `Base working tree: ${info.cwd}`
492
+ ].join("\n"),
493
+ // 'critical', not 'high': choosing `resolve` lets a resolver agent EDIT
494
+ // FILES IN THE BASE WORKING TREE. That is the one Brain decision in the
495
+ // system whose wrong answer is not recoverable by simply continuing —
496
+ // unlike a budget grant (bounded, capped) or a goal-done verdict (the
497
+ // run can be restarted).
498
+ //
499
+ // Consequences of the level, both intended:
500
+ // - it is at or above the council floor, so a multi-LLM panel decides
501
+ // it whenever one can convene;
502
+ // - with no council (a single-model install resolves the autonomy
503
+ // ceiling to 'high'), it exceeds the ceiling and never reaches an
504
+ // unchecked single model. The policy tier's `ask_human` then routes
505
+ // to the terminal policy, which refuses a recommended option at
506
+ // critical risk and denies — i.e. the worktree is kept for human
507
+ // review, which is exactly the safe outcome.
508
+ risk: "critical",
509
+ fallback: "ask_human",
510
+ options: [
511
+ {
512
+ id: "resolve",
513
+ label: "Try the configured conflict resolver",
514
+ consequence: "A resolver agent may edit conflicted files in the base working tree.",
515
+ risk: "critical"
516
+ },
517
+ {
518
+ id: "review",
519
+ label: "Keep the worktree for human review",
520
+ consequence: "No automatic conflict resolution is attempted.",
521
+ risk: "low",
522
+ recommended: true
523
+ }
524
+ ]
525
+ });
526
+ phase.metadata = {
527
+ ...phase.metadata,
528
+ brainConflictDecision: decision.type,
529
+ brainConflictDecisionAt: Date.now()
530
+ };
531
+ if (decision.type !== "answer") return false;
532
+ return decision.optionId === "resolve";
533
+ }
534
+ async function keepWorktreeForReview(int, phase) {
535
+ const handle = int.phaseWorktrees.get(phase.id);
536
+ if (!int.worktrees || !handle) return;
537
+ try {
538
+ await int.worktrees.commitAll(handle, `goal(${phase.name}) [failed]: ${phase.id}`);
539
+ } catch {
540
+ }
541
+ setIntegrationMetadata(int.graph, phase, "not_merged_failed_phase", {
542
+ branch: handle.branch,
543
+ worktreeDir: handle.dir
544
+ });
545
+ await int.worktrees.release(handle, { keep: true }).catch(() => {
546
+ });
547
+ }
548
+
549
+ // src/goal/phase-orchestrator-runtime.ts
550
+ function normalizePhaseGraphForResume(graph) {
551
+ graph.activePhaseIds = [];
552
+ for (const phase of graph.phases.values()) {
553
+ if (phase.status === "running") {
554
+ phase.status = "pending";
555
+ phase.updatedAt = Date.now();
556
+ }
557
+ if (phase.status === "pending") {
558
+ for (const task of phase.taskGraph.nodes.values()) {
559
+ if (task.status === "in_progress") task.status = "pending";
560
+ }
561
+ }
562
+ }
563
+ }
564
+ function createNoopEventBus() {
565
+ return {
566
+ emit: () => {
567
+ },
568
+ on: () => () => {
569
+ },
570
+ off: () => {
571
+ },
572
+ once: () => () => {
573
+ },
574
+ listeners: /* @__PURE__ */ new Map(),
575
+ wildcards: [],
576
+ setLogger: () => {
577
+ },
578
+ onAny: () => () => {
579
+ },
580
+ offAny: () => {
581
+ },
582
+ emitAsync: async () => [],
583
+ waitFor: async () => {
584
+ }
585
+ };
586
+ }
587
+
588
+ // src/goal/phase-orchestrator.ts
589
+ var PhaseOrchestrator = class {
590
+ graph;
591
+ ctx;
592
+ opts;
593
+ events;
594
+ stopped = false;
595
+ paused = false;
596
+ /**
597
+ * Run-wide abort source. stop() aborts it; every in-flight
598
+ * ctx.executeTask call observes the abort through its per-task signal
599
+ * (composed from this controller and the task's own timeout controller).
600
+ * Recreated by start() so a stopped orchestrator can be reused.
601
+ */
602
+ stopController = new AbortController();
603
+ runningPhases = /* @__PURE__ */ new Set();
604
+ tickInterval = null;
605
+ trackerCache = /* @__PURE__ */ new Map();
606
+ taskRetryCounts = /* @__PURE__ */ new Map();
607
+ // ── Git-worktree isolation (optional) ──────────────────────────────────────
608
+ worktrees;
609
+ logger;
610
+ /** Per-phase worktree handles, keyed by phase id. */
611
+ phaseWorktrees = /* @__PURE__ */ new Map();
612
+ /** Serializes all merges back to the base branch (one at a time). */
613
+ mergeQueue = Promise.resolve();
614
+ /** Per-phase merge promise, so a phase merges only after its deps do. */
615
+ phaseMergePromise = /* @__PURE__ */ new Map();
616
+ constructor(opts) {
617
+ this.graph = opts.graph;
618
+ this.ctx = opts.ctx;
619
+ this.events = opts.events ?? createNoopEventBus();
620
+ this.worktrees = opts.worktrees;
621
+ this.logger = opts.logger ?? noOpLogger;
622
+ this.opts = {
623
+ maxConcurrentPhases: opts.maxConcurrentPhases ?? 1,
624
+ maxConcurrentTasks: opts.maxConcurrentTasks ?? 2,
625
+ maxRetries: opts.maxRetries ?? 2,
626
+ maxVerifyAttempts: opts.maxVerifyAttempts ?? 2,
627
+ autonomous: opts.autonomous ?? true,
628
+ phaseDelayMs: opts.phaseDelayMs ?? 0,
629
+ // Autonomous goals must fail closed. Continuing after a terminal task or
630
+ // gate failure can make dependent phases look complete without their
631
+ // required evidence.
632
+ stopOnFailure: opts.stopOnFailure ?? true,
633
+ taskTimeoutMs: opts.taskTimeoutMs ?? 6e5,
634
+ events: this.events
635
+ };
636
+ }
637
+ // ─── Lifecycle ────────────────────────────────────────────────────────────
638
+ /**
639
+ * Start the full phase flow.
640
+ * In autonomous mode, starts root phases and automatically starts the next phase when they finish.
641
+ */
642
+ async start() {
643
+ this.stopped = false;
644
+ this.paused = false;
645
+ this.stopController = new AbortController();
646
+ this.normalizeForResume();
647
+ this.graph.startedAt = Date.now();
648
+ this.graph.updatedAt = Date.now();
649
+ let readyPhases = this.getReadyPhases();
650
+ while (readyPhases.length > 0 && !this.stopped) {
651
+ await this.waitWhilePaused();
652
+ if (this.stopped) break;
653
+ const batch = readyPhases.slice(0, this.opts.maxConcurrentPhases);
654
+ await Promise.all(batch.map((p) => this.startPhase(p)));
655
+ if (this.opts.phaseDelayMs > 0) {
656
+ await this.delay(this.opts.phaseDelayMs);
657
+ }
658
+ await this.waitWhilePaused();
659
+ if (this.stopped) break;
660
+ readyPhases = this.getReadyPhases().filter(
661
+ (p) => !this.runningPhases.has(p.id) && p.status !== "completed" && p.status !== "failed"
662
+ );
663
+ }
664
+ await this.drainMerges();
665
+ if (this.opts.autonomous) {
666
+ this.tickInterval = setInterval(() => this.tick(), 1e3);
667
+ }
668
+ }
669
+ /**
670
+ * Make a (possibly resumed) graph runnable. A graph loaded from disk after an
671
+ * interrupted run can carry transient state from the dead process: phases left
672
+ * `running` and tasks left `in_progress`. The scheduler only starts `pending`
673
+ * phases (getReadyPhases) and only runs `pending` tasks (getExecutableTasks),
674
+ * so without this a resumed phase/task would stall forever. Reset that
675
+ * transient state to `pending`; terminal phases (completed/failed/skipped) and
676
+ * already-completed tasks are untouched, so completed work is never re-run.
677
+ * For a freshly built graph this is a no-op.
678
+ */
679
+ normalizeForResume() {
680
+ normalizePhaseGraphForResume(this.graph);
681
+ }
682
+ /** Wait for all pending phase merges, dependency-ordered and globally serialized. */
683
+ async drainMerges() {
684
+ await Promise.allSettled([...this.phaseMergePromise.values()]);
685
+ await this.mergeQueue.catch((err) => {
686
+ const msg = toErrorMessage(err);
687
+ this.logger.warn(msg, { event: "orchestrator.merge_queue_failed" });
688
+ });
689
+ }
690
+ /** Pause: active phases continue, but no new phase starts. */
691
+ pause() {
692
+ this.paused = true;
693
+ }
694
+ /** Resume: new phases may start again. */
695
+ resume() {
696
+ this.paused = false;
697
+ this.tick().catch((err) => {
698
+ const msg = toErrorMessage(err);
699
+ this.logger.error(msg, { event: "orchestrator.tick_failed" });
700
+ });
701
+ }
702
+ /** Stop completely, including active phases. */
703
+ stop() {
704
+ this.stopped = true;
705
+ this.stopController.abort();
706
+ if (this.tickInterval) {
707
+ clearInterval(this.tickInterval);
708
+ this.tickInterval = null;
709
+ }
710
+ for (const phaseId of this.runningPhases) {
711
+ const phase = this.graph.phases.get(phaseId);
712
+ if (phase) {
713
+ this.updatePhaseStatus(phase, "paused");
714
+ }
715
+ }
716
+ this.trackerCache.clear();
717
+ this.taskRetryCounts.clear();
718
+ if (this.worktrees) {
719
+ for (const handle of this.worktrees.list()) {
720
+ void this.worktrees.release(handle, { keep: true }).catch(() => {
721
+ });
722
+ }
723
+ }
724
+ }
725
+ // ─── Tick Loop (Autonomous) ───────────────────────────────────────────────
726
+ async tick() {
727
+ if (this.stopped || this.paused) return;
728
+ const active = this.getActivePhases();
729
+ const queued = this.getReadyPhases();
730
+ this.emit("autonomous.tick", {
731
+ activePhases: active.map((p) => p.id),
732
+ queuedPhases: queued.map((p) => p.id)
733
+ });
734
+ this.ctx.onTick?.({ activePhases: active, readyPhases: queued });
735
+ const availableSlots = this.opts.maxConcurrentPhases - active.length;
736
+ if (availableSlots > 0 && queued.length > 0) {
737
+ for (const phase of queued.slice(0, availableSlots)) {
738
+ if (phase.status === "pending") {
739
+ await this.startPhase(phase);
740
+ }
741
+ }
742
+ }
743
+ if (this.isComplete()) {
744
+ this.onGraphComplete();
745
+ return;
746
+ }
747
+ if (this.opts.stopOnFailure && this.graph.failedPhaseIds.length > 0) {
748
+ const failedPhase = this.graph.phases.get(this.graph.failedPhaseIds[0] ?? "");
749
+ if (failedPhase) {
750
+ this.onGraphFailed(failedPhase);
751
+ }
752
+ return;
753
+ }
754
+ }
755
+ // ─── Phase Execution ──────────────────────────────────────────────────────
756
+ async startPhase(phase) {
757
+ if (phase.status !== "pending" && phase.status !== "ready") return;
758
+ this.updatePhaseStatus(phase, "running");
759
+ phase.startedAt = Date.now();
760
+ this.runningPhases.add(phase.id);
761
+ this.graph.activePhaseIds.push(phase.id);
762
+ if (this.worktrees && !this.phaseWorktrees.has(phase.id)) {
763
+ try {
764
+ const handle = await this.worktrees.allocate(phase.id, {
765
+ slugHint: phase.name,
766
+ ownerLabel: phase.name
767
+ });
768
+ if (handle.status === "active") this.phaseWorktrees.set(phase.id, handle);
769
+ } catch {
770
+ }
771
+ }
772
+ this.emit("phase.started", { phaseId: phase.id, name: phase.name });
773
+ try {
774
+ if (phase.taskGraph.nodes.size === 0) {
775
+ await this.failPhaseAfterTasks(
776
+ phase,
777
+ "Phase has no executable tasks. Add task evidence before starting this phase."
778
+ );
779
+ return;
780
+ }
781
+ await this.executePhaseTasks(phase);
782
+ if (this.stopped) return;
783
+ const failedTasks = this.getFailedTaskCount(phase);
784
+ const completedTasks = this.getCompletedTaskCount(phase);
785
+ this.emit("phase.allTasksDone", {
786
+ phaseId: phase.id,
787
+ completed: completedTasks,
788
+ failed: failedTasks
789
+ });
790
+ if (failedTasks > 0 && this.opts.stopOnFailure) {
791
+ await this.failPhaseAfterTasks(phase, `${failedTasks} task(s) failed`);
792
+ return;
793
+ }
794
+ const verdict = await this.runVerifyGate(phase);
795
+ if (!verdict.ok) {
796
+ await this.failPhaseAfterTasks(
797
+ phase,
798
+ `verification failed${verdict.output ? `: ${this.truncate(verdict.output)}` : ""}`
799
+ );
800
+ return;
801
+ }
802
+ this.updatePhaseStatus(phase, "completed");
803
+ phase.completedAt = Date.now();
804
+ phase.actualDurationMs = Date.now() - (phase.startedAt ?? Date.now());
805
+ this.runningPhases.delete(phase.id);
806
+ this.graph.activePhaseIds = this.graph.activePhaseIds.filter((id) => id !== phase.id);
807
+ this.graph.completedPhaseIds.push(phase.id);
808
+ this.emit("phase.completed", {
809
+ phaseId: phase.id,
810
+ name: phase.name,
811
+ durationMs: phase.actualDurationMs
812
+ });
813
+ this.ctx.onPhaseComplete?.(phase);
814
+ await this.commitAndEnqueueMerge(phase);
815
+ } catch (error) {
816
+ this.updatePhaseStatus(phase, "failed");
817
+ phase.completedAt = Date.now();
818
+ phase.actualDurationMs = Date.now() - (phase.startedAt ?? Date.now());
819
+ this.runningPhases.delete(phase.id);
820
+ this.graph.activePhaseIds = this.graph.activePhaseIds.filter((id) => id !== phase.id);
821
+ this.graph.failedPhaseIds.push(phase.id);
822
+ this.emit("phase.failed", {
823
+ phaseId: phase.id,
824
+ name: phase.name,
825
+ error: error instanceof Error ? error.message : String(error)
826
+ });
827
+ this.ctx.onPhaseFail?.(phase, error instanceof Error ? error : new Error(String(error)));
828
+ await this.keepWorktreeForReview(phase);
829
+ }
830
+ }
831
+ // ─── Verification gate ──────────────────────────────────────────────────────
832
+ /**
833
+ * Run the verification gate for a phase whose tasks all succeeded. Verifies in
834
+ * the phase's worktree; on failure, runs the repair pass and re-verifies, up to
835
+ * `maxVerifyAttempts` repairs. Returns the final verdict. When no `verifyPhase`
836
+ * callback is wired the gate is a no-op and always passes.
837
+ */
838
+ async runVerifyGate(phase) {
839
+ if (!this.ctx.verifyPhase) return { ok: true };
840
+ const env = this.worktreeEnv(phase);
841
+ for (let attempt = 0; attempt <= this.opts.maxVerifyAttempts; attempt++) {
842
+ if (this.stopped) return { ok: false, output: "stopped before verification completed" };
843
+ this.emit("phase.verifying", { phaseId: phase.id, name: phase.name, attempt });
844
+ let verdict;
845
+ try {
846
+ verdict = await this.ctx.verifyPhase(phase, env);
847
+ } catch (err) {
848
+ verdict = { ok: false, output: toErrorMessage(err) };
849
+ }
850
+ if (verdict.ok) return { ok: true };
851
+ this.emit("phase.verifyFailed", {
852
+ phaseId: phase.id,
853
+ name: phase.name,
854
+ attempt,
855
+ error: verdict.output
856
+ });
857
+ if (attempt >= this.opts.maxVerifyAttempts || !this.ctx.repairPhase || this.stopped) {
858
+ return { ok: false, output: verdict.output };
859
+ }
860
+ this.emit("phase.repairing", { phaseId: phase.id, name: phase.name, attempt: attempt + 1 });
861
+ try {
862
+ await this.ctx.repairPhase(
863
+ phase,
864
+ verdict.output ?? "verification failed",
865
+ attempt + 1,
866
+ env
867
+ );
868
+ } catch {
869
+ }
870
+ }
871
+ return { ok: false };
872
+ }
873
+ /** Worktree env (cwd/branch) for a phase, or undefined if it runs on the shared tree. */
874
+ /** Bundle of orchestrator state the worktree-integration steps drive. */
875
+ integrationCtx() {
876
+ return {
877
+ graph: this.graph,
878
+ ctx: this.ctx,
879
+ logger: this.logger,
880
+ worktrees: this.worktrees,
881
+ phaseWorktrees: this.phaseWorktrees,
882
+ phaseMergePromise: this.phaseMergePromise,
883
+ runningPhases: this.runningPhases,
884
+ getMergeQueue: () => this.mergeQueue,
885
+ setMergeQueue: (queue) => {
886
+ this.mergeQueue = queue;
887
+ },
888
+ emit: (event, payload) => this.emit(event, payload),
889
+ updatePhaseStatus: (phase, status) => this.updatePhaseStatus(phase, status)
890
+ };
891
+ }
892
+ worktreeEnv(phase) {
893
+ return worktreeEnv(this.integrationCtx(), phase);
894
+ }
895
+ async failPhaseAfterTasks(phase, error) {
896
+ await failPhaseAfterTasks(this.integrationCtx(), phase, error);
897
+ }
898
+ /** Trim long verifier output so it fits cleanly in an event/error message. */
899
+ truncate(text, max = 500) {
900
+ return truncate(text, max);
901
+ }
902
+ // ─── Worktree integration ───────────────────────────────────────────────────
903
+ async commitAndEnqueueMerge(phase) {
904
+ await commitAndEnqueueMerge(this.integrationCtx(), phase);
905
+ }
906
+ /** A failed phase keeps its worktree on disk for inspection (no merge). */
907
+ async keepWorktreeForReview(phase) {
908
+ await keepWorktreeForReview(this.integrationCtx(), phase);
909
+ }
910
+ async executePhaseTasks(phase) {
911
+ const pendingTasks = this.getExecutableTasks(phase);
912
+ while (pendingTasks.length > 0 && !this.stopped) {
913
+ const batch = pendingTasks.splice(0, this.opts.maxConcurrentTasks);
914
+ const results = await Promise.allSettled(
915
+ batch.map((task) => this.executeSingleTask(task, phase))
916
+ );
917
+ for (let i = 0; i < results.length; i++) {
918
+ const result = results[i];
919
+ const task = batch[i];
920
+ if (!result || !task) continue;
921
+ if (result.status === "fulfilled") {
922
+ this.markTaskCompleted(phase, task);
923
+ } else {
924
+ this.markTaskFailed(phase, task, result.reason);
925
+ }
926
+ }
927
+ const newReady = this.getExecutableTasks(phase);
928
+ pendingTasks.length = 0;
929
+ pendingTasks.push(...newReady);
930
+ }
931
+ }
932
+ async executeSingleTask(task, phase) {
933
+ const tracker = this.getTrackerForPhase(phase);
934
+ tracker.updateNodeStatus(task.id, "in_progress");
935
+ this.emit("phase.taskStarted", {
936
+ phaseId: phase.id,
937
+ taskId: task.id,
938
+ taskTitle: task.title,
939
+ agentName: task.assignee
940
+ });
941
+ const handle = this.phaseWorktrees.get(phase.id);
942
+ const timeoutController = this.opts.taskTimeoutMs > 0 ? new AbortController() : void 0;
943
+ const signal = timeoutController ? AbortSignal.any([this.stopController.signal, timeoutController.signal]) : this.stopController.signal;
944
+ const taskPromise = this.ctx.executeTask(
945
+ task,
946
+ phase.id,
947
+ { cwd: handle?.dir, branch: handle?.branch },
948
+ signal
949
+ );
950
+ if (!timeoutController) return taskPromise;
951
+ const timeoutMs = this.opts.taskTimeoutMs;
952
+ const timedOut = /* @__PURE__ */ Symbol("timed_out");
953
+ const result = await Promise.race([
954
+ taskPromise,
955
+ new Promise((resolve2) => {
956
+ const timer = setTimeout(() => {
957
+ timeoutController.abort();
958
+ resolve2(timedOut);
959
+ }, timeoutMs);
960
+ taskPromise.then(() => clearTimeout(timer)).catch(() => clearTimeout(timer));
961
+ })
962
+ ]);
963
+ if (result !== timedOut) return result;
964
+ this.emit("phase.taskTimedOut", {
965
+ phaseId: phase.id,
966
+ taskId: task.id,
967
+ taskTitle: task.title,
968
+ timeoutMs
969
+ });
970
+ const settled = taskPromise.then(
971
+ () => void 0,
972
+ () => void 0
973
+ );
974
+ const grace = new Promise((resolve2) => {
975
+ const timer = setTimeout(resolve2, 5e3);
976
+ timer.unref?.();
977
+ void settled.then(() => clearTimeout(timer));
978
+ });
979
+ await Promise.race([settled, grace]);
980
+ throw new Error(
981
+ `Task "${task.title}" (${task.id}) exceeded timeout of ${timeoutMs} ms`
982
+ );
983
+ }
984
+ markTaskCompleted(phase, task) {
985
+ const tracker = this.getTrackerForPhase(phase);
986
+ tracker.updateNodeStatus(task.id, "completed");
987
+ this.emit("phase.taskCompleted", {
988
+ phaseId: phase.id,
989
+ taskId: task.id,
990
+ taskTitle: task.title
991
+ });
992
+ }
993
+ markTaskFailed(phase, task, error) {
994
+ const tracker = this.getTrackerForPhase(phase);
995
+ const taskKey = `${phase.id}:${task.id}`;
996
+ const currentRetries = this.taskRetryCounts.get(taskKey) ?? 0;
997
+ if (this.stopped) {
998
+ tracker.updateNodeStatus(task.id, "pending", "Stopped before completion");
999
+ return;
1000
+ }
1001
+ if (currentRetries < this.opts.maxRetries) {
1002
+ this.taskRetryCounts.set(taskKey, currentRetries + 1);
1003
+ tracker.updateNodeStatus(
1004
+ task.id,
1005
+ "pending",
1006
+ `Retry ${currentRetries + 1}/${this.opts.maxRetries}`
1007
+ );
1008
+ this.emit("phase.taskRetrying", {
1009
+ phaseId: phase.id,
1010
+ taskId: task.id,
1011
+ taskTitle: task.title,
1012
+ attempt: currentRetries + 1,
1013
+ maxRetries: this.opts.maxRetries
1014
+ });
1015
+ } else {
1016
+ tracker.updateNodeStatus(
1017
+ task.id,
1018
+ "failed",
1019
+ error instanceof Error ? error.message : String(error)
1020
+ );
1021
+ this.emit("phase.taskFailed", {
1022
+ phaseId: phase.id,
1023
+ taskId: task.id,
1024
+ taskTitle: task.title,
1025
+ error: error instanceof Error ? error.message : String(error)
1026
+ });
1027
+ }
1028
+ }
1029
+ // ─── Helpers ──────────────────────────────────────────────────────────────
1030
+ getReadyPhases() {
1031
+ return getReadyPhases(this.graph);
1032
+ }
1033
+ getActivePhases() {
1034
+ return getActivePhases(this.graph);
1035
+ }
1036
+ getExecutableTasks(phase) {
1037
+ return getExecutableTasks((p) => this.getTrackerForPhase(p), phase);
1038
+ }
1039
+ getTrackerForPhase(phase) {
1040
+ const cached = this.trackerCache.get(phase.id);
1041
+ if (cached) return cached;
1042
+ const tracker = new TaskTracker({ store: new DefaultTaskStore() });
1043
+ tracker.setGraph(phase.taskGraph);
1044
+ this.trackerCache.set(phase.id, tracker);
1045
+ return tracker;
1046
+ }
1047
+ getFailedTaskCount(phase) {
1048
+ return getFailedTaskCount((p) => this.getTrackerForPhase(p), phase);
1049
+ }
1050
+ getCompletedTaskCount(phase) {
1051
+ return getCompletedTaskCount((p) => this.getTrackerForPhase(p), phase);
1052
+ }
1053
+ updatePhaseStatus(phase, status) {
1054
+ const from = phase.status;
1055
+ phase.status = status;
1056
+ phase.updatedAt = Date.now();
1057
+ this.graph.updatedAt = Date.now();
1058
+ this.emit("phase.statusChange", { phaseId: phase.id, from, to: status });
1059
+ }
1060
+ isComplete() {
1061
+ return isGraphComplete(this.graph);
1062
+ }
1063
+ onGraphComplete() {
1064
+ this.graph.completedAt = Date.now();
1065
+ const durationMs = this.graph.completedAt - (this.graph.startedAt ?? this.graph.completedAt);
1066
+ this.emit("graph.completed", { graphId: this.graph.id, durationMs });
1067
+ this.stop();
1068
+ }
1069
+ onGraphFailed(failedPhase) {
1070
+ this.emit("graph.failed", {
1071
+ graphId: this.graph.id,
1072
+ failedPhaseId: failedPhase.id,
1073
+ error: `Phase "${failedPhase.name}" failed`
1074
+ });
1075
+ this.stop();
1076
+ }
1077
+ // ─── Progress ─────────────────────────────────────────────────────────────
1078
+ getProgress() {
1079
+ return getProgress(this.graph, (p) => this.getTrackerForPhase(p));
1080
+ }
1081
+ getGraph() {
1082
+ return this.graph;
1083
+ }
1084
+ isRunning() {
1085
+ return !this.stopped && this.runningPhases.size > 0;
1086
+ }
1087
+ isPaused() {
1088
+ return this.paused;
1089
+ }
1090
+ // ─── Agent Assignment ─────────────────────────────────────────────────────
1091
+ assignAgent(phaseId, agentId) {
1092
+ const phase = this.graph.phases.get(phaseId);
1093
+ if (!phase) return;
1094
+ if (!phase.assignedAgents.includes(agentId)) {
1095
+ phase.assignedAgents.push(agentId);
1096
+ this.emit("agent.assigned", { phaseId, agentId });
1097
+ }
1098
+ }
1099
+ releaseAgent(phaseId, agentId) {
1100
+ const phase = this.graph.phases.get(phaseId);
1101
+ if (!phase) return;
1102
+ phase.assignedAgents = phase.assignedAgents.filter((id) => id !== agentId);
1103
+ this.emit("agent.released", { phaseId, agentId });
1104
+ }
1105
+ // ─── Interactive board mutations ──────────────────────────────────────────
1106
+ //
1107
+ // These are driven by an interactive board (WebUI/TUI), not the autonomous
1108
+ // loop. Each mutates the live graph, emits a typed event so every surface
1109
+ // stays in sync, and bumps updatedAt so the host re-persists.
1110
+ /** Find the phase whose task graph currently holds `taskId`. */
1111
+ findPhaseOfTask(taskId) {
1112
+ return findPhaseOfTaskInGraph(this.graph, taskId);
1113
+ }
1114
+ /**
1115
+ * Move a task to another phase's task graph. Edges that referenced the task
1116
+ * are dropped (cross-phase dependencies are not modeled). No-op when the task
1117
+ * or target phase is missing, or it is already in the target phase.
1118
+ */
1119
+ moveTask(taskId, toPhaseId) {
1120
+ return movePhaseTask(this.boardMutationContext(), taskId, toPhaseId);
1121
+ }
1122
+ /** (Re)assign a task to a specific agent (or clear with agentName/agentId omitted). */
1123
+ setTaskAssignee(taskId, agentId, agentName) {
1124
+ return setPhaseTaskAssignee(this.boardMutationContext(), taskId, agentId, agentName);
1125
+ }
1126
+ /** Add a new task to a phase. Returns the created task id, or null if the phase is missing. */
1127
+ addTask(phaseId, spec) {
1128
+ return addPhaseTask(this.boardMutationContext(), phaseId, spec);
1129
+ }
1130
+ /**
1131
+ * Requeue a task to `pending` (clearing its retry counter) and nudge a
1132
+ * terminal/paused phase back to `ready` so the loop re-runs it. Backs both the
1133
+ * board's "retry" and "start" affordances.
1134
+ */
1135
+ requeueTask(taskId) {
1136
+ return requeuePhaseTask(this.boardMutationContext(), taskId);
1137
+ }
1138
+ boardMutationContext() {
1139
+ return {
1140
+ graph: this.graph,
1141
+ trackerCache: this.trackerCache,
1142
+ taskRetryCounts: this.taskRetryCounts,
1143
+ phaseWorktrees: this.phaseWorktrees,
1144
+ getTrackerForPhase: (phase) => this.getTrackerForPhase(phase),
1145
+ updatePhaseStatus: (phase, status) => this.updatePhaseStatus(phase, status),
1146
+ emit: (event, payload) => this.emit(event, payload)
1147
+ };
1148
+ }
1149
+ // ─── Events ───────────────────────────────────────────────────────────────
1150
+ emit(event, payload) {
1151
+ this.events.emit(event, payload);
1152
+ }
1153
+ async waitWhilePaused() {
1154
+ while (this.paused && !this.stopped) {
1155
+ await this.delay(100);
1156
+ }
1157
+ }
1158
+ delay(ms) {
1159
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1160
+ }
1161
+ };
1162
+
1163
+ // src/goal/goal-runner.ts
1164
+ var GoalRunner = class {
1165
+ graph = null;
1166
+ orchestrator = null;
1167
+ opts;
1168
+ progressInterval = null;
1169
+ maxRunTimer = null;
1170
+ graphCompletedHandler = (payload) => {
1171
+ const p = payload;
1172
+ if (this.graph && p.graphId === this.graph.id) {
1173
+ this.opts.onComplete?.(this.graph);
1174
+ this.cleanup();
1175
+ }
1176
+ };
1177
+ graphFailedHandler = (payload) => {
1178
+ const p = payload;
1179
+ if (this.graph && p.graphId === this.graph.id) {
1180
+ const failedPhase = this.graph.phases.get(p.failedPhaseId);
1181
+ if (failedPhase) {
1182
+ this.opts.onFail?.(this.graph, failedPhase, new Error(p.error));
1183
+ }
1184
+ if (this.opts.stopOnFailure) {
1185
+ this.cleanup();
1186
+ }
1187
+ }
1188
+ };
1189
+ /** Stores the unsubscribe function returned by EventBus.on() */
1190
+ unsubscribeCompleted = null;
1191
+ unsubscribeFailed = null;
1192
+ constructor(opts) {
1193
+ this.opts = opts;
1194
+ }
1195
+ async start() {
1196
+ const builder = new PhaseGraphBuilder({
1197
+ title: this.opts.title,
1198
+ description: this.opts.description,
1199
+ phases: this.opts.phases,
1200
+ autonomous: this.opts.autonomous,
1201
+ stopOnFailure: this.opts.stopOnFailure
1202
+ });
1203
+ this.graph = await builder.build();
1204
+ const ctx = {
1205
+ executeTask: this.opts.executeTask,
1206
+ brain: this.opts.brain,
1207
+ onPhaseComplete: (phase) => {
1208
+ this.opts.onPhaseComplete?.(phase);
1209
+ },
1210
+ onPhaseFail: (phase, error) => {
1211
+ this.opts.onPhaseFail?.(phase, error);
1212
+ },
1213
+ onTick: (tickCtx) => {
1214
+ this.opts.onTick?.(tickCtx);
1215
+ }
1216
+ };
1217
+ if (this.opts.verifyPhase !== void 0) ctx.verifyPhase = this.opts.verifyPhase;
1218
+ if (this.opts.repairPhase !== void 0) ctx.repairPhase = this.opts.repairPhase;
1219
+ if (this.opts.resolveConflict !== void 0) ctx.resolveConflict = this.opts.resolveConflict;
1220
+ this.orchestrator = new PhaseOrchestrator({
1221
+ graph: this.graph,
1222
+ ctx,
1223
+ maxConcurrentPhases: this.opts.maxConcurrentPhases,
1224
+ maxConcurrentTasks: this.opts.maxConcurrentTasks,
1225
+ maxRetries: this.opts.maxRetries,
1226
+ maxVerifyAttempts: this.opts.maxVerifyAttempts,
1227
+ autonomous: this.opts.autonomous,
1228
+ phaseDelayMs: this.opts.phaseDelayMs,
1229
+ stopOnFailure: this.opts.stopOnFailure,
1230
+ events: this.opts.events,
1231
+ worktrees: this.opts.worktrees
1232
+ });
1233
+ if (this.opts.events) {
1234
+ const bus = this.opts.events;
1235
+ const onUntyped = (event, handler) => bus.on(event, handler);
1236
+ this.unsubscribeCompleted = onUntyped("graph.completed", this.graphCompletedHandler);
1237
+ this.unsubscribeFailed = onUntyped("graph.failed", this.graphFailedHandler);
1238
+ }
1239
+ await this.orchestrator.start();
1240
+ this.maxRunTimer = setTimeout(
1241
+ () => {
1242
+ this.opts.onProgress?.({
1243
+ totalPhases: 0,
1244
+ pending: 0,
1245
+ ready: 0,
1246
+ running: 0,
1247
+ paused: 0,
1248
+ completed: 0,
1249
+ failed: 0,
1250
+ skipped: 0,
1251
+ percentComplete: 0,
1252
+ totalTasks: 0,
1253
+ completedTasks: 0,
1254
+ failedTasks: 0,
1255
+ estimatedHours: 0,
1256
+ actualHours: 0
1257
+ });
1258
+ this.stop();
1259
+ },
1260
+ this.opts.maxRunDurationMs ?? 7 * 24 * 60 * 6e4
1261
+ );
1262
+ if (this.opts.maxRunDurationMs !== void 0 && this.opts.maxRunDurationMs <= 0) {
1263
+ clearTimeout(this.maxRunTimer);
1264
+ this.maxRunTimer = null;
1265
+ }
1266
+ this.maxRunTimer?.unref?.();
1267
+ if (this.opts.onProgress) {
1268
+ this.progressInterval = setInterval(() => {
1269
+ const progress = this.orchestrator?.getProgress();
1270
+ if (progress) this.opts.onProgress?.(progress);
1271
+ }, 2e3);
1272
+ }
1273
+ return this.graph;
1274
+ }
1275
+ pause() {
1276
+ this.orchestrator?.pause();
1277
+ }
1278
+ resume() {
1279
+ this.orchestrator?.resume();
1280
+ }
1281
+ stop() {
1282
+ this.orchestrator?.stop();
1283
+ this.cleanup();
1284
+ }
1285
+ getProgress() {
1286
+ return this.orchestrator?.getProgress() ?? null;
1287
+ }
1288
+ getGraph() {
1289
+ return this.graph;
1290
+ }
1291
+ isRunning() {
1292
+ return this.orchestrator?.isRunning() ?? false;
1293
+ }
1294
+ isPaused() {
1295
+ return this.orchestrator?.isPaused() ?? false;
1296
+ }
1297
+ assignAgent(phaseId, agentId) {
1298
+ this.orchestrator?.assignAgent(phaseId, agentId);
1299
+ }
1300
+ releaseAgent(phaseId, agentId) {
1301
+ this.orchestrator?.releaseAgent(phaseId, agentId);
1302
+ }
1303
+ cleanup() {
1304
+ if (this.progressInterval) {
1305
+ clearInterval(this.progressInterval);
1306
+ this.progressInterval = null;
1307
+ }
1308
+ if (this.maxRunTimer) {
1309
+ clearTimeout(this.maxRunTimer);
1310
+ this.maxRunTimer = null;
1311
+ }
1312
+ this.unsubscribeCompleted?.();
1313
+ this.unsubscribeCompleted = null;
1314
+ this.unsubscribeFailed?.();
1315
+ this.unsubscribeFailed = null;
1316
+ }
1317
+ };
1318
+ async function createGoalRunnerFromTaskGraph(taskGraph, options) {
1319
+ const graph = await PhaseGraphBuilder.fromTaskGraph(taskGraph, {
1320
+ title: options.title ?? taskGraph.title,
1321
+ tasksPerPhase: options.tasksPerPhase
1322
+ });
1323
+ const phases = Array.from(graph.phases.values()).map((p) => ({
1324
+ name: p.name,
1325
+ description: p.description,
1326
+ priority: p.priority,
1327
+ estimateHours: p.estimateHours,
1328
+ parallelizable: p.parallelizable
1329
+ }));
1330
+ return new GoalRunner({
1331
+ ...options,
1332
+ title: options.title ?? taskGraph.title,
1333
+ phases
1334
+ });
1335
+ }
1336
+
1337
+ // src/goal/goal-planner.ts
1338
+ var VALID_TASK_TYPES = /* @__PURE__ */ new Set([
1339
+ "feature",
1340
+ "bugfix",
1341
+ "refactor",
1342
+ "docs",
1343
+ "test",
1344
+ "chore"
1345
+ ]);
1346
+ var VALID_PRIORITIES = /* @__PURE__ */ new Set([
1347
+ "critical",
1348
+ "high",
1349
+ "medium",
1350
+ "low"
1351
+ ]);
1352
+ var GoalPlanner = class {
1353
+ constructor(opts) {
1354
+ this.opts = opts;
1355
+ }
1356
+ opts;
1357
+ /** Convert the goal into a phase-and-todo plan. */
1358
+ async plan() {
1359
+ const prompt = this.buildPrompt();
1360
+ const raw = await this.opts.runOnce(prompt);
1361
+ const { phases, warnings } = this.parse(raw);
1362
+ const totalTasks = phases.reduce((s, p) => s + (p.taskTemplates?.length ?? 0), 0);
1363
+ const estimatedCostTokens = totalTasks > 0 ? totalTasks * 5 * 2e3 : void 0;
1364
+ const maxTotal = this.opts.maxTotalTasks ?? 0;
1365
+ let budgetExceeded = false;
1366
+ if (maxTotal > 0 && totalTasks > maxTotal) {
1367
+ warnings.push(
1368
+ `Plan has ${totalTasks} total tasks, exceeding the limit of ${maxTotal}. Consider narrowing the scope or increasing the limit.`
1369
+ );
1370
+ budgetExceeded = true;
1371
+ }
1372
+ return { phases, raw, parseFailed: phases.length === 0, warnings, estimatedCostTokens, budgetExceeded };
1373
+ }
1374
+ /** Instruction prompt for the plan the model should produce. */
1375
+ buildPrompt() {
1376
+ const minP = this.opts.minPhases ?? 3;
1377
+ const maxP = this.opts.maxPhases ?? 8;
1378
+ const todos = this.opts.todosPerPhase ?? 6;
1379
+ const ctx = this.opts.projectContext?.trim();
1380
+ return renderInstructionTemplate(readBundledInstructionText("goal/phase-planner.md"), {
1381
+ minPhases: String(minP),
1382
+ maxPhases: String(maxP),
1383
+ todosPerPhase: String(todos),
1384
+ goal: this.opts.goal,
1385
+ projectContext: ctx ? `
1386
+ PROJECT CONTEXT:
1387
+ ${ctx}
1388
+ ` : ""
1389
+ });
1390
+ }
1391
+ /** Extract JSON from raw output, validate it, and convert it to PhaseTemplate[].
1392
+ * Returns `{ phases, warnings }` where warnings describe coerced defaults. */
1393
+ parse(raw) {
1394
+ const json = extractJSONArray(raw);
1395
+ if (!json) return { phases: [], warnings: ["No JSON array found in model output"] };
1396
+ let data;
1397
+ try {
1398
+ data = JSON.parse(json);
1399
+ } catch {
1400
+ return { phases: [], warnings: ["Failed to parse JSON from model output"] };
1401
+ }
1402
+ if (!Array.isArray(data)) return { phases: [], warnings: ["Model output is not a JSON array"] };
1403
+ const warnings = [];
1404
+ const phases = [];
1405
+ for (const entry of data) {
1406
+ const result = this.coercePhase(entry);
1407
+ if (result) {
1408
+ warnings.push(...result.warnings);
1409
+ if (result.phase.taskTemplates?.length) {
1410
+ phases.push(result.phase);
1411
+ } else {
1412
+ warnings.push(`Phase "${result.phase.name}" has no executable tasks and was rejected.`);
1413
+ }
1414
+ }
1415
+ }
1416
+ return { phases, warnings };
1417
+ }
1418
+ coercePhase(entry) {
1419
+ if (!entry || typeof entry !== "object") return null;
1420
+ const e = entry;
1421
+ const name = typeof e.name === "string" ? e.name.trim() : "";
1422
+ if (!name) return null;
1423
+ const w = [];
1424
+ const rawTasks = Array.isArray(e.tasks) ? e.tasks : Array.isArray(e.taskTemplates) ? e.taskTemplates : [];
1425
+ const taskTemplates = [];
1426
+ for (const t of rawTasks) {
1427
+ const result = this.coerceTask(t);
1428
+ if (result) {
1429
+ taskTemplates.push(result.task);
1430
+ w.push(...result.warnings);
1431
+ }
1432
+ }
1433
+ const coercedPriority = coercePriority(e.priority);
1434
+ if (typeof e.priority === "string" && coercedPriority !== e.priority) {
1435
+ w.push('Phase "' + name + '": invalid priority "' + String(e.priority) + '", defaulting to "' + coercedPriority + '"');
1436
+ }
1437
+ return {
1438
+ phase: {
1439
+ name,
1440
+ description: typeof e.description === "string" ? e.description : "",
1441
+ priority: coercedPriority,
1442
+ estimateHours: coerceHours(e.estimateHours, 4),
1443
+ parallelizable: e.parallelizable === true,
1444
+ taskTemplates
1445
+ },
1446
+ warnings: w
1447
+ };
1448
+ }
1449
+ coerceTask(t) {
1450
+ if (!t || typeof t !== "object") return null;
1451
+ const o = t;
1452
+ const title = typeof o.title === "string" ? o.title.trim() : "";
1453
+ if (!title) return null;
1454
+ const w = [];
1455
+ let type = "feature";
1456
+ if (typeof o.type === "string") {
1457
+ if (VALID_TASK_TYPES.has(o.type)) {
1458
+ type = o.type;
1459
+ } else {
1460
+ w.push('Task "' + title + '": invalid type "' + o.type + '", defaulting to "feature"');
1461
+ }
1462
+ }
1463
+ const coercedPriority = coercePriority(o.priority);
1464
+ if (typeof o.priority === "string" && coercedPriority !== o.priority) {
1465
+ w.push('Task "' + title + '": invalid priority "' + String(o.priority) + '", defaulting to "' + coercedPriority + '"');
1466
+ }
1467
+ return {
1468
+ task: {
1469
+ title,
1470
+ description: typeof o.description === "string" ? o.description : "",
1471
+ type,
1472
+ priority: coercedPriority,
1473
+ estimateHours: coerceHours(o.estimateHours, 2),
1474
+ tags: Array.isArray(o.tags) ? o.tags.map(String) : []
1475
+ },
1476
+ warnings: w
1477
+ };
1478
+ }
1479
+ };
1480
+ function coercePriority(value) {
1481
+ return VALID_PRIORITIES.has(value) ? value : "medium";
1482
+ }
1483
+ function coerceHours(value, fallback) {
1484
+ const n = Number(value);
1485
+ return Number.isFinite(n) && n > 0 ? n : fallback;
1486
+ }
1487
+ function extractJSONArray(text) {
1488
+ const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
1489
+ const candidates = [];
1490
+ if (fence?.[1]) candidates.push(fence[1]);
1491
+ candidates.push(text);
1492
+ for (const candidate of candidates) {
1493
+ const balanced = firstBalancedArray(candidate);
1494
+ if (balanced) return balanced;
1495
+ }
1496
+ return null;
1497
+ }
1498
+ function firstBalancedArray(text) {
1499
+ const start = text.indexOf("[");
1500
+ if (start === -1) return null;
1501
+ let depth = 0;
1502
+ let inString = false;
1503
+ let escaped = false;
1504
+ for (let i = start; i < text.length; i++) {
1505
+ const ch = text[i] ?? "";
1506
+ if (inString) {
1507
+ if (escaped) {
1508
+ escaped = false;
1509
+ } else if (ch === "\\") {
1510
+ escaped = true;
1511
+ } else if (ch === '"') {
1512
+ inString = false;
1513
+ }
1514
+ continue;
1515
+ }
1516
+ if (ch === '"') {
1517
+ inString = true;
1518
+ } else if (ch === "[") {
1519
+ depth++;
1520
+ } else if (ch === "]") {
1521
+ depth--;
1522
+ if (depth === 0) {
1523
+ return text.slice(start, i + 1);
1524
+ }
1525
+ }
1526
+ }
1527
+ return null;
1528
+ }
1529
+
1530
+ // src/goal/goal-assessor.ts
1531
+ var GoalAssessor = class {
1532
+ constructor(opts) {
1533
+ this.opts = opts;
1534
+ }
1535
+ opts;
1536
+ /** Analyze the goal and return a structured realism assessment. */
1537
+ async assess() {
1538
+ const prompt = this.buildPrompt();
1539
+ const raw = await this.opts.runOnce(prompt);
1540
+ return this.parse(raw);
1541
+ }
1542
+ /** Build the instruction prompt for the LLM. */
1543
+ buildPrompt() {
1544
+ return renderInstructionTemplate(
1545
+ readBundledInstructionText("goal/goal-assessor.md"),
1546
+ { goal: this.opts.goal }
1547
+ );
1548
+ }
1549
+ /** Extract and validate the JSON object from the raw LLM output. */
1550
+ parse(raw) {
1551
+ const json = extractJSONObject(raw);
1552
+ if (!json) {
1553
+ return {
1554
+ realistic: true,
1555
+ durationClaimed: null,
1556
+ explanation: "",
1557
+ recommendedDuration: null,
1558
+ concerns: [],
1559
+ raw,
1560
+ parseFailed: true,
1561
+ parseError: "No JSON object found in model output"
1562
+ };
1563
+ }
1564
+ let data;
1565
+ try {
1566
+ data = JSON.parse(json);
1567
+ } catch {
1568
+ return {
1569
+ realistic: true,
1570
+ durationClaimed: null,
1571
+ explanation: "",
1572
+ recommendedDuration: null,
1573
+ concerns: [],
1574
+ raw,
1575
+ parseFailed: true,
1576
+ parseError: "Failed to parse JSON from model output"
1577
+ };
1578
+ }
1579
+ return {
1580
+ realistic: data.realistic === true,
1581
+ durationClaimed: typeof data.durationClaimed === "string" ? data.durationClaimed : null,
1582
+ explanation: typeof data.explanation === "string" ? data.explanation : "",
1583
+ recommendedDuration: typeof data.recommendedDuration === "string" ? data.recommendedDuration : null,
1584
+ concerns: Array.isArray(data.concerns) ? data.concerns.filter((c) => typeof c === "string") : [],
1585
+ raw,
1586
+ parseFailed: false
1587
+ };
1588
+ }
1589
+ };
1590
+ function extractJSONObject(text) {
1591
+ const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
1592
+ const candidates = [];
1593
+ if (fence?.[1]) candidates.push(fence[1]);
1594
+ candidates.push(text);
1595
+ for (const candidate of candidates) {
1596
+ const balanced = firstBalancedObject(candidate);
1597
+ if (balanced) return balanced;
1598
+ }
1599
+ return null;
1600
+ }
1601
+ function firstBalancedObject(text) {
1602
+ const start = text.indexOf("{");
1603
+ if (start === -1) return null;
1604
+ let depth = 0;
1605
+ let inString = false;
1606
+ let escaped = false;
1607
+ for (let i = start; i < text.length; i++) {
1608
+ const ch = text[i] ?? "";
1609
+ if (inString) {
1610
+ if (escaped) {
1611
+ escaped = false;
1612
+ } else if (ch === "\\") {
1613
+ escaped = true;
1614
+ } else if (ch === '"') {
1615
+ inString = false;
1616
+ }
1617
+ continue;
1618
+ }
1619
+ if (ch === '"') {
1620
+ inString = true;
1621
+ } else if (ch === "{") {
1622
+ depth++;
1623
+ } else if (ch === "}") {
1624
+ depth--;
1625
+ if (depth === 0) {
1626
+ return text.slice(start, i + 1);
1627
+ }
1628
+ }
1629
+ }
1630
+ return null;
1631
+ }
1632
+
1633
+ // src/goal/types.ts
1634
+ var PHASE_EVENT_NAMES = [
1635
+ "phase.statusChange",
1636
+ "phase.started",
1637
+ "phase.completed",
1638
+ "phase.failed",
1639
+ "phase.taskStarted",
1640
+ "phase.taskCompleted",
1641
+ "phase.taskFailed",
1642
+ "phase.taskRetrying",
1643
+ "phase.taskTimedOut",
1644
+ "phase.allTasksDone",
1645
+ "phase.verifying",
1646
+ "phase.verifyFailed",
1647
+ "phase.repairing",
1648
+ "phase.conflictResolving",
1649
+ "phase.conflictResolved",
1650
+ "phase.taskMoved",
1651
+ "phase.taskAssigned",
1652
+ "phase.taskAdded",
1653
+ "graph.completed",
1654
+ "graph.failed",
1655
+ "autonomous.tick",
1656
+ "agent.assigned",
1657
+ "agent.released"
1658
+ ];
1659
+
1660
+ // src/goal/phase-store.ts
1661
+ import * as fsp from "node:fs/promises";
1662
+ import * as path from "node:path";
1663
+ var PHASE_STORE_VERSION = 1;
1664
+ var PhaseStore = class {
1665
+ baseDir;
1666
+ legacyBaseDirs;
1667
+ constructor(opts) {
1668
+ this.baseDir = opts.baseDir;
1669
+ this.legacyBaseDirs = (opts.legacyBaseDirs ?? []).filter(
1670
+ (dir) => path.resolve(dir) !== path.resolve(this.baseDir)
1671
+ );
1672
+ }
1673
+ async save(graph) {
1674
+ const filePath = this.getFilePath(graph.id);
1675
+ const serialized = this.serializeGraph(graph);
1676
+ await withFileLock(filePath, async () => {
1677
+ await atomicWrite(filePath, JSON.stringify(serialized, null, 2), { mode: 384 });
1678
+ });
1679
+ }
1680
+ async load(graphId) {
1681
+ const filePath = this.getFilePath(graphId);
1682
+ const current = await this.loadFromPath(filePath);
1683
+ if (current) return current;
1684
+ for (const legacyDir of this.legacyBaseDirs) {
1685
+ const legacyPath = path.join(legacyDir, `${graphId}.json`);
1686
+ const legacy = await this.loadFromPath(legacyPath);
1687
+ if (!legacy) continue;
1688
+ try {
1689
+ await this.save(legacy);
1690
+ await this.removeMigratedLegacyFile(legacyDir, legacyPath);
1691
+ } catch {
1692
+ return null;
1693
+ }
1694
+ return legacy;
1695
+ }
1696
+ return null;
1697
+ }
1698
+ async delete(graphId) {
1699
+ const paths = [
1700
+ this.getFilePath(graphId),
1701
+ ...this.legacyBaseDirs.map((dir) => path.join(dir, `${graphId}.json`))
1702
+ ];
1703
+ await Promise.all(paths.map((filePath) => fsp.unlink(filePath).catch(() => void 0)));
1704
+ }
1705
+ async list() {
1706
+ try {
1707
+ await this.migrateLegacyGraphs();
1708
+ const entries = await fsp.readdir(this.baseDir, { withFileTypes: true });
1709
+ const graphs = [];
1710
+ for (const entry of entries) {
1711
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
1712
+ try {
1713
+ const raw = await fsp.readFile(path.join(this.baseDir, entry.name), "utf8");
1714
+ const serialized = JSON.parse(raw);
1715
+ const done = serialized.completedPhaseIds.length;
1716
+ const total = serialized.phases.length;
1717
+ graphs.push({
1718
+ id: serialized.id,
1719
+ title: serialized.title,
1720
+ updatedAt: serialized.updatedAt,
1721
+ status: done === total ? "completed" : done > 0 ? "in_progress" : "pending"
1722
+ });
1723
+ } catch {
1724
+ }
1725
+ }
1726
+ return graphs.sort((a, b) => b.updatedAt - a.updatedAt);
1727
+ } catch {
1728
+ return [];
1729
+ }
1730
+ }
1731
+ getFilePath(graphId) {
1732
+ return path.join(this.baseDir, `${graphId}.json`);
1733
+ }
1734
+ async loadFromPath(filePath) {
1735
+ try {
1736
+ const raw = await fsp.readFile(filePath, "utf8");
1737
+ const serialized = JSON.parse(raw);
1738
+ return this.deserializeGraph(serialized);
1739
+ } catch {
1740
+ return null;
1741
+ }
1742
+ }
1743
+ async migrateLegacyGraphs() {
1744
+ for (const legacyDir of this.legacyBaseDirs) {
1745
+ const entries = await fsp.readdir(legacyDir, { withFileTypes: true }).catch(() => []);
1746
+ for (const entry of entries) {
1747
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
1748
+ const legacyPath = path.join(legacyDir, entry.name);
1749
+ const currentPath = this.getFilePath(path.basename(entry.name, ".json"));
1750
+ if (await this.pathExists(currentPath)) {
1751
+ await this.removeMigratedLegacyFile(legacyDir, legacyPath);
1752
+ continue;
1753
+ }
1754
+ const graph = await this.loadFromPath(legacyPath);
1755
+ if (!graph) continue;
1756
+ await this.save(graph);
1757
+ await this.removeMigratedLegacyFile(legacyDir, legacyPath);
1758
+ }
1759
+ }
1760
+ }
1761
+ async pathExists(filePath) {
1762
+ try {
1763
+ await fsp.access(filePath);
1764
+ return true;
1765
+ } catch {
1766
+ return false;
1767
+ }
1768
+ }
1769
+ async removeMigratedLegacyFile(legacyDir, legacyPath) {
1770
+ await fsp.unlink(legacyPath).catch(() => void 0);
1771
+ await fsp.rmdir(legacyDir).catch(() => void 0);
1772
+ }
1773
+ serializeGraph(graph) {
1774
+ return {
1775
+ version: PHASE_STORE_VERSION,
1776
+ id: graph.id,
1777
+ title: graph.title,
1778
+ description: graph.description,
1779
+ phases: Array.from(graph.phases.values()).map((p) => this.serializePhase(p)),
1780
+ rootPhaseIds: graph.rootPhaseIds,
1781
+ activePhaseIds: graph.activePhaseIds,
1782
+ completedPhaseIds: graph.completedPhaseIds,
1783
+ failedPhaseIds: graph.failedPhaseIds,
1784
+ autonomous: graph.autonomous,
1785
+ stopOnComplete: graph.stopOnComplete,
1786
+ createdAt: graph.createdAt,
1787
+ updatedAt: graph.updatedAt,
1788
+ startedAt: graph.startedAt,
1789
+ completedAt: graph.completedAt
1790
+ };
1791
+ }
1792
+ serializePhase(phase) {
1793
+ return {
1794
+ id: phase.id,
1795
+ name: phase.name,
1796
+ description: phase.description,
1797
+ status: phase.status,
1798
+ taskGraph: this.serializeTaskGraph(phase.taskGraph),
1799
+ dependsOn: phase.dependsOn,
1800
+ nextPhases: phase.nextPhases,
1801
+ parallelizable: phase.parallelizable,
1802
+ priority: phase.priority,
1803
+ estimateHours: phase.estimateHours,
1804
+ actualDurationMs: phase.actualDurationMs,
1805
+ startedAt: phase.startedAt,
1806
+ completedAt: phase.completedAt,
1807
+ assignedAgents: phase.assignedAgents,
1808
+ metadata: phase.metadata,
1809
+ createdAt: phase.createdAt,
1810
+ updatedAt: phase.updatedAt
1811
+ };
1812
+ }
1813
+ serializeTaskGraph(graph) {
1814
+ return {
1815
+ id: graph.id,
1816
+ specId: graph.specId,
1817
+ requiredRequirementIds: graph.requiredRequirementIds,
1818
+ title: graph.title,
1819
+ nodes: Array.from(graph.nodes.values()).map((n) => this.serializeTaskNode(n)),
1820
+ edges: graph.edges,
1821
+ rootNodes: graph.rootNodes,
1822
+ createdAt: graph.createdAt,
1823
+ updatedAt: graph.updatedAt
1824
+ };
1825
+ }
1826
+ serializeTaskNode(node) {
1827
+ return { ...node };
1828
+ }
1829
+ deserializeGraph(serialized) {
1830
+ const fileVersion = serialized.version ?? 0;
1831
+ if (fileVersion > PHASE_STORE_VERSION) {
1832
+ throw new Error(
1833
+ `Cannot load phase graph: file version ${fileVersion} is newer than supported version ${PHASE_STORE_VERSION}. Upgrade WrongStack to load this file.`
1834
+ );
1835
+ }
1836
+ const phases = /* @__PURE__ */ new Map();
1837
+ for (const sp of serialized.phases) {
1838
+ phases.set(sp.id, this.deserializePhase(sp));
1839
+ }
1840
+ return {
1841
+ id: serialized.id,
1842
+ title: serialized.title,
1843
+ description: serialized.description,
1844
+ phases,
1845
+ rootPhaseIds: serialized.rootPhaseIds,
1846
+ activePhaseIds: serialized.activePhaseIds,
1847
+ completedPhaseIds: serialized.completedPhaseIds,
1848
+ failedPhaseIds: serialized.failedPhaseIds,
1849
+ autonomous: serialized.autonomous,
1850
+ stopOnComplete: serialized.stopOnComplete,
1851
+ createdAt: serialized.createdAt,
1852
+ updatedAt: serialized.updatedAt,
1853
+ startedAt: serialized.startedAt,
1854
+ completedAt: serialized.completedAt
1855
+ };
1856
+ }
1857
+ deserializePhase(serialized) {
1858
+ return {
1859
+ id: serialized.id,
1860
+ name: serialized.name,
1861
+ description: serialized.description,
1862
+ status: serialized.status,
1863
+ taskGraph: this.deserializeTaskGraph(serialized.taskGraph),
1864
+ dependsOn: serialized.dependsOn,
1865
+ nextPhases: serialized.nextPhases,
1866
+ parallelizable: serialized.parallelizable,
1867
+ priority: serialized.priority,
1868
+ estimateHours: serialized.estimateHours,
1869
+ actualDurationMs: serialized.actualDurationMs,
1870
+ startedAt: serialized.startedAt,
1871
+ completedAt: serialized.completedAt,
1872
+ assignedAgents: serialized.assignedAgents,
1873
+ metadata: serialized.metadata,
1874
+ createdAt: serialized.createdAt,
1875
+ updatedAt: serialized.updatedAt
1876
+ };
1877
+ }
1878
+ deserializeTaskGraph(serialized) {
1879
+ const nodes = /* @__PURE__ */ new Map();
1880
+ for (const sn of serialized.nodes) {
1881
+ nodes.set(sn.id, sn);
1882
+ }
1883
+ return {
1884
+ id: serialized.id,
1885
+ specId: serialized.specId,
1886
+ requiredRequirementIds: serialized.requiredRequirementIds,
1887
+ title: serialized.title,
1888
+ nodes,
1889
+ edges: serialized.edges ?? [],
1890
+ rootNodes: serialized.rootNodes ?? [],
1891
+ createdAt: serialized.createdAt,
1892
+ updatedAt: serialized.updatedAt
1893
+ };
1894
+ }
1895
+ };
1896
+
1897
+ // src/goal/checkpoint.ts
1898
+ import * as fsp2 from "node:fs/promises";
1899
+ import * as path2 from "node:path";
1900
+ var CheckpointManager = class {
1901
+ store;
1902
+ maxCheckpoints;
1903
+ maxCheckpointAgeMs;
1904
+ checkpoints = /* @__PURE__ */ new Map();
1905
+ baseDir;
1906
+ constructor(opts) {
1907
+ this.store = opts.store;
1908
+ this.maxCheckpoints = opts.maxCheckpoints ?? 10;
1909
+ this.maxCheckpointAgeMs = opts.maxCheckpointAgeMs ?? 7 * 24 * 60 * 60 * 1e3;
1910
+ this.baseDir = opts.baseDir ?? path2.join(opts.store.baseDir, ".checkpoints");
1911
+ }
1912
+ async initialize() {
1913
+ await fsp2.mkdir(this.baseDir, { recursive: true });
1914
+ await this.loadFromDisk();
1915
+ }
1916
+ async saveCheckpoint(graph, label) {
1917
+ await this.store.save(graph);
1918
+ const activePhase = Array.from(graph.phases.values()).find(
1919
+ (p) => p.status === "running" || p.status === "paused"
1920
+ );
1921
+ const checkpoint = {
1922
+ id: crypto.randomUUID(),
1923
+ graphId: graph.id,
1924
+ phaseId: activePhase?.id ?? graph.rootPhaseIds[0] ?? "",
1925
+ phaseStatus: activePhase?.status ?? "pending",
1926
+ taskStatuses: activePhase ? Array.from(activePhase.taskGraph.nodes.values()).map((t) => ({
1927
+ taskId: t.id,
1928
+ status: t.status,
1929
+ title: t.title
1930
+ })) : [],
1931
+ timestamp: Date.now(),
1932
+ label
1933
+ };
1934
+ this.checkpoints.set(checkpoint.id, checkpoint);
1935
+ await this.saveToDisk(checkpoint);
1936
+ await this.pruneCheckpoints(graph.id);
1937
+ return checkpoint;
1938
+ }
1939
+ async restoreCheckpoint(checkpointId) {
1940
+ let checkpoint = this.checkpoints.get(checkpointId);
1941
+ if (!checkpoint) {
1942
+ await this.loadFromDisk();
1943
+ checkpoint = this.checkpoints.get(checkpointId);
1944
+ }
1945
+ if (!checkpoint) return null;
1946
+ const graph = await this.store.load(checkpoint.graphId);
1947
+ if (!graph) return null;
1948
+ const phase = graph.phases.get(checkpoint.phaseId);
1949
+ if (phase) {
1950
+ phase.status = checkpoint.phaseStatus;
1951
+ phase.updatedAt = Date.now();
1952
+ for (const ts of checkpoint.taskStatuses) {
1953
+ const task = phase.taskGraph.nodes.get(ts.taskId);
1954
+ if (task) {
1955
+ task.status = ts.status;
1956
+ task.updatedAt = Date.now();
1957
+ }
1958
+ }
1959
+ }
1960
+ graph.updatedAt = Date.now();
1961
+ return graph;
1962
+ }
1963
+ listCheckpoints(graphId) {
1964
+ const all = Array.from(this.checkpoints.values());
1965
+ const filtered = graphId ? all.filter((c) => c.graphId === graphId) : all;
1966
+ return filtered.sort((a, b) => b.timestamp - a.timestamp);
1967
+ }
1968
+ async deleteCheckpoint(checkpointId) {
1969
+ const checkpoint = this.checkpoints.get(checkpointId);
1970
+ if (!checkpoint) return false;
1971
+ this.checkpoints.delete(checkpointId);
1972
+ await this.deleteFromDisk(checkpointId);
1973
+ return true;
1974
+ }
1975
+ async saveToDisk(checkpoint) {
1976
+ const filePath = path2.join(this.baseDir, `${checkpoint.graphId}.json`);
1977
+ const serialized = {
1978
+ ...checkpoint
1979
+ };
1980
+ await withFileLock(filePath, async () => {
1981
+ let existing = [];
1982
+ try {
1983
+ const raw = await fsp2.readFile(filePath, "utf8");
1984
+ const parsed = JSON.parse(raw);
1985
+ if (Array.isArray(parsed)) {
1986
+ existing = parsed;
1987
+ }
1988
+ } catch {
1989
+ }
1990
+ existing.push(serialized);
1991
+ await atomicWrite(filePath, JSON.stringify(existing, null, 2), { mode: 384 });
1992
+ });
1993
+ }
1994
+ async deleteFromDisk(checkpointId) {
1995
+ let entries;
1996
+ try {
1997
+ entries = await fsp2.readdir(this.baseDir);
1998
+ } catch {
1999
+ return;
2000
+ }
2001
+ for (const filename of entries) {
2002
+ if (!filename.endsWith(".json")) continue;
2003
+ const filePath = path2.join(this.baseDir, filename);
2004
+ try {
2005
+ await withFileLock(filePath, async () => {
2006
+ const raw = await fsp2.readFile(filePath, "utf8");
2007
+ const parsed = JSON.parse(raw);
2008
+ if (!Array.isArray(parsed)) return;
2009
+ const existing = parsed;
2010
+ const filtered = existing.filter((c) => c.id !== checkpointId);
2011
+ if (filtered.length !== existing.length) {
2012
+ if (filtered.length === 0) {
2013
+ await fsp2.unlink(filePath);
2014
+ } else {
2015
+ await atomicWrite(filePath, JSON.stringify(filtered, null, 2), { mode: 384 });
2016
+ }
2017
+ }
2018
+ });
2019
+ } catch {
2020
+ }
2021
+ }
2022
+ }
2023
+ async loadFromDisk() {
2024
+ let entries;
2025
+ try {
2026
+ entries = await fsp2.readdir(this.baseDir);
2027
+ } catch {
2028
+ return;
2029
+ }
2030
+ for (const filename of entries) {
2031
+ if (!filename.endsWith(".json")) continue;
2032
+ const filePath = path2.join(this.baseDir, filename);
2033
+ try {
2034
+ const raw = await fsp2.readFile(filePath, "utf8");
2035
+ const parsed = JSON.parse(raw);
2036
+ if (!Array.isArray(parsed)) continue;
2037
+ const checkpoints = parsed;
2038
+ for (const sc of checkpoints) {
2039
+ const checkpoint = {
2040
+ id: sc.id,
2041
+ graphId: sc.graphId,
2042
+ phaseId: sc.phaseId,
2043
+ phaseStatus: sc.phaseStatus,
2044
+ taskStatuses: sc.taskStatuses,
2045
+ timestamp: sc.timestamp,
2046
+ label: sc.label
2047
+ };
2048
+ this.checkpoints.set(checkpoint.id, checkpoint);
2049
+ }
2050
+ } catch {
2051
+ }
2052
+ }
2053
+ }
2054
+ async pruneCheckpoints(graphId) {
2055
+ const now = Date.now();
2056
+ const graphCheckpoints = Array.from(this.checkpoints.values()).filter((checkpoint) => checkpoint.graphId === graphId).sort((a, b) => a.timestamp - b.timestamp);
2057
+ for (const cp of graphCheckpoints) {
2058
+ if (now - cp.timestamp > this.maxCheckpointAgeMs) {
2059
+ this.checkpoints.delete(cp.id);
2060
+ await this.deleteFromDisk(cp.id);
2061
+ }
2062
+ }
2063
+ const remaining = Array.from(this.checkpoints.values()).filter((checkpoint) => checkpoint.graphId === graphId).sort((a, b) => a.timestamp - b.timestamp);
2064
+ while (remaining.length > this.maxCheckpoints) {
2065
+ const oldest = remaining.shift();
2066
+ if (oldest) {
2067
+ this.checkpoints.delete(oldest.id);
2068
+ await this.deleteFromDisk(oldest.id);
2069
+ }
2070
+ }
2071
+ }
2072
+ };
2073
+
2074
+ export {
2075
+ PhaseGraphBuilder,
2076
+ PhaseOrchestrator,
2077
+ GoalRunner,
2078
+ createGoalRunnerFromTaskGraph,
2079
+ GoalPlanner,
2080
+ extractJSONArray,
2081
+ GoalAssessor,
2082
+ PHASE_EVENT_NAMES,
2083
+ PhaseStore,
2084
+ CheckpointManager
2085
+ };
2086
+ //# sourceMappingURL=chunk-XQ2XGIBF.js.map