@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
@@ -1,3439 +1,37 @@
1
- // src/tasking/task-store.ts
2
- var DefaultTaskStore = class {
3
- graphs = /* @__PURE__ */ new Map();
4
- async saveGraph(graph) {
5
- this.graphs.set(graph.id, this.cloneGraph(graph));
6
- }
7
- async loadGraph(id) {
8
- const graph = this.graphs.get(id);
9
- return graph ? this.cloneGraph(graph) : null;
10
- }
11
- async listGraphs() {
12
- return Array.from(this.graphs.values()).map((graph) => ({
13
- id: graph.id,
14
- title: graph.title,
15
- updatedAt: graph.updatedAt
16
- }));
17
- }
18
- async deleteGraph(id) {
19
- this.graphs.delete(id);
20
- }
21
- cloneGraph(graph) {
22
- return {
23
- ...graph,
24
- nodes: new Map(graph.nodes),
25
- edges: [...graph.edges],
26
- rootNodes: [...graph.rootNodes]
27
- };
28
- }
29
- };
30
-
31
- // src/utils/error.ts
32
- function toErrorMessage(err) {
33
- return err instanceof Error ? err.message : String(err);
34
- }
35
-
36
- // src/types/errors.ts
37
- var ERROR_CODES = {
38
- // Provider
39
- PROVIDER_RATE_LIMITED: "PROVIDER_RATE_LIMITED",
40
- PROVIDER_AUTH_FAILED: "PROVIDER_AUTH_FAILED",
41
- PROVIDER_OVERLOADED: "PROVIDER_OVERLOADED",
42
- PROVIDER_INVALID_REQUEST: "PROVIDER_INVALID_REQUEST",
43
- PROVIDER_SERVER_ERROR: "PROVIDER_SERVER_ERROR",
44
- PROVIDER_NETWORK_ERROR: "PROVIDER_NETWORK_ERROR",
45
- PROVIDER_CONTEXT_OVERFLOW: "PROVIDER_CONTEXT_OVERFLOW",
46
- // Tool
47
- TOOL_NOT_FOUND: "TOOL_NOT_FOUND",
48
- TOOL_PERMISSION_DENIED: "TOOL_PERMISSION_DENIED",
49
- TOOL_EXECUTION_FAILED: "TOOL_EXECUTION_FAILED",
50
- TOOL_TIMEOUT: "TOOL_TIMEOUT",
51
- TOOL_INPUT_INVALID: "TOOL_INPUT_INVALID",
52
- // Config
53
- CONFIG_INVALID: "CONFIG_INVALID",
54
- CONFIG_NOT_FOUND: "CONFIG_NOT_FOUND",
55
- CONFIG_PARSE_FAILED: "CONFIG_PARSE_FAILED",
56
- CONFIG_MIGRATION_NEEDED: "CONFIG_MIGRATION_NEEDED",
57
- // Plugin
58
- PLUGIN_LOAD_FAILED: "PLUGIN_LOAD_FAILED",
59
- PLUGIN_API_MISMATCH: "PLUGIN_API_MISMATCH",
60
- PLUGIN_MISSING_DEPENDENCY: "PLUGIN_MISSING_DEPENDENCY",
61
- // Agent
62
- AGENT_ITERATION_LIMIT: "AGENT_ITERATION_LIMIT",
63
- AGENT_CONTEXT_OVERFLOW: "AGENT_CONTEXT_OVERFLOW",
64
- AGENT_ABORTED: "AGENT_ABORTED",
65
- AGENT_RUN_FAILED: "AGENT_RUN_FAILED",
66
- // Session
67
- SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
68
- SESSION_CORRUPTED: "SESSION_CORRUPTED",
69
- SESSION_WRITE_FAILED: "SESSION_WRITE_FAILED",
70
- // Container / Registry
71
- CONTAINER_TOKEN_ALREADY_BOUND: "CONTAINER_TOKEN_ALREADY_BOUND",
72
- CONTAINER_TOKEN_NOT_BOUND: "CONTAINER_TOKEN_NOT_BOUND",
73
- CONTAINER_CIRCULAR_DEPENDENCY: "CONTAINER_CIRCULAR_DEPENDENCY",
74
- REGISTRY_DUPLICATE: "REGISTRY_DUPLICATE",
75
- REGISTRY_NOT_FOUND: "REGISTRY_NOT_FOUND",
76
- REGISTRY_INVALID: "REGISTRY_INVALID",
77
- // File system
78
- FS_READ_FAILED: "FS_READ_FAILED",
79
- FS_WRITE_FAILED: "FS_WRITE_FAILED",
80
- FS_MKDIR_FAILED: "FS_MKDIR_FAILED",
81
- FS_DELETE_FAILED: "FS_DELETE_FAILED",
82
- FS_ATOMIC_WRITE_FAILED: "FS_ATOMIC_WRITE_FAILED",
83
- // SDD (Spec-Driven Development)
84
- SDD_VALIDATION_FAILED: "SDD_VALIDATION_FAILED",
85
- SDD_PARSE_FAILED: "SDD_PARSE_FAILED",
86
- SDD_INVALID_STATE: "SDD_INVALID_STATE",
87
- SDD_NOT_READY: "SDD_NOT_READY",
88
- // General
89
- VALIDATION_ERROR: "VALIDATION_ERROR",
90
- PARSE_FAILED: "PARSE_FAILED",
91
- UNKNOWN: "UNKNOWN"
92
- };
93
- var WrongStackError = class extends Error {
94
- code;
95
- subsystem;
96
- severity;
97
- recoverable;
98
- context;
99
- constructor(opts) {
100
- super(opts.message, { cause: opts.cause });
101
- this.name = "WrongStackError";
102
- this.code = opts.code;
103
- this.subsystem = opts.subsystem;
104
- this.severity = opts.severity ?? "error";
105
- this.recoverable = opts.recoverable ?? false;
106
- this.context = opts.context;
107
- }
108
- /**
109
- * Render a one-line user-facing description.
110
- * Subclasses should override for domain-specific formatting.
111
- */
112
- describe() {
113
- const ctx = this.context ? ` ${formatContext(this.context)}` : "";
114
- return `${this.code}: ${this.message}${ctx}`;
115
- }
116
- };
117
- function formatContext(ctx) {
118
- const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
119
- return parts.length > 0 ? `[${parts.join(" ")}]` : "";
120
- }
121
- var SddError = class extends WrongStackError {
122
- constructor(opts) {
123
- super({
124
- message: opts.message,
125
- code: opts.code,
126
- subsystem: "sdd",
127
- severity: opts.code === ERROR_CODES.SDD_PARSE_FAILED ? "warning" : "error",
128
- recoverable: opts.code === ERROR_CODES.SDD_NOT_READY,
129
- context: opts.context,
130
- cause: opts.cause
131
- });
132
- this.name = "SddError";
133
- }
134
- };
135
- var FsError = class extends WrongStackError {
136
- path;
137
- constructor(opts) {
138
- super({
139
- message: opts.message,
140
- code: opts.code,
141
- subsystem: "fs",
142
- severity: "error",
143
- recoverable: opts.code !== ERROR_CODES.FS_READ_FAILED,
144
- context: { path: opts.path, ...opts.context },
145
- cause: opts.cause
146
- });
147
- this.name = "FsError";
148
- this.path = opts.path;
149
- }
150
- };
151
-
152
- // src/types/task-graph.ts
153
- function computeTaskProgress(graph) {
154
- let completed = 0;
155
- let pending = 0;
156
- let inProgress = 0;
157
- let blocked = 0;
158
- let failed = 0;
159
- let review = 0;
160
- let estimatedHours = 0;
161
- let actualHours = 0;
162
- for (const n of graph.nodes.values()) {
163
- switch (n.status) {
164
- case "completed":
165
- completed++;
166
- break;
167
- case "pending":
168
- pending++;
169
- break;
170
- case "in_progress":
171
- inProgress++;
172
- break;
173
- case "blocked":
174
- blocked++;
175
- break;
176
- case "failed":
177
- failed++;
178
- break;
179
- case "review":
180
- review++;
181
- break;
182
- }
183
- estimatedHours += n.estimateHours ?? 0;
184
- actualHours += n.actualHours ?? 0;
185
- }
186
- const total = graph.nodes.size;
187
- return {
188
- total,
189
- pending,
190
- inProgress,
191
- blocked,
192
- failed,
193
- review,
194
- completed,
195
- percentComplete: total > 0 ? Math.round(completed / total * 100) : 0,
196
- estimatedHours,
197
- actualHours
198
- };
199
- }
200
-
201
- // src/tasking/task-tracker.ts
202
- var TaskTracker = class _TaskTracker {
203
- constructor(opts) {
204
- this.opts = opts;
205
- }
206
- opts;
207
- graph = null;
208
- transitions = [];
209
- listeners = [];
210
- /**
211
- * Re-entrancy guard for status cascades: true while an auto-block /
212
- * auto-unblock cascade is running. Listener-driven mutations during a
213
- * cascade still record their own primary transition — only their
214
- * cascading is skipped (prevents unbounded recursion).
215
- */
216
- cascading = false;
217
- static MAX_TRANSITIONS = 1e4;
218
- /**
219
- * Subscribe to live task mutations (add / update / status change). Returns an
220
- * unsubscribe fn. This is the hook the board projector uses to stream a live
221
- * snapshot — the tracker was previously fire-and-forget with no observability.
222
- */
223
- subscribe(listener) {
224
- this.listeners.push(listener);
225
- return () => {
226
- const i = this.listeners.indexOf(listener);
227
- if (i >= 0) this.listeners.splice(i, 1);
228
- };
229
- }
230
- notifyChange(change) {
231
- for (const l of this.listeners) {
232
- try {
233
- l(change);
234
- } catch {
235
- }
236
- }
237
- }
238
- /**
239
- * Attach an existing graph (used by PhaseOrchestrator to associate a tracker
240
- * with a phase's pre-built task graph without re-creating it).
241
- */
242
- setGraph(graph) {
243
- this.graph = graph;
244
- }
245
- async createGraph(specId, title) {
246
- this.graph = {
247
- id: crypto.randomUUID(),
248
- specId,
249
- title,
250
- nodes: /* @__PURE__ */ new Map(),
251
- edges: [],
252
- rootNodes: [],
253
- createdAt: Date.now(),
254
- updatedAt: Date.now()
255
- };
256
- await this.opts.store.saveGraph(this.graph);
257
- return this.graph;
258
- }
259
- async loadGraph(id) {
260
- this.graph = await this.opts.store.loadGraph(id);
261
- return this.graph;
262
- }
263
- addNode(node) {
264
- if (!this.graph)
265
- throw new SddError({
266
- message: "No graph loaded",
267
- code: ERROR_CODES.SDD_INVALID_STATE
268
- });
269
- const now = Date.now();
270
- const newNode = {
271
- ...node,
272
- id: crypto.randomUUID(),
273
- status: node.status ?? "pending",
274
- createdAt: now,
275
- updatedAt: now
276
- };
277
- this.graph.nodes.set(newNode.id, newNode);
278
- if (!node.parentId) {
279
- this.graph.rootNodes.push(newNode.id);
280
- }
281
- this.graph.updatedAt = now;
282
- this.persist();
283
- this.notifyChange({ type: "node_added", nodeId: newNode.id, node: newNode });
284
- return newNode;
285
- }
286
- addEdge(from, to, type = "depends_on") {
287
- if (!this.graph)
288
- throw new SddError({
289
- message: "No graph loaded",
290
- code: ERROR_CODES.SDD_INVALID_STATE
291
- });
292
- if (!this.graph.nodes.has(from) || !this.graph.nodes.has(to)) {
293
- throw new SddError({
294
- message: `Cannot add ${type} edge ${from} -> ${to}: endpoint task is not in the graph`,
295
- code: ERROR_CODES.SDD_NOT_READY,
296
- context: { from, to, type }
297
- });
298
- }
299
- if (from === to) {
300
- throw new SddError({
301
- message: `Cannot add ${type} edge: self-loop on task ${from}`,
302
- code: ERROR_CODES.SDD_INVALID_STATE,
303
- context: { nodeId: from, type }
304
- });
305
- }
306
- if (this.graph.edges.some((e) => e.from === from && e.to === to && e.type === type)) return;
307
- if (type === "depends_on" && this.dependsOnTransitively(from, to, /* @__PURE__ */ new Set())) {
308
- throw new SddError({
309
- message: `Cannot add depends_on edge ${from} -> ${to}: it would create a dependency cycle`,
310
- code: ERROR_CODES.SDD_INVALID_STATE,
311
- context: { from, to }
312
- });
313
- }
314
- this.graph.edges.push({
315
- id: crypto.randomUUID(),
316
- from,
317
- to,
318
- type
319
- });
320
- this.graph.updatedAt = Date.now();
321
- this.persist();
322
- }
323
- /**
324
- * Declare that `taskId` depends on `depId` (a `depends_on` edge `depId → taskId`),
325
- * guarding against self-loops, duplicates, missing nodes, and cycles. Returns
326
- * true if the dependency now holds (added or already present), false if it was
327
- * rejected (would create a cycle / unknown node). This is the safe entry point
328
- * for wiring agent-declared `dependsOn` references into the graph.
329
- */
330
- addDependency(depId, taskId) {
331
- if (!this.graph) return false;
332
- if (depId === taskId) return false;
333
- if (!this.graph.nodes.has(depId) || !this.graph.nodes.has(taskId)) return false;
334
- if (this.getBlockers(taskId).includes(depId)) return true;
335
- if (this.dependsOnTransitively(depId, taskId, /* @__PURE__ */ new Set())) return false;
336
- this.addEdge(depId, taskId, "depends_on");
337
- return true;
338
- }
339
- /** True when `taskId` transitively depends on `targetId` (follows depends_on blockers). */
340
- dependsOnTransitively(taskId, targetId, seen) {
341
- if (taskId === targetId) return true;
342
- if (seen.has(taskId)) return false;
343
- seen.add(taskId);
344
- for (const blocker of this.getBlockers(taskId)) {
345
- if (this.dependsOnTransitively(blocker, targetId, seen)) return true;
346
- }
347
- return false;
348
- }
349
- /**
350
- * Merge `patch` into a node's `metadata` (used for per-task model/provider/
351
- * fallback assignment and the cancel marker). Persists + notifies as a node
352
- * update. No-op if the node is missing.
353
- */
354
- patchMetadata(id, patch) {
355
- if (!this.graph) return;
356
- const node = this.graph.nodes.get(id);
357
- if (!node) return;
358
- node.metadata = { ...node.metadata, ...patch };
359
- node.updatedAt = Date.now();
360
- this.graph.updatedAt = node.updatedAt;
361
- this.persist();
362
- this.notifyChange({ type: "node_updated", nodeId: id, node });
363
- }
364
- /**
365
- * Remove a node and every edge touching it. Intended for deleting a task that
366
- * has not started yet — callers must gate on status (do not remove a running
367
- * task). Dependents simply lose this blocker (re-evaluated by `canStart`).
368
- * Returns true if a node was removed.
369
- */
370
- removeNode(id) {
371
- if (!this.graph) return false;
372
- const node = this.graph.nodes.get(id);
373
- if (!node) return false;
374
- if (node.specRequirementId && this.graph.requiredRequirementIds?.includes(node.specRequirementId) && !Array.from(this.graph.nodes.values()).some(
375
- (candidate) => candidate.id !== id && candidate.specRequirementId === node.specRequirementId
376
- )) {
377
- return false;
378
- }
379
- this.graph.nodes.delete(id);
380
- this.graph.edges = this.graph.edges.filter((e) => e.from !== id && e.to !== id);
381
- this.graph.rootNodes = this.graph.rootNodes.filter((r) => r !== id);
382
- for (const n of this.graph.nodes.values()) {
383
- if (n.children?.includes(id)) n.children = n.children.filter((c) => c !== id);
384
- }
385
- this.graph.updatedAt = Date.now();
386
- this.persist();
387
- this.notifyChange({ type: "node_removed", nodeId: id, node });
388
- return true;
389
- }
390
- updateNodeStatus(id, status, reason) {
391
- if (!this.graph)
392
- throw new SddError({
393
- message: "No graph loaded",
394
- code: ERROR_CODES.SDD_INVALID_STATE
395
- });
396
- const node = this.graph.nodes.get(id);
397
- if (!node)
398
- throw new SddError({
399
- message: `Node ${id} not found`,
400
- code: ERROR_CODES.SDD_NOT_READY,
401
- context: { nodeId: id }
402
- });
403
- const from = node.status;
404
- const now = Date.now();
405
- node.status = status;
406
- node.updatedAt = now;
407
- if (status === "completed") {
408
- node.completedAt = now;
409
- node.startedAt = node.startedAt ?? now;
410
- }
411
- if (status === "in_progress") {
412
- node.startedAt = now;
413
- }
414
- this.transitions.push({ nodeId: id, from, to: status, timestamp: now, reason });
415
- if (this.transitions.length > _TaskTracker.MAX_TRANSITIONS) {
416
- this.transitions.splice(0, this.transitions.length - _TaskTracker.MAX_TRANSITIONS);
417
- }
418
- if (status === "completed") {
419
- this.unblockDependents(id);
420
- }
421
- if (status === "in_progress") {
422
- this.checkAndBlockIfNeeded(id);
423
- }
424
- this.graph.updatedAt = now;
425
- this.persist();
426
- this.notifyChange({
427
- type: "status_changed",
428
- nodeId: id,
429
- node,
430
- transition: { nodeId: id, from, to: status, timestamp: now, reason }
431
- });
432
- }
433
- updateNode(id, patch) {
434
- if (!this.graph)
435
- throw new SddError({
436
- message: "No graph loaded",
437
- code: ERROR_CODES.SDD_INVALID_STATE
438
- });
439
- const node = this.graph.nodes.get(id);
440
- if (!node)
441
- throw new SddError({
442
- message: `Node ${id} not found`,
443
- code: ERROR_CODES.SDD_NOT_READY,
444
- context: { nodeId: id }
445
- });
446
- if (patch.title !== void 0) node.title = patch.title;
447
- if (patch.description !== void 0) node.description = patch.description;
448
- if (patch.priority !== void 0) node.priority = patch.priority;
449
- if (patch.estimateHours !== void 0) node.estimateHours = patch.estimateHours;
450
- if (patch.tags !== void 0) node.tags = patch.tags;
451
- if (patch.assignee !== void 0) node.assignee = patch.assignee;
452
- node.updatedAt = Date.now();
453
- this.graph.updatedAt = node.updatedAt;
454
- this.persist();
455
- this.notifyChange({ type: "node_updated", nodeId: id, node });
456
- }
457
- getNode(id) {
458
- return this.graph?.nodes.get(id);
459
- }
460
- getAllNodes(filter, sort) {
461
- if (!this.graph) return [];
462
- let nodes = Array.from(this.graph.nodes.values());
463
- if (filter) {
464
- nodes = nodes.filter((n) => {
465
- if (filter.status?.length && !filter.status.includes(n.status)) return false;
466
- if (filter.priority?.length && !filter.priority.includes(n.priority)) return false;
467
- if (filter.type?.length && !filter.type.includes(n.type)) return false;
468
- if (filter.assignee?.length && n.assignee && !filter.assignee.includes(n.assignee))
469
- return false;
470
- if (filter.tags?.length && n.tags && !n.tags.some((t) => filter.tags?.includes(t)))
471
- return false;
472
- if (filter.specRequirementId && n.specRequirementId !== filter.specRequirementId)
473
- return false;
474
- return true;
475
- });
476
- }
477
- if (sort) {
478
- nodes.sort((a, b) => {
479
- const cmp = compareByField(a, b, sort.field);
480
- return sort.direction === "asc" ? cmp : -cmp;
481
- });
482
- }
483
- return nodes;
484
- }
485
- getChildren(parentId) {
486
- if (!this.graph) return [];
487
- return Array.from(this.graph.nodes.values()).filter((n) => n.parentId === parentId);
488
- }
489
- getDependents(taskId) {
490
- if (!this.graph) return [];
491
- return this.graph.edges.filter((e) => e.from === taskId && e.type === "depends_on").map((e) => e.to);
492
- }
493
- getBlockers(taskId) {
494
- if (!this.graph) return [];
495
- return this.graph.edges.filter((e) => e.to === taskId && e.type === "depends_on").map((e) => e.from);
496
- }
497
- canStart(taskId) {
498
- const blockers = this.getBlockers(taskId);
499
- return blockers.every((id) => {
500
- const node = this.graph?.nodes.get(id);
501
- return node?.status === "completed" || node?.status === "failed";
502
- });
503
- }
504
- getProgress() {
505
- if (!this.graph) {
506
- return {
507
- total: 0,
508
- pending: 0,
509
- inProgress: 0,
510
- blocked: 0,
511
- failed: 0,
512
- review: 0,
513
- completed: 0,
514
- percentComplete: 0,
515
- estimatedHours: 0,
516
- actualHours: 0
517
- };
518
- }
519
- return computeTaskProgress(this.graph);
520
- }
521
- /**
522
- * Recorded transitions in insertion order (oldest first — the array is
523
- * trimmed from the front). With `taskId`, only that task's transitions are
524
- * returned, still oldest-first; a consumer wanting newest-first reverses
525
- * the result. Per-task filtering is possible because every TaskTransition
526
- * carries `nodeId` (BIZ-005).
527
- */
528
- getTransitions(taskId) {
529
- return taskId === void 0 ? [...this.transitions] : this.transitions.filter((t) => t.nodeId === taskId);
530
- }
531
- /**
532
- * Record a cascade-driven status change (BIZ-003): mutate the node, push a
533
- * TaskTransition, persist, and notify subscribers — the same effects an
534
- * explicit updateNodeStatus produces — so the board projector's live
535
- * snapshot and the transition log observe auto-block/auto-unblock.
536
- */
537
- recordCascadeStatusChange(nodeId, node, to, reason) {
538
- const from = node.status;
539
- if (from === to) return;
540
- const now = Date.now();
541
- node.status = to;
542
- node.updatedAt = now;
543
- if (this.graph) this.graph.updatedAt = now;
544
- this.transitions.push({ nodeId, from, to, timestamp: now, reason });
545
- if (this.transitions.length > _TaskTracker.MAX_TRANSITIONS) {
546
- this.transitions.splice(0, this.transitions.length - _TaskTracker.MAX_TRANSITIONS);
547
- }
548
- this.persist();
549
- this.notifyChange({
550
- type: "status_changed",
551
- nodeId,
552
- node,
553
- transition: { nodeId, from, to, timestamp: now, reason }
554
- });
555
- }
556
- unblockDependents(completedId) {
557
- if (!this.graph || this.cascading) return;
558
- this.cascading = true;
559
- try {
560
- const dependents = this.getDependents(completedId);
561
- for (const depId of dependents) {
562
- const dep = this.graph.nodes.get(depId);
563
- if (dep?.status !== "blocked") continue;
564
- const remainingBlockers = this.getBlockers(depId);
565
- const allUnblocked = remainingBlockers.every((id) => {
566
- const blocker = this.graph?.nodes.get(id);
567
- return blocker?.status === "completed" || blocker?.status === "failed";
568
- });
569
- if (allUnblocked) {
570
- this.recordCascadeStatusChange(
571
- depId,
572
- dep,
573
- "pending",
574
- `auto-unblocked: blocker ${completedId} reached a terminal state`
575
- );
576
- }
577
- }
578
- } finally {
579
- this.cascading = false;
580
- }
581
- }
582
- checkAndBlockIfNeeded(taskId) {
583
- if (!this.graph || this.cascading) return;
584
- this.cascading = true;
585
- try {
586
- const blockers = this.getBlockers(taskId);
587
- const incomplete = blockers.filter((id) => {
588
- const blocker = this.graph?.nodes.get(id);
589
- return blocker?.status !== "completed" && blocker?.status !== "failed";
590
- });
591
- if (incomplete.length > 0) {
592
- const node = this.graph.nodes.get(taskId);
593
- if (node) {
594
- this.recordCascadeStatusChange(
595
- taskId,
596
- node,
597
- "blocked",
598
- `auto-blocked: incomplete blocker(s) ${incomplete.join(", ")}`
599
- );
600
- }
601
- }
602
- } finally {
603
- this.cascading = false;
604
- }
605
- }
606
- /**
607
- * Fire-and-forget persistence with attached error handler.
608
- * Synchronous mutators (addNode/addEdge/updateNodeStatus) use this to
609
- * avoid forcing an async cascade through every caller; if the store
610
- * is missing or throwing, the error is surfaced via onPersistError.
611
- */
612
- persist() {
613
- if (!this.graph) return;
614
- this.opts.store.saveGraph(this.graph).catch((err) => {
615
- this.opts.onPersistError ? this.opts.onPersistError(err) : console.warn(
616
- JSON.stringify({
617
- level: "warn",
618
- event: "task_tracker.save_graph_failed",
619
- message: toErrorMessage(err),
620
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
621
- })
622
- );
623
- });
624
- }
625
- };
626
- var PRIORITY_RANK = {
627
- critical: 0,
628
- high: 1,
629
- medium: 2,
630
- low: 3
631
- };
632
- var STATUS_RANK = {
633
- in_progress: 0,
634
- pending: 1,
635
- review: 2,
636
- blocked: 3,
637
- failed: 4,
638
- completed: 5
639
- };
640
- function compareByField(a, b, field) {
641
- switch (field) {
642
- case "priority":
643
- return PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority];
644
- case "status":
645
- return STATUS_RANK[a.status] - STATUS_RANK[b.status];
646
- case "createdAt":
647
- return a.createdAt - b.createdAt;
648
- case "updatedAt":
649
- return a.updatedAt - b.updatedAt;
650
- }
651
- }
652
-
653
- // src/goal/phase-graph-builder.ts
654
- var PhaseGraphBuilder = class _PhaseGraphBuilder {
655
- constructor(opts) {
656
- this.opts = opts;
657
- }
658
- opts;
659
- async build() {
660
- const graphId = crypto.randomUUID();
661
- const phases = /* @__PURE__ */ new Map();
662
- const phaseIds = [];
663
- for (let i = 0; i < this.opts.phases.length; i++) {
664
- const tmpl = this.opts.phases[i] ?? { name: "", description: "", tasks: [], taskTemplates: [], parallelizable: false, priority: "medium", estimateHours: 0 };
665
- const phaseId = crypto.randomUUID();
666
- phaseIds.push(phaseId);
667
- const store = this.opts.externalTaskStore ?? new DefaultTaskStore();
668
- const tracker = new TaskTracker({ store });
669
- const taskGraph = await tracker.createGraph(phaseId, `${tmpl.name} Tasks`);
670
- if (tmpl.taskTemplates && tmpl.taskTemplates.length > 0) {
671
- for (const tt of tmpl.taskTemplates) {
672
- tracker.addNode({
673
- title: tt.title,
674
- description: tt.description,
675
- type: tt.type,
676
- priority: tt.priority,
677
- status: "pending",
678
- estimateHours: tt.estimateHours,
679
- tags: tt.tags ?? []
680
- });
681
- }
682
- }
683
- const phase = {
684
- id: phaseId,
685
- name: tmpl.name,
686
- description: tmpl.description,
687
- status: "pending",
688
- taskGraph,
689
- dependsOn: i > 0 ? [phaseIds[i - 1] ?? ""] : [],
690
- nextPhases: i < this.opts.phases.length - 1 ? [phaseIds[i + 1] ?? ""] : [],
691
- parallelizable: tmpl.parallelizable,
692
- priority: tmpl.priority,
693
- estimateHours: tmpl.estimateHours,
694
- assignedAgents: [],
695
- createdAt: Date.now(),
696
- updatedAt: Date.now()
697
- };
698
- phases.set(phaseId, phase);
699
- }
700
- const phaseArray = Array.from(phases.values());
701
- for (let i = 0; i < phaseArray.length; i++) {
702
- const phase = phaseArray[i];
703
- if (!phase) continue;
704
- phase.nextPhases = i < phaseArray.length - 1 ? [phaseArray[i + 1]?.id ?? ""] : [];
705
- }
706
- const graph = {
707
- id: graphId,
708
- title: this.opts.title,
709
- description: this.opts.description ?? "",
710
- phases,
711
- rootPhaseIds: phaseIds.length > 0 ? [phaseIds[0] ?? ""] : [],
712
- activePhaseIds: [],
713
- completedPhaseIds: [],
714
- failedPhaseIds: [],
715
- autonomous: this.opts.autonomous ?? true,
716
- stopOnComplete: true,
717
- multiBoard: this.opts.multiBoard,
718
- verifyTasks: this.opts.verifyTasks,
719
- chimeraReview: this.opts.chimeraReview,
720
- createdAt: Date.now(),
721
- updatedAt: Date.now()
722
- };
723
- return graph;
724
- }
725
- /**
726
- * Create phases from an existing TaskGraph.
727
- * Groups tasks into phases using topological sort so dependencies
728
- * stay in the same phase wherever possible.
729
- */
730
- static fromTaskGraph(taskGraph, options) {
731
- const tasksPerPhase = options.tasksPerPhase ?? 5;
732
- const nodes = Array.from(taskGraph.nodes.values());
733
- const dependents = /* @__PURE__ */ new Map();
734
- for (const edge of taskGraph.edges) {
735
- const list = dependents.get(edge.from) ?? [];
736
- list.push(edge.to);
737
- dependents.set(edge.from, list);
738
- }
739
- const inDegree = /* @__PURE__ */ new Map();
740
- for (const node of nodes) inDegree.set(node.id, 0);
741
- for (const edge of taskGraph.edges) {
742
- inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1);
743
- }
744
- const queue = [];
745
- for (const [id, degree] of inDegree) {
746
- if (degree === 0) queue.push(id);
747
- }
748
- const prioOrder = {
749
- critical: 0,
750
- high: 1,
751
- medium: 2,
752
- low: 3
753
- };
754
- const topoSorted = [];
755
- while (queue.length > 0) {
756
- queue.sort((a, b) => {
757
- const na = taskGraph.nodes.get(a);
758
- const nb = taskGraph.nodes.get(b);
759
- const naPrio = na ? prioOrder[na.priority] : 4;
760
- const nbPrio = nb ? prioOrder[nb.priority] : 4;
761
- return naPrio - nbPrio;
762
- });
763
- const id = queue.shift();
764
- topoSorted.push(id);
765
- for (const depId of dependents.get(id) ?? []) {
766
- const deg = (inDegree.get(depId) ?? 1) - 1;
767
- inDegree.set(depId, deg);
768
- if (deg === 0) queue.push(depId);
769
- }
770
- }
771
- const groups = [];
772
- let currentGroup = [];
773
- const assigned = /* @__PURE__ */ new Set();
774
- for (const id of topoSorted) {
775
- if (assigned.has(id)) continue;
776
- assigned.add(id);
777
- currentGroup.push(id);
778
- const deps = dependents.get(id) ?? [];
779
- for (const depId of deps) {
780
- if (!assigned.has(depId) && currentGroup.length < tasksPerPhase) {
781
- assigned.add(depId);
782
- currentGroup.push(depId);
783
- }
784
- }
785
- if (currentGroup.length >= tasksPerPhase) {
786
- groups.push(currentGroup);
787
- currentGroup = [];
788
- }
789
- }
790
- if (currentGroup.length > 0) groups.push(currentGroup);
791
- const phaseTemplates = groups.map((groupIds, idx) => {
792
- const group = groupIds.map((id) => taskGraph.nodes.get(id)).filter((n) => n != null);
793
- const hasCritical = group.some((t) => t.priority === "critical");
794
- const totalHours = group.reduce((sum, t) => sum + (t.estimateHours ?? 2), 0);
795
- return {
796
- name: `Phase ${idx + 1}: ${group[0]?.title.slice(0, 30) ?? "Tasks"}`,
797
- description: group.map((t) => t.title).join(", "),
798
- priority: hasCritical ? "critical" : "high",
799
- estimateHours: totalHours,
800
- parallelizable: false,
801
- taskTemplates: group.map((t) => ({
802
- title: t.title,
803
- description: t.description,
804
- type: t.type,
805
- priority: t.priority,
806
- estimateHours: t.estimateHours ?? 2,
807
- tags: t.tags ?? []
808
- }))
809
- };
810
- });
811
- const builder = new _PhaseGraphBuilder({
812
- ...options,
813
- phases: phaseTemplates
814
- });
815
- return builder.build();
816
- }
817
- };
818
-
819
- // src/infrastructure/logger.ts
820
- import * as fsp from "node:fs/promises";
821
- import * as path from "node:path";
822
-
823
- // src/utils/term.ts
824
- var hasStdout = () => typeof process !== "undefined" && !!process.stdout;
825
- function isStdoutTTY() {
826
- return hasStdout() && Boolean(process.stdout.isTTY);
827
- }
828
- var activeOutputGuard = null;
829
- function writeTo(s, stream) {
830
- if (!stream || typeof stream.write !== "function") return false;
831
- const guard = activeOutputGuard;
832
- if (!guard) {
833
- stream.write(s);
834
- return true;
835
- }
836
- guard.suspend();
837
- stream.write(s);
838
- guard.resume();
839
- return true;
840
- }
841
- function writeErr(s, stream = process.stderr) {
842
- return writeTo(s, stream);
843
- }
844
- var ESCAPE_TERMINATOR = Object.freeze({
845
- BEL: "\x07",
846
- // OSC 0 / OSC 2 (title)
847
- ST: "\x1B\\"
848
- // DCS / strict OSC
849
- });
850
-
851
- // src/utils/color.ts
852
- var isColorTty = () => {
853
- if (envFlag(process.env.NO_COLOR)) return false;
854
- if (envFlag(process.env.FORCE_COLOR)) return true;
855
- return isStdoutTTY();
856
- };
857
- function envFlag(value) {
858
- if (value === void 0) return false;
859
- if (value.trim() === "") return false;
860
- return !/^(0|false|no|off)$/i.test(value.trim());
861
- }
862
- var COLOR = isColorTty();
863
- var wrap = (open, close) => (s) => COLOR ? `\x1B[${open}m${s}\x1B[${close}m` : s;
864
- var color = {
865
- reset: wrap("0", "0"),
866
- bold: wrap("1", "22"),
867
- dim: wrap("2", "22"),
868
- italic: wrap("3", "23"),
869
- underline: wrap("4", "24"),
870
- red: wrap("31", "39"),
871
- green: wrap("32", "39"),
872
- yellow: wrap("33", "39"),
873
- blue: wrap("34", "39"),
874
- magenta: wrap("35", "39"),
875
- cyan: wrap("36", "39"),
876
- gray: wrap("90", "39"),
877
- amber: wrap("38;5;214", "39"),
878
- pink: wrap("38;5;205", "39"),
879
- bgRed: wrap("41", "49"),
880
- bgGreen: wrap("42", "49")
881
- };
882
-
883
- // src/infrastructure/logger.ts
884
- var LEVEL_RANK = {
885
- error: 0,
886
- warn: 1,
887
- info: 2,
888
- debug: 3,
889
- trace: 4
890
- };
891
- var COLORS = {
892
- error: color.red,
893
- warn: color.yellow,
894
- info: color.cyan,
895
- debug: color.gray,
896
- trace: color.dim
897
- };
898
- var LOG_LEVELS = /* @__PURE__ */ new Set(["error", "warn", "info", "debug", "trace"]);
899
- var LOG_FORMATS = /* @__PURE__ */ new Set(["pretty", "json"]);
900
- var DefaultLogger = class _DefaultLogger {
901
- /** How many file writes between rotation size checks (statSync is not free). */
902
- static ROTATE_CHECK_EVERY = 100;
903
- static MAX_PENDING_FILE_WRITES = 2e3;
904
- static MAX_PENDING_FILE_BYTES = 8 * 1024 * 1024;
905
- level;
906
- file;
907
- bindings;
908
- format;
909
- stderr;
910
- maxFileBytes;
911
- writesSinceRotateCheck = 0;
912
- /**
913
- * Serialized async tail for file writes. Every appendFile (and any
914
- * chained rotation) is awaited through this promise so file I/O
915
- * never overlaps itself — preserving the per-line ordering the
916
- * sync version had, but without blocking the caller thread. Any
917
- * rejection is swallowed (`catch(() => {})`) because logging must
918
- * never crash the host.
919
- *
920
- * Children share the parent's tail: `child.tail === parent.tail`
921
- * for the lifetime of the chain. Read/write access goes through
922
- * `_tail` so that, when a child has been wired to a parent, both
923
- * `enqueueRotate` and `log` always observe the parent's current tail
924
- * rather than a stale snapshot taken at `child()` time.
925
- */
926
- tail = Promise.resolve();
927
- parent = null;
928
- pendingFileWrites = 0;
929
- pendingFileBytes = 0;
930
- get root() {
931
- return this.parent ? this.parent.root : this;
932
- }
933
- /**
934
- * Resolve the current tail. For the root logger this is the field;
935
- * for a child logger we always read through the parent so that a
936
- * child's appends land on the parent's most recent tail, and a
937
- * parent's `flush()` waits for everything the child chained.
938
- */
939
- get _tail() {
940
- return this.parent ? this.parent._tail : this.tail;
941
- }
942
- set _tail(next) {
943
- if (this.parent) this.parent.tail = next;
944
- else this.tail = next;
945
- }
946
- constructor(opts = {}) {
947
- this.level = opts.level ?? parseLogLevel(process.env.WRONGSTACK_LOG_LEVEL);
948
- this.file = opts.file;
949
- this.bindings = opts.bindings ?? {};
950
- this.format = opts.format ?? parseLogFormat(process.env.WRONGSTACK_LOG_FORMAT);
951
- this.stderr = opts.stderr !== false;
952
- this.maxFileBytes = opts.maxFileBytes ?? 10 * 1024 * 1024;
953
- if (this.file) {
954
- const dir = path.dirname(this.file);
955
- this._tail = this._tail.then(async () => {
956
- await fsp.mkdir(dir, { recursive: true });
957
- }).catch(() => void 0);
958
- }
959
- }
960
- error(msg, ctx) {
961
- this.log("error", msg, ctx);
962
- }
963
- warn(msg, ctx) {
964
- this.log("warn", msg, ctx);
965
- }
966
- info(msg, ctx) {
967
- this.log("info", msg, ctx);
968
- }
969
- debug(msg, ctx) {
970
- this.log("debug", msg, ctx);
971
- }
972
- trace(msg, ctx) {
973
- this.log("trace", msg, ctx);
974
- }
975
- child(bindings) {
976
- const child = Object.create(_DefaultLogger.prototype);
977
- child.level = this.level;
978
- child.file = this.file;
979
- child.bindings = { ...this.bindings, ...bindings };
980
- child.format = this.format;
981
- child.stderr = this.stderr;
982
- child.maxFileBytes = this.maxFileBytes;
983
- child.parent = this;
984
- child.writesSinceRotateCheck = this.writesSinceRotateCheck;
985
- return child;
986
- }
987
- /**
988
- * Wait until all queued file writes (and any pending rotation) have
989
- * completed. `log()` is fire-and-forget by design — the caller never
990
- * blocks on disk — so tests, shutdown handlers, and processes that
991
- * need a deterministic "everything is on disk now" guarantee should
992
- * `await logger.flush()` before reading the file or exiting.
993
- */
994
- flush() {
995
- return this._tail;
996
- }
997
- /**
998
- * Size-based rotation: when the file outgrows `maxFileBytes`, rename it to
999
- * `<file>.1` (dropping the previous `.1`) so the live file restarts empty.
1000
- * Checked on the first write and every ROTATE_CHECK_EVERY writes after.
1001
- * Best-effort: a rename can fail on Windows while another process holds
1002
- * the file — the next check retries. Multiple processes appending to the
1003
- * same log all run this check; whoever crosses the threshold first wins.
1004
- *
1005
- * Async: the rotation runs on the file-write tail (so its writes don't
1006
- * interleave with the next append), and the caller never blocks on a
1007
- * statSync / renameSync syscall on the hot log path.
1008
- */
1009
- enqueueRotate(file) {
1010
- if (this.writesSinceRotateCheck++ % _DefaultLogger.ROTATE_CHECK_EVERY !== 0) return;
1011
- this._tail = this._tail.then(async () => {
1012
- let st;
1013
- try {
1014
- st = await fsp.stat(file);
1015
- } catch {
1016
- return;
1017
- }
1018
- if (st.size < this.maxFileBytes) return;
1019
- try {
1020
- await fsp.rm(`${file}.1`, { force: true });
1021
- await fsp.rename(file, `${file}.1`);
1022
- } catch {
1023
- }
1024
- }).catch(() => void 0);
1025
- }
1026
- log(level, msg, ctx) {
1027
- const r = LEVEL_RANK[level];
1028
- const allowed = LEVEL_RANK[this.level];
1029
- if (r > allowed) return;
1030
- const ts = (/* @__PURE__ */ new Date()).toISOString();
1031
- const entry = { ts, level, msg, ...this.bindings };
1032
- if (ctx !== void 0) {
1033
- entry.ctx = ctx instanceof Error ? { message: ctx.message, stack: ctx.stack } : ctx;
1034
- }
1035
- if (this.file) {
1036
- const line = `${JSON.stringify(entry)}
1037
- `;
1038
- const bytes = Buffer.byteLength(line, "utf8");
1039
- const root = this.root;
1040
- if (root.pendingFileWrites < _DefaultLogger.MAX_PENDING_FILE_WRITES && bytes <= _DefaultLogger.MAX_PENDING_FILE_BYTES && root.pendingFileBytes + bytes <= _DefaultLogger.MAX_PENDING_FILE_BYTES) {
1041
- root.pendingFileWrites += 1;
1042
- root.pendingFileBytes += bytes;
1043
- this.enqueueRotate(this.file);
1044
- this._tail = this._tail.then(() => fsp.appendFile(this.file, line, { mode: 384 })).catch(() => void 0).finally(() => {
1045
- root.pendingFileWrites = Math.max(0, root.pendingFileWrites - 1);
1046
- root.pendingFileBytes = Math.max(0, root.pendingFileBytes - bytes);
1047
- });
1048
- }
1049
- }
1050
- if (!this.stderr) return;
1051
- if (this.format === "json") {
1052
- writeErr(`${JSON.stringify(entry)}
1053
- `);
1054
- } else {
1055
- const head = `${color.dim(ts)} ${COLORS[level](level.toUpperCase().padEnd(5))} ${msg}`;
1056
- if (ctx !== void 0) {
1057
- writeErr(`${head} ${formatCtx(ctx)}
1058
- `);
1059
- } else {
1060
- writeErr(`${head}
1061
- `);
1062
- }
1063
- }
1064
- }
1065
- };
1066
- function parseLogLevel(raw) {
1067
- return raw && LOG_LEVELS.has(raw) ? raw : "info";
1068
- }
1069
- function parseLogFormat(raw) {
1070
- return raw && LOG_FORMATS.has(raw) ? raw : "pretty";
1071
- }
1072
- function formatCtx(ctx) {
1073
- if (ctx instanceof Error) return color.dim(ctx.message);
1074
- if (typeof ctx === "string") return color.dim(ctx);
1075
- try {
1076
- return color.dim(JSON.stringify(ctx));
1077
- } catch {
1078
- return color.dim(String(ctx));
1079
- }
1080
- }
1081
- var noOpLogger = {
1082
- // 'error' is the quietest level the Logger contract offers; the methods
1083
- // discard everything regardless, this only matters to level checks.
1084
- level: "error",
1085
- error: () => {
1086
- },
1087
- warn: () => {
1088
- },
1089
- info: () => {
1090
- },
1091
- debug: () => {
1092
- },
1093
- trace: () => {
1094
- },
1095
- child: () => noOpLogger
1096
- };
1097
-
1098
- // src/goal/phase-board-mutations.ts
1099
- function findPhaseOfTaskInGraph(graph, taskId) {
1100
- for (const phase of graph.phases.values()) {
1101
- if (phase.taskGraph.nodes.has(taskId)) return phase;
1102
- }
1103
- return void 0;
1104
- }
1105
- function movePhaseTask(ctx, taskId, toPhaseId) {
1106
- const from = findPhaseOfTaskInGraph(ctx.graph, taskId);
1107
- const to = ctx.graph.phases.get(toPhaseId);
1108
- if (!from || !to || from.id === toPhaseId) return false;
1109
- const node = from.taskGraph.nodes.get(taskId);
1110
- if (!node) return false;
1111
- const dependents = from.taskGraph.edges.filter((e) => e.from === taskId).map((e) => e.to);
1112
- for (const depId of dependents) {
1113
- const depNode = from.taskGraph.nodes.get(depId);
1114
- if (depNode) {
1115
- depNode.parentId = void 0;
1116
- depNode.children = void 0;
1117
- }
1118
- from.taskGraph.edges = from.taskGraph.edges.filter(
1119
- (e) => !(e.from === taskId && e.to === depId)
1120
- );
1121
- if (!from.taskGraph.rootNodes.includes(depId)) {
1122
- from.taskGraph.rootNodes.push(depId);
1123
- }
1124
- }
1125
- from.taskGraph.nodes.delete(taskId);
1126
- from.taskGraph.rootNodes = from.taskGraph.rootNodes.filter((id) => id !== taskId);
1127
- from.taskGraph.edges = from.taskGraph.edges.filter((e) => e.from !== taskId && e.to !== taskId);
1128
- from.taskGraph.updatedAt = Date.now();
1129
- node.parentId = void 0;
1130
- node.children = void 0;
1131
- node.updatedAt = Date.now();
1132
- to.taskGraph.nodes.set(taskId, node);
1133
- to.taskGraph.rootNodes.push(taskId);
1134
- to.taskGraph.updatedAt = Date.now();
1135
- ctx.trackerCache.delete(from.id);
1136
- ctx.trackerCache.delete(to.id);
1137
- ctx.graph.updatedAt = Date.now();
1138
- ctx.emit("phase.taskMoved", { taskId, fromPhaseId: from.id, toPhaseId });
1139
- return true;
1140
- }
1141
- function setPhaseTaskAssignee(ctx, taskId, agentId, agentName) {
1142
- const phase = findPhaseOfTaskInGraph(ctx.graph, taskId);
1143
- if (!phase) return false;
1144
- const tracker = ctx.getTrackerForPhase(phase);
1145
- tracker.updateNode(taskId, { assignee: agentName ?? agentId ?? "" });
1146
- ctx.graph.updatedAt = Date.now();
1147
- ctx.emit("phase.taskAssigned", {
1148
- phaseId: phase.id,
1149
- taskId,
1150
- ...agentId !== void 0 ? { agentId } : {},
1151
- ...agentName !== void 0 ? { agentName } : {}
1152
- });
1153
- return true;
1154
- }
1155
- function addPhaseTask(ctx, phaseId, spec) {
1156
- const phase = ctx.graph.phases.get(phaseId);
1157
- if (!phase) return null;
1158
- const tracker = ctx.getTrackerForPhase(phase);
1159
- const node = tracker.addNode({
1160
- title: spec.title,
1161
- description: spec.description ?? "",
1162
- type: spec.type ?? "feature",
1163
- priority: spec.priority ?? "medium",
1164
- status: "pending"
1165
- });
1166
- ctx.graph.updatedAt = Date.now();
1167
- ctx.emit("phase.taskAdded", { phaseId, taskId: node.id, taskTitle: node.title });
1168
- return node.id;
1169
- }
1170
- function requeuePhaseTask(ctx, taskId) {
1171
- const phase = findPhaseOfTaskInGraph(ctx.graph, taskId);
1172
- if (!phase) return false;
1173
- const tracker = ctx.getTrackerForPhase(phase);
1174
- tracker.updateNodeStatus(taskId, "pending");
1175
- ctx.taskRetryCounts.delete(`${phase.id}:${taskId}`);
1176
- if (phase.status === "completed" || phase.status === "failed" || phase.status === "paused") {
1177
- ctx.graph.failedPhaseIds = ctx.graph.failedPhaseIds.filter((id) => id !== phase.id);
1178
- ctx.graph.completedPhaseIds = ctx.graph.completedPhaseIds.filter((id) => id !== phase.id);
1179
- ctx.graph.activePhaseIds = ctx.graph.activePhaseIds.filter((id) => id !== phase.id);
1180
- ctx.phaseWorktrees.delete(phase.id);
1181
- ctx.updatePhaseStatus(phase, "pending");
1182
- }
1183
- ctx.graph.updatedAt = Date.now();
1184
- return true;
1185
- }
1186
-
1187
- // src/goal/phase-orchestrator-queries.ts
1188
- function getReadyPhases(graph) {
1189
- const ready = [];
1190
- for (const phase of graph.phases.values()) {
1191
- if (phase.status !== "pending") continue;
1192
- const depsDone = phase.dependsOn.every((depId) => {
1193
- const dep = graph.phases.get(depId);
1194
- return dep?.status === "completed" || dep?.status === "skipped";
1195
- });
1196
- if (depsDone) {
1197
- ready.push(phase);
1198
- }
1199
- }
1200
- const prioOrder = { critical: 0, high: 1, medium: 2, low: 3 };
1201
- ready.sort((a, b) => (prioOrder[a.priority] ?? 4) - (prioOrder[b.priority] ?? 4));
1202
- return ready;
1203
- }
1204
- function getActivePhases(graph) {
1205
- return Array.from(graph.phases.values()).filter((p) => p.status === "running");
1206
- }
1207
- function getExecutableTasks(trackerFor, phase) {
1208
- const tracker = trackerFor(phase);
1209
- return tracker.getAllNodes({ status: ["pending", "blocked"] }).filter(
1210
- (n) => n.status === "pending" && tracker.canStart(n.id)
1211
- ).sort((a, b) => {
1212
- const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
1213
- return (priorityOrder[a.priority] ?? 4) - (priorityOrder[b.priority] ?? 4);
1214
- });
1215
- }
1216
- function getFailedTaskCount(trackerFor, phase) {
1217
- return trackerFor(phase).getAllNodes({ status: ["failed"] }).length;
1218
- }
1219
- function getCompletedTaskCount(trackerFor, phase) {
1220
- return trackerFor(phase).getAllNodes({ status: ["completed"] }).length;
1221
- }
1222
- function isGraphComplete(graph) {
1223
- const allPhases = Array.from(graph.phases.values());
1224
- return allPhases.every((p) => p.status === "completed" || p.status === "skipped");
1225
- }
1226
- function getProgress(graph, trackerFor) {
1227
- const phases = Array.from(graph.phases.values());
1228
- let pending = 0;
1229
- let ready = 0;
1230
- let running = 0;
1231
- let paused = 0;
1232
- let completed = 0;
1233
- let failed = 0;
1234
- let skipped = 0;
1235
- let totalTasks = 0;
1236
- let completedTasks = 0;
1237
- let failedTasks = 0;
1238
- let estimatedHours = 0;
1239
- let actualHours = 0;
1240
- for (const p of phases) {
1241
- switch (p.status) {
1242
- case "pending":
1243
- pending++;
1244
- break;
1245
- case "ready":
1246
- ready++;
1247
- break;
1248
- case "running":
1249
- running++;
1250
- break;
1251
- case "paused":
1252
- paused++;
1253
- break;
1254
- case "completed":
1255
- completed++;
1256
- break;
1257
- case "failed":
1258
- failed++;
1259
- break;
1260
- case "skipped":
1261
- skipped++;
1262
- break;
1263
- default:
1264
- break;
1265
- }
1266
- estimatedHours += p.estimateHours;
1267
- if (p.actualDurationMs) actualHours += p.actualDurationMs / 36e5;
1268
- const tracker = trackerFor(p);
1269
- const progress = tracker.getProgress();
1270
- totalTasks += progress.total;
1271
- completedTasks += progress.completed;
1272
- failedTasks += progress.failed;
1273
- }
1274
- const totalPhases = phases.length;
1275
- const done = completed + skipped;
1276
- return {
1277
- totalPhases,
1278
- pending,
1279
- ready,
1280
- running,
1281
- paused,
1282
- completed,
1283
- failed,
1284
- skipped,
1285
- percentComplete: totalPhases > 0 ? Math.min(100, Math.round(done / totalPhases * 100)) : 0,
1286
- totalTasks,
1287
- completedTasks,
1288
- failedTasks,
1289
- estimatedHours,
1290
- actualHours
1291
- };
1292
- }
1293
- function setIntegrationMetadata(graph, phase, status, details = {}) {
1294
- phase.metadata = {
1295
- ...phase.metadata,
1296
- integrationStatus: status,
1297
- integrationBranch: details.branch,
1298
- integrationWorktreeDir: details.worktreeDir,
1299
- integrationConflictFiles: details.conflictFiles,
1300
- integrationError: details.error,
1301
- integrationUpdatedAt: Date.now()
1302
- };
1303
- phase.updatedAt = Date.now();
1304
- graph.updatedAt = Date.now();
1305
- }
1306
- function truncate(text, max = 500) {
1307
- const t = text.trim();
1308
- return t.length <= max ? t : `${t.slice(0, max)}\u2026 (+${t.length - max} chars)`;
1309
- }
1310
-
1311
- // src/goal/phase-orchestrator-integration.ts
1312
- function worktreeEnv(int, phase) {
1313
- const handle = int.phaseWorktrees.get(phase.id);
1314
- return handle ? { cwd: handle.dir, branch: handle.branch } : void 0;
1315
- }
1316
- async function failPhaseAfterTasks(int, phase, error) {
1317
- int.updatePhaseStatus(phase, "failed");
1318
- phase.completedAt = Date.now();
1319
- phase.actualDurationMs = Date.now() - (phase.startedAt ?? Date.now());
1320
- int.runningPhases.delete(phase.id);
1321
- int.graph.activePhaseIds = int.graph.activePhaseIds.filter((id) => id !== phase.id);
1322
- if (!int.graph.failedPhaseIds.includes(phase.id)) int.graph.failedPhaseIds.push(phase.id);
1323
- int.emit("phase.failed", { phaseId: phase.id, name: phase.name, error });
1324
- int.ctx.onPhaseFail?.(phase, new Error(error));
1325
- await keepWorktreeForReview(int, phase);
1326
- }
1327
- function markPhaseMergeFailed(int, phase, error) {
1328
- if (phase.status !== "failed") {
1329
- int.graph.completedPhaseIds = int.graph.completedPhaseIds.filter((id) => id !== phase.id);
1330
- int.updatePhaseStatus(phase, "failed");
1331
- }
1332
- if (!int.graph.failedPhaseIds.includes(phase.id)) int.graph.failedPhaseIds.push(phase.id);
1333
- int.emit("phase.failed", { phaseId: phase.id, name: phase.name, error });
1334
- int.ctx.onPhaseFail?.(phase, new Error(error));
1335
- }
1336
- async function commitAndEnqueueMerge(int, phase) {
1337
- const handle = int.phaseWorktrees.get(phase.id);
1338
- if (!int.worktrees || !handle) return;
1339
- try {
1340
- await int.worktrees.commitAll(handle, `goal(${phase.name}): ${phase.id}`);
1341
- } catch {
1342
- }
1343
- const depPromises = phase.dependsOn.map((d) => int.phaseMergePromise.get(d)).filter((p) => Boolean(p));
1344
- const merged = (async () => {
1345
- await Promise.allSettled(depPromises);
1346
- int.setMergeQueue(
1347
- int.getMergeQueue().then(() => mergeOne(int, phase, handle)).catch((err) => {
1348
- const msg = toErrorMessage(err);
1349
- int.logger.error(msg, { event: "orchestrator.merge_failed", phaseId: phase.id });
1350
- markPhaseMergeFailed(int, phase, msg);
1351
- })
1352
- );
1353
- await int.getMergeQueue();
1354
- })();
1355
- int.phaseMergePromise.set(phase.id, merged);
1356
- }
1357
- async function mergeOne(int, phase, handle) {
1358
- if (!int.worktrees) return;
1359
- try {
1360
- const resolve4 = int.ctx.resolveConflict ? async (info) => {
1361
- const shouldResolve = await shouldAttemptConflictResolution(int, phase, info);
1362
- if (!shouldResolve) return false;
1363
- int.emit("phase.conflictResolving", {
1364
- phaseId: phase.id,
1365
- name: phase.name,
1366
- files: info.conflictFiles
1367
- });
1368
- const resolved = await int.ctx.resolveConflict?.(phase, info);
1369
- return resolved ?? false;
1370
- } : void 0;
1371
- const mergeOpts = {
1372
- squash: true
1373
- };
1374
- if (resolve4 !== void 0) mergeOpts.resolve = resolve4;
1375
- const result = await int.worktrees.merge(handle, mergeOpts);
1376
- if (result.resolved) {
1377
- int.emit("phase.conflictResolved", { phaseId: phase.id, name: phase.name });
1378
- }
1379
- setIntegrationMetadata(int.graph, phase, result.ok ? "merged" : "needs_review", {
1380
- branch: handle.branch,
1381
- worktreeDir: handle.dir,
1382
- conflictFiles: result.conflictFiles
1383
- });
1384
- await int.worktrees.release(handle, { keep: !result.ok });
1385
- } catch (err) {
1386
- setIntegrationMetadata(int.graph, phase, "merge_failed", {
1387
- branch: handle.branch,
1388
- worktreeDir: handle.dir,
1389
- error: toErrorMessage(err)
1390
- });
1391
- markPhaseMergeFailed(int, phase, `worktree merge failed: ${toErrorMessage(err)}`);
1392
- }
1393
- }
1394
- async function shouldAttemptConflictResolution(int, phase, info) {
1395
- if (!int.ctx.brain) return true;
1396
- const decision = await int.ctx.brain.decide({
1397
- id: `goal-conflict-${phase.id}`,
1398
- source: "goal",
1399
- question: `Should Goal try to resolve merge conflicts for phase "${phase.name}" automatically?`,
1400
- context: [
1401
- `Phase id: ${phase.id}`,
1402
- `Conflicted files: ${info.conflictFiles.join(", ") || "(unknown)"}`,
1403
- `Base working tree: ${info.cwd}`
1404
- ].join("\n"),
1405
- // 'critical', not 'high': choosing `resolve` lets a resolver agent EDIT
1406
- // FILES IN THE BASE WORKING TREE. That is the one Brain decision in the
1407
- // system whose wrong answer is not recoverable by simply continuing —
1408
- // unlike a budget grant (bounded, capped) or a goal-done verdict (the
1409
- // run can be restarted).
1410
- //
1411
- // Consequences of the level, both intended:
1412
- // - it is at or above the council floor, so a multi-LLM panel decides
1413
- // it whenever one can convene;
1414
- // - with no council (a single-model install resolves the autonomy
1415
- // ceiling to 'high'), it exceeds the ceiling and never reaches an
1416
- // unchecked single model. The policy tier's `ask_human` then routes
1417
- // to the terminal policy, which refuses a recommended option at
1418
- // critical risk and denies — i.e. the worktree is kept for human
1419
- // review, which is exactly the safe outcome.
1420
- risk: "critical",
1421
- fallback: "ask_human",
1422
- options: [
1423
- {
1424
- id: "resolve",
1425
- label: "Try the configured conflict resolver",
1426
- consequence: "A resolver agent may edit conflicted files in the base working tree.",
1427
- risk: "critical"
1428
- },
1429
- {
1430
- id: "review",
1431
- label: "Keep the worktree for human review",
1432
- consequence: "No automatic conflict resolution is attempted.",
1433
- risk: "low",
1434
- recommended: true
1435
- }
1436
- ]
1437
- });
1438
- phase.metadata = {
1439
- ...phase.metadata,
1440
- brainConflictDecision: decision.type,
1441
- brainConflictDecisionAt: Date.now()
1442
- };
1443
- if (decision.type !== "answer") return false;
1444
- return decision.optionId === "resolve";
1445
- }
1446
- async function keepWorktreeForReview(int, phase) {
1447
- const handle = int.phaseWorktrees.get(phase.id);
1448
- if (!int.worktrees || !handle) return;
1449
- try {
1450
- await int.worktrees.commitAll(handle, `goal(${phase.name}) [failed]: ${phase.id}`);
1451
- } catch {
1452
- }
1453
- setIntegrationMetadata(int.graph, phase, "not_merged_failed_phase", {
1454
- branch: handle.branch,
1455
- worktreeDir: handle.dir
1456
- });
1457
- await int.worktrees.release(handle, { keep: true }).catch(() => {
1458
- });
1459
- }
1460
-
1461
- // src/goal/phase-orchestrator-runtime.ts
1462
- function normalizePhaseGraphForResume(graph) {
1463
- graph.activePhaseIds = [];
1464
- for (const phase of graph.phases.values()) {
1465
- if (phase.status === "running") {
1466
- phase.status = "pending";
1467
- phase.updatedAt = Date.now();
1468
- }
1469
- if (phase.status === "pending") {
1470
- for (const task of phase.taskGraph.nodes.values()) {
1471
- if (task.status === "in_progress") task.status = "pending";
1472
- }
1473
- }
1474
- }
1475
- }
1476
- function createNoopEventBus() {
1477
- return {
1478
- emit: () => {
1479
- },
1480
- on: () => () => {
1481
- },
1482
- off: () => {
1483
- },
1484
- once: () => () => {
1485
- },
1486
- listeners: /* @__PURE__ */ new Map(),
1487
- wildcards: [],
1488
- setLogger: () => {
1489
- },
1490
- onAny: () => () => {
1491
- },
1492
- offAny: () => {
1493
- },
1494
- emitAsync: async () => [],
1495
- waitFor: async () => {
1496
- }
1497
- };
1498
- }
1499
-
1500
- // src/goal/phase-orchestrator.ts
1501
- var PhaseOrchestrator = class {
1502
- graph;
1503
- ctx;
1504
- opts;
1505
- events;
1506
- stopped = false;
1507
- paused = false;
1508
- /**
1509
- * Run-wide abort source. stop() aborts it; every in-flight
1510
- * ctx.executeTask call observes the abort through its per-task signal
1511
- * (composed from this controller and the task's own timeout controller).
1512
- * Recreated by start() so a stopped orchestrator can be reused.
1513
- */
1514
- stopController = new AbortController();
1515
- runningPhases = /* @__PURE__ */ new Set();
1516
- tickInterval = null;
1517
- trackerCache = /* @__PURE__ */ new Map();
1518
- taskRetryCounts = /* @__PURE__ */ new Map();
1519
- // ── Git-worktree isolation (optional) ──────────────────────────────────────
1520
- worktrees;
1521
- logger;
1522
- /** Per-phase worktree handles, keyed by phase id. */
1523
- phaseWorktrees = /* @__PURE__ */ new Map();
1524
- /** Serializes all merges back to the base branch (one at a time). */
1525
- mergeQueue = Promise.resolve();
1526
- /** Per-phase merge promise, so a phase merges only after its deps do. */
1527
- phaseMergePromise = /* @__PURE__ */ new Map();
1528
- constructor(opts) {
1529
- this.graph = opts.graph;
1530
- this.ctx = opts.ctx;
1531
- this.events = opts.events ?? createNoopEventBus();
1532
- this.worktrees = opts.worktrees;
1533
- this.logger = opts.logger ?? noOpLogger;
1534
- this.opts = {
1535
- maxConcurrentPhases: opts.maxConcurrentPhases ?? 1,
1536
- maxConcurrentTasks: opts.maxConcurrentTasks ?? 2,
1537
- maxRetries: opts.maxRetries ?? 2,
1538
- maxVerifyAttempts: opts.maxVerifyAttempts ?? 2,
1539
- autonomous: opts.autonomous ?? true,
1540
- phaseDelayMs: opts.phaseDelayMs ?? 0,
1541
- // Autonomous goals must fail closed. Continuing after a terminal task or
1542
- // gate failure can make dependent phases look complete without their
1543
- // required evidence.
1544
- stopOnFailure: opts.stopOnFailure ?? true,
1545
- taskTimeoutMs: opts.taskTimeoutMs ?? 6e5,
1546
- events: this.events
1547
- };
1548
- }
1549
- // ─── Lifecycle ────────────────────────────────────────────────────────────
1550
- /**
1551
- * Start the full phase flow.
1552
- * In autonomous mode, starts root phases and automatically starts the next phase when they finish.
1553
- */
1554
- async start() {
1555
- this.stopped = false;
1556
- this.paused = false;
1557
- this.stopController = new AbortController();
1558
- this.normalizeForResume();
1559
- this.graph.startedAt = Date.now();
1560
- this.graph.updatedAt = Date.now();
1561
- let readyPhases = this.getReadyPhases();
1562
- while (readyPhases.length > 0 && !this.stopped) {
1563
- await this.waitWhilePaused();
1564
- if (this.stopped) break;
1565
- const batch = readyPhases.slice(0, this.opts.maxConcurrentPhases);
1566
- await Promise.all(batch.map((p) => this.startPhase(p)));
1567
- if (this.opts.phaseDelayMs > 0) {
1568
- await this.delay(this.opts.phaseDelayMs);
1569
- }
1570
- await this.waitWhilePaused();
1571
- if (this.stopped) break;
1572
- readyPhases = this.getReadyPhases().filter(
1573
- (p) => !this.runningPhases.has(p.id) && p.status !== "completed" && p.status !== "failed"
1574
- );
1575
- }
1576
- await this.drainMerges();
1577
- if (this.opts.autonomous) {
1578
- this.tickInterval = setInterval(() => this.tick(), 1e3);
1579
- }
1580
- }
1581
- /**
1582
- * Make a (possibly resumed) graph runnable. A graph loaded from disk after an
1583
- * interrupted run can carry transient state from the dead process: phases left
1584
- * `running` and tasks left `in_progress`. The scheduler only starts `pending`
1585
- * phases (getReadyPhases) and only runs `pending` tasks (getExecutableTasks),
1586
- * so without this a resumed phase/task would stall forever. Reset that
1587
- * transient state to `pending`; terminal phases (completed/failed/skipped) and
1588
- * already-completed tasks are untouched, so completed work is never re-run.
1589
- * For a freshly built graph this is a no-op.
1590
- */
1591
- normalizeForResume() {
1592
- normalizePhaseGraphForResume(this.graph);
1593
- }
1594
- /** Wait for all pending phase merges, dependency-ordered and globally serialized. */
1595
- async drainMerges() {
1596
- await Promise.allSettled([...this.phaseMergePromise.values()]);
1597
- await this.mergeQueue.catch((err) => {
1598
- const msg = toErrorMessage(err);
1599
- this.logger.warn(msg, { event: "orchestrator.merge_queue_failed" });
1600
- });
1601
- }
1602
- /** Pause: active phases continue, but no new phase starts. */
1603
- pause() {
1604
- this.paused = true;
1605
- }
1606
- /** Resume: new phases may start again. */
1607
- resume() {
1608
- this.paused = false;
1609
- this.tick().catch((err) => {
1610
- const msg = toErrorMessage(err);
1611
- this.logger.error(msg, { event: "orchestrator.tick_failed" });
1612
- });
1613
- }
1614
- /** Stop completely, including active phases. */
1615
- stop() {
1616
- this.stopped = true;
1617
- this.stopController.abort();
1618
- if (this.tickInterval) {
1619
- clearInterval(this.tickInterval);
1620
- this.tickInterval = null;
1621
- }
1622
- for (const phaseId of this.runningPhases) {
1623
- const phase = this.graph.phases.get(phaseId);
1624
- if (phase) {
1625
- this.updatePhaseStatus(phase, "paused");
1626
- }
1627
- }
1628
- this.trackerCache.clear();
1629
- this.taskRetryCounts.clear();
1630
- if (this.worktrees) {
1631
- for (const handle of this.worktrees.list()) {
1632
- void this.worktrees.release(handle, { keep: true }).catch(() => {
1633
- });
1634
- }
1635
- }
1636
- }
1637
- // ─── Tick Loop (Autonomous) ───────────────────────────────────────────────
1638
- async tick() {
1639
- if (this.stopped || this.paused) return;
1640
- const active = this.getActivePhases();
1641
- const queued = this.getReadyPhases();
1642
- this.emit("autonomous.tick", {
1643
- activePhases: active.map((p) => p.id),
1644
- queuedPhases: queued.map((p) => p.id)
1645
- });
1646
- this.ctx.onTick?.({ activePhases: active, readyPhases: queued });
1647
- const availableSlots = this.opts.maxConcurrentPhases - active.length;
1648
- if (availableSlots > 0 && queued.length > 0) {
1649
- for (const phase of queued.slice(0, availableSlots)) {
1650
- if (phase.status === "pending") {
1651
- await this.startPhase(phase);
1652
- }
1653
- }
1654
- }
1655
- if (this.isComplete()) {
1656
- this.onGraphComplete();
1657
- return;
1658
- }
1659
- if (this.opts.stopOnFailure && this.graph.failedPhaseIds.length > 0) {
1660
- const failedPhase = this.graph.phases.get(this.graph.failedPhaseIds[0] ?? "");
1661
- if (failedPhase) {
1662
- this.onGraphFailed(failedPhase);
1663
- }
1664
- return;
1665
- }
1666
- }
1667
- // ─── Phase Execution ──────────────────────────────────────────────────────
1668
- async startPhase(phase) {
1669
- if (phase.status !== "pending" && phase.status !== "ready") return;
1670
- this.updatePhaseStatus(phase, "running");
1671
- phase.startedAt = Date.now();
1672
- this.runningPhases.add(phase.id);
1673
- this.graph.activePhaseIds.push(phase.id);
1674
- if (this.worktrees && !this.phaseWorktrees.has(phase.id)) {
1675
- try {
1676
- const handle = await this.worktrees.allocate(phase.id, {
1677
- slugHint: phase.name,
1678
- ownerLabel: phase.name
1679
- });
1680
- if (handle.status === "active") this.phaseWorktrees.set(phase.id, handle);
1681
- } catch {
1682
- }
1683
- }
1684
- this.emit("phase.started", { phaseId: phase.id, name: phase.name });
1685
- try {
1686
- if (phase.taskGraph.nodes.size === 0) {
1687
- await this.failPhaseAfterTasks(
1688
- phase,
1689
- "Phase has no executable tasks. Add task evidence before starting this phase."
1690
- );
1691
- return;
1692
- }
1693
- await this.executePhaseTasks(phase);
1694
- if (this.stopped) return;
1695
- const failedTasks = this.getFailedTaskCount(phase);
1696
- const completedTasks = this.getCompletedTaskCount(phase);
1697
- this.emit("phase.allTasksDone", {
1698
- phaseId: phase.id,
1699
- completed: completedTasks,
1700
- failed: failedTasks
1701
- });
1702
- if (failedTasks > 0 && this.opts.stopOnFailure) {
1703
- await this.failPhaseAfterTasks(phase, `${failedTasks} task(s) failed`);
1704
- return;
1705
- }
1706
- const verdict = await this.runVerifyGate(phase);
1707
- if (!verdict.ok) {
1708
- await this.failPhaseAfterTasks(
1709
- phase,
1710
- `verification failed${verdict.output ? `: ${this.truncate(verdict.output)}` : ""}`
1711
- );
1712
- return;
1713
- }
1714
- this.updatePhaseStatus(phase, "completed");
1715
- phase.completedAt = Date.now();
1716
- phase.actualDurationMs = Date.now() - (phase.startedAt ?? Date.now());
1717
- this.runningPhases.delete(phase.id);
1718
- this.graph.activePhaseIds = this.graph.activePhaseIds.filter((id) => id !== phase.id);
1719
- this.graph.completedPhaseIds.push(phase.id);
1720
- this.emit("phase.completed", {
1721
- phaseId: phase.id,
1722
- name: phase.name,
1723
- durationMs: phase.actualDurationMs
1724
- });
1725
- this.ctx.onPhaseComplete?.(phase);
1726
- await this.commitAndEnqueueMerge(phase);
1727
- } catch (error) {
1728
- this.updatePhaseStatus(phase, "failed");
1729
- phase.completedAt = Date.now();
1730
- phase.actualDurationMs = Date.now() - (phase.startedAt ?? Date.now());
1731
- this.runningPhases.delete(phase.id);
1732
- this.graph.activePhaseIds = this.graph.activePhaseIds.filter((id) => id !== phase.id);
1733
- this.graph.failedPhaseIds.push(phase.id);
1734
- this.emit("phase.failed", {
1735
- phaseId: phase.id,
1736
- name: phase.name,
1737
- error: error instanceof Error ? error.message : String(error)
1738
- });
1739
- this.ctx.onPhaseFail?.(phase, error instanceof Error ? error : new Error(String(error)));
1740
- await this.keepWorktreeForReview(phase);
1741
- }
1742
- }
1743
- // ─── Verification gate ──────────────────────────────────────────────────────
1744
- /**
1745
- * Run the verification gate for a phase whose tasks all succeeded. Verifies in
1746
- * the phase's worktree; on failure, runs the repair pass and re-verifies, up to
1747
- * `maxVerifyAttempts` repairs. Returns the final verdict. When no `verifyPhase`
1748
- * callback is wired the gate is a no-op and always passes.
1749
- */
1750
- async runVerifyGate(phase) {
1751
- if (!this.ctx.verifyPhase) return { ok: true };
1752
- const env = this.worktreeEnv(phase);
1753
- for (let attempt = 0; attempt <= this.opts.maxVerifyAttempts; attempt++) {
1754
- if (this.stopped) return { ok: false, output: "stopped before verification completed" };
1755
- this.emit("phase.verifying", { phaseId: phase.id, name: phase.name, attempt });
1756
- let verdict;
1757
- try {
1758
- verdict = await this.ctx.verifyPhase(phase, env);
1759
- } catch (err) {
1760
- verdict = { ok: false, output: toErrorMessage(err) };
1761
- }
1762
- if (verdict.ok) return { ok: true };
1763
- this.emit("phase.verifyFailed", {
1764
- phaseId: phase.id,
1765
- name: phase.name,
1766
- attempt,
1767
- error: verdict.output
1768
- });
1769
- if (attempt >= this.opts.maxVerifyAttempts || !this.ctx.repairPhase || this.stopped) {
1770
- return { ok: false, output: verdict.output };
1771
- }
1772
- this.emit("phase.repairing", { phaseId: phase.id, name: phase.name, attempt: attempt + 1 });
1773
- try {
1774
- await this.ctx.repairPhase(
1775
- phase,
1776
- verdict.output ?? "verification failed",
1777
- attempt + 1,
1778
- env
1779
- );
1780
- } catch {
1781
- }
1782
- }
1783
- return { ok: false };
1784
- }
1785
- /** Worktree env (cwd/branch) for a phase, or undefined if it runs on the shared tree. */
1786
- /** Bundle of orchestrator state the worktree-integration steps drive. */
1787
- integrationCtx() {
1788
- return {
1789
- graph: this.graph,
1790
- ctx: this.ctx,
1791
- logger: this.logger,
1792
- worktrees: this.worktrees,
1793
- phaseWorktrees: this.phaseWorktrees,
1794
- phaseMergePromise: this.phaseMergePromise,
1795
- runningPhases: this.runningPhases,
1796
- getMergeQueue: () => this.mergeQueue,
1797
- setMergeQueue: (queue) => {
1798
- this.mergeQueue = queue;
1799
- },
1800
- emit: (event, payload) => this.emit(event, payload),
1801
- updatePhaseStatus: (phase, status) => this.updatePhaseStatus(phase, status)
1802
- };
1803
- }
1804
- worktreeEnv(phase) {
1805
- return worktreeEnv(this.integrationCtx(), phase);
1806
- }
1807
- async failPhaseAfterTasks(phase, error) {
1808
- await failPhaseAfterTasks(this.integrationCtx(), phase, error);
1809
- }
1810
- /** Trim long verifier output so it fits cleanly in an event/error message. */
1811
- truncate(text, max = 500) {
1812
- return truncate(text, max);
1813
- }
1814
- // ─── Worktree integration ───────────────────────────────────────────────────
1815
- async commitAndEnqueueMerge(phase) {
1816
- await commitAndEnqueueMerge(this.integrationCtx(), phase);
1817
- }
1818
- /** A failed phase keeps its worktree on disk for inspection (no merge). */
1819
- async keepWorktreeForReview(phase) {
1820
- await keepWorktreeForReview(this.integrationCtx(), phase);
1821
- }
1822
- async executePhaseTasks(phase) {
1823
- const pendingTasks = this.getExecutableTasks(phase);
1824
- while (pendingTasks.length > 0 && !this.stopped) {
1825
- const batch = pendingTasks.splice(0, this.opts.maxConcurrentTasks);
1826
- const results = await Promise.allSettled(
1827
- batch.map((task) => this.executeSingleTask(task, phase))
1828
- );
1829
- for (let i = 0; i < results.length; i++) {
1830
- const result = results[i];
1831
- const task = batch[i];
1832
- if (!result || !task) continue;
1833
- if (result.status === "fulfilled") {
1834
- this.markTaskCompleted(phase, task);
1835
- } else {
1836
- this.markTaskFailed(phase, task, result.reason);
1837
- }
1838
- }
1839
- const newReady = this.getExecutableTasks(phase);
1840
- pendingTasks.length = 0;
1841
- pendingTasks.push(...newReady);
1842
- }
1843
- }
1844
- async executeSingleTask(task, phase) {
1845
- const tracker = this.getTrackerForPhase(phase);
1846
- tracker.updateNodeStatus(task.id, "in_progress");
1847
- this.emit("phase.taskStarted", {
1848
- phaseId: phase.id,
1849
- taskId: task.id,
1850
- taskTitle: task.title,
1851
- agentName: task.assignee
1852
- });
1853
- const handle = this.phaseWorktrees.get(phase.id);
1854
- const timeoutController = this.opts.taskTimeoutMs > 0 ? new AbortController() : void 0;
1855
- const signal = timeoutController ? AbortSignal.any([this.stopController.signal, timeoutController.signal]) : this.stopController.signal;
1856
- const taskPromise = this.ctx.executeTask(
1857
- task,
1858
- phase.id,
1859
- { cwd: handle?.dir, branch: handle?.branch },
1860
- signal
1861
- );
1862
- if (!timeoutController) return taskPromise;
1863
- const timeoutMs = this.opts.taskTimeoutMs;
1864
- const timedOut = /* @__PURE__ */ Symbol("timed_out");
1865
- const result = await Promise.race([
1866
- taskPromise,
1867
- new Promise((resolve4) => {
1868
- const timer = setTimeout(() => {
1869
- timeoutController.abort();
1870
- resolve4(timedOut);
1871
- }, timeoutMs);
1872
- taskPromise.then(() => clearTimeout(timer)).catch(() => clearTimeout(timer));
1873
- })
1874
- ]);
1875
- if (result !== timedOut) return result;
1876
- this.emit("phase.taskTimedOut", {
1877
- phaseId: phase.id,
1878
- taskId: task.id,
1879
- taskTitle: task.title,
1880
- timeoutMs
1881
- });
1882
- const settled = taskPromise.then(
1883
- () => void 0,
1884
- () => void 0
1885
- );
1886
- const grace = new Promise((resolve4) => {
1887
- const timer = setTimeout(resolve4, 5e3);
1888
- timer.unref?.();
1889
- void settled.then(() => clearTimeout(timer));
1890
- });
1891
- await Promise.race([settled, grace]);
1892
- throw new Error(
1893
- `Task "${task.title}" (${task.id}) exceeded timeout of ${timeoutMs} ms`
1894
- );
1895
- }
1896
- markTaskCompleted(phase, task) {
1897
- const tracker = this.getTrackerForPhase(phase);
1898
- tracker.updateNodeStatus(task.id, "completed");
1899
- this.emit("phase.taskCompleted", {
1900
- phaseId: phase.id,
1901
- taskId: task.id,
1902
- taskTitle: task.title
1903
- });
1904
- }
1905
- markTaskFailed(phase, task, error) {
1906
- const tracker = this.getTrackerForPhase(phase);
1907
- const taskKey = `${phase.id}:${task.id}`;
1908
- const currentRetries = this.taskRetryCounts.get(taskKey) ?? 0;
1909
- if (this.stopped) {
1910
- tracker.updateNodeStatus(task.id, "pending", "Stopped before completion");
1911
- return;
1912
- }
1913
- if (currentRetries < this.opts.maxRetries) {
1914
- this.taskRetryCounts.set(taskKey, currentRetries + 1);
1915
- tracker.updateNodeStatus(
1916
- task.id,
1917
- "pending",
1918
- `Retry ${currentRetries + 1}/${this.opts.maxRetries}`
1919
- );
1920
- this.emit("phase.taskRetrying", {
1921
- phaseId: phase.id,
1922
- taskId: task.id,
1923
- taskTitle: task.title,
1924
- attempt: currentRetries + 1,
1925
- maxRetries: this.opts.maxRetries
1926
- });
1927
- } else {
1928
- tracker.updateNodeStatus(
1929
- task.id,
1930
- "failed",
1931
- error instanceof Error ? error.message : String(error)
1932
- );
1933
- this.emit("phase.taskFailed", {
1934
- phaseId: phase.id,
1935
- taskId: task.id,
1936
- taskTitle: task.title,
1937
- error: error instanceof Error ? error.message : String(error)
1938
- });
1939
- }
1940
- }
1941
- // ─── Helpers ──────────────────────────────────────────────────────────────
1942
- getReadyPhases() {
1943
- return getReadyPhases(this.graph);
1944
- }
1945
- getActivePhases() {
1946
- return getActivePhases(this.graph);
1947
- }
1948
- getExecutableTasks(phase) {
1949
- return getExecutableTasks((p) => this.getTrackerForPhase(p), phase);
1950
- }
1951
- getTrackerForPhase(phase) {
1952
- const cached = this.trackerCache.get(phase.id);
1953
- if (cached) return cached;
1954
- const tracker = new TaskTracker({ store: new DefaultTaskStore() });
1955
- tracker.setGraph(phase.taskGraph);
1956
- this.trackerCache.set(phase.id, tracker);
1957
- return tracker;
1958
- }
1959
- getFailedTaskCount(phase) {
1960
- return getFailedTaskCount((p) => this.getTrackerForPhase(p), phase);
1961
- }
1962
- getCompletedTaskCount(phase) {
1963
- return getCompletedTaskCount((p) => this.getTrackerForPhase(p), phase);
1964
- }
1965
- updatePhaseStatus(phase, status) {
1966
- const from = phase.status;
1967
- phase.status = status;
1968
- phase.updatedAt = Date.now();
1969
- this.graph.updatedAt = Date.now();
1970
- this.emit("phase.statusChange", { phaseId: phase.id, from, to: status });
1971
- }
1972
- isComplete() {
1973
- return isGraphComplete(this.graph);
1974
- }
1975
- onGraphComplete() {
1976
- this.graph.completedAt = Date.now();
1977
- const durationMs = this.graph.completedAt - (this.graph.startedAt ?? this.graph.completedAt);
1978
- this.emit("graph.completed", { graphId: this.graph.id, durationMs });
1979
- this.stop();
1980
- }
1981
- onGraphFailed(failedPhase) {
1982
- this.emit("graph.failed", {
1983
- graphId: this.graph.id,
1984
- failedPhaseId: failedPhase.id,
1985
- error: `Phase "${failedPhase.name}" failed`
1986
- });
1987
- this.stop();
1988
- }
1989
- // ─── Progress ─────────────────────────────────────────────────────────────
1990
- getProgress() {
1991
- return getProgress(this.graph, (p) => this.getTrackerForPhase(p));
1992
- }
1993
- getGraph() {
1994
- return this.graph;
1995
- }
1996
- isRunning() {
1997
- return !this.stopped && this.runningPhases.size > 0;
1998
- }
1999
- isPaused() {
2000
- return this.paused;
2001
- }
2002
- // ─── Agent Assignment ─────────────────────────────────────────────────────
2003
- assignAgent(phaseId, agentId) {
2004
- const phase = this.graph.phases.get(phaseId);
2005
- if (!phase) return;
2006
- if (!phase.assignedAgents.includes(agentId)) {
2007
- phase.assignedAgents.push(agentId);
2008
- this.emit("agent.assigned", { phaseId, agentId });
2009
- }
2010
- }
2011
- releaseAgent(phaseId, agentId) {
2012
- const phase = this.graph.phases.get(phaseId);
2013
- if (!phase) return;
2014
- phase.assignedAgents = phase.assignedAgents.filter((id) => id !== agentId);
2015
- this.emit("agent.released", { phaseId, agentId });
2016
- }
2017
- // ─── Interactive board mutations ──────────────────────────────────────────
2018
- //
2019
- // These are driven by an interactive board (WebUI/TUI), not the autonomous
2020
- // loop. Each mutates the live graph, emits a typed event so every surface
2021
- // stays in sync, and bumps updatedAt so the host re-persists.
2022
- /** Find the phase whose task graph currently holds `taskId`. */
2023
- findPhaseOfTask(taskId) {
2024
- return findPhaseOfTaskInGraph(this.graph, taskId);
2025
- }
2026
- /**
2027
- * Move a task to another phase's task graph. Edges that referenced the task
2028
- * are dropped (cross-phase dependencies are not modeled). No-op when the task
2029
- * or target phase is missing, or it is already in the target phase.
2030
- */
2031
- moveTask(taskId, toPhaseId) {
2032
- return movePhaseTask(this.boardMutationContext(), taskId, toPhaseId);
2033
- }
2034
- /** (Re)assign a task to a specific agent (or clear with agentName/agentId omitted). */
2035
- setTaskAssignee(taskId, agentId, agentName) {
2036
- return setPhaseTaskAssignee(this.boardMutationContext(), taskId, agentId, agentName);
2037
- }
2038
- /** Add a new task to a phase. Returns the created task id, or null if the phase is missing. */
2039
- addTask(phaseId, spec) {
2040
- return addPhaseTask(this.boardMutationContext(), phaseId, spec);
2041
- }
2042
- /**
2043
- * Requeue a task to `pending` (clearing its retry counter) and nudge a
2044
- * terminal/paused phase back to `ready` so the loop re-runs it. Backs both the
2045
- * board's "retry" and "start" affordances.
2046
- */
2047
- requeueTask(taskId) {
2048
- return requeuePhaseTask(this.boardMutationContext(), taskId);
2049
- }
2050
- boardMutationContext() {
2051
- return {
2052
- graph: this.graph,
2053
- trackerCache: this.trackerCache,
2054
- taskRetryCounts: this.taskRetryCounts,
2055
- phaseWorktrees: this.phaseWorktrees,
2056
- getTrackerForPhase: (phase) => this.getTrackerForPhase(phase),
2057
- updatePhaseStatus: (phase, status) => this.updatePhaseStatus(phase, status),
2058
- emit: (event, payload) => this.emit(event, payload)
2059
- };
2060
- }
2061
- // ─── Events ───────────────────────────────────────────────────────────────
2062
- emit(event, payload) {
2063
- this.events.emit(event, payload);
2064
- }
2065
- async waitWhilePaused() {
2066
- while (this.paused && !this.stopped) {
2067
- await this.delay(100);
2068
- }
2069
- }
2070
- delay(ms) {
2071
- return new Promise((resolve4) => setTimeout(resolve4, ms));
2072
- }
2073
- };
2074
-
2075
- // src/goal/goal-runner.ts
2076
- var GoalRunner = class {
2077
- graph = null;
2078
- orchestrator = null;
2079
- opts;
2080
- progressInterval = null;
2081
- maxRunTimer = null;
2082
- graphCompletedHandler = (payload) => {
2083
- const p = payload;
2084
- if (this.graph && p.graphId === this.graph.id) {
2085
- this.opts.onComplete?.(this.graph);
2086
- this.cleanup();
2087
- }
2088
- };
2089
- graphFailedHandler = (payload) => {
2090
- const p = payload;
2091
- if (this.graph && p.graphId === this.graph.id) {
2092
- const failedPhase = this.graph.phases.get(p.failedPhaseId);
2093
- if (failedPhase) {
2094
- this.opts.onFail?.(this.graph, failedPhase, new Error(p.error));
2095
- }
2096
- if (this.opts.stopOnFailure) {
2097
- this.cleanup();
2098
- }
2099
- }
2100
- };
2101
- /** Stores the unsubscribe function returned by EventBus.on() */
2102
- unsubscribeCompleted = null;
2103
- unsubscribeFailed = null;
2104
- constructor(opts) {
2105
- this.opts = opts;
2106
- }
2107
- async start() {
2108
- const builder = new PhaseGraphBuilder({
2109
- title: this.opts.title,
2110
- description: this.opts.description,
2111
- phases: this.opts.phases,
2112
- autonomous: this.opts.autonomous,
2113
- stopOnFailure: this.opts.stopOnFailure
2114
- });
2115
- this.graph = await builder.build();
2116
- const ctx = {
2117
- executeTask: this.opts.executeTask,
2118
- brain: this.opts.brain,
2119
- onPhaseComplete: (phase) => {
2120
- this.opts.onPhaseComplete?.(phase);
2121
- },
2122
- onPhaseFail: (phase, error) => {
2123
- this.opts.onPhaseFail?.(phase, error);
2124
- },
2125
- onTick: (tickCtx) => {
2126
- this.opts.onTick?.(tickCtx);
2127
- }
2128
- };
2129
- if (this.opts.verifyPhase !== void 0) ctx.verifyPhase = this.opts.verifyPhase;
2130
- if (this.opts.repairPhase !== void 0) ctx.repairPhase = this.opts.repairPhase;
2131
- if (this.opts.resolveConflict !== void 0) ctx.resolveConflict = this.opts.resolveConflict;
2132
- this.orchestrator = new PhaseOrchestrator({
2133
- graph: this.graph,
2134
- ctx,
2135
- maxConcurrentPhases: this.opts.maxConcurrentPhases,
2136
- maxConcurrentTasks: this.opts.maxConcurrentTasks,
2137
- maxRetries: this.opts.maxRetries,
2138
- maxVerifyAttempts: this.opts.maxVerifyAttempts,
2139
- autonomous: this.opts.autonomous,
2140
- phaseDelayMs: this.opts.phaseDelayMs,
2141
- stopOnFailure: this.opts.stopOnFailure,
2142
- events: this.opts.events,
2143
- worktrees: this.opts.worktrees
2144
- });
2145
- if (this.opts.events) {
2146
- const bus = this.opts.events;
2147
- const onUntyped = (event, handler) => bus.on(event, handler);
2148
- this.unsubscribeCompleted = onUntyped("graph.completed", this.graphCompletedHandler);
2149
- this.unsubscribeFailed = onUntyped("graph.failed", this.graphFailedHandler);
2150
- }
2151
- await this.orchestrator.start();
2152
- this.maxRunTimer = setTimeout(
2153
- () => {
2154
- this.opts.onProgress?.({
2155
- totalPhases: 0,
2156
- pending: 0,
2157
- ready: 0,
2158
- running: 0,
2159
- paused: 0,
2160
- completed: 0,
2161
- failed: 0,
2162
- skipped: 0,
2163
- percentComplete: 0,
2164
- totalTasks: 0,
2165
- completedTasks: 0,
2166
- failedTasks: 0,
2167
- estimatedHours: 0,
2168
- actualHours: 0
2169
- });
2170
- this.stop();
2171
- },
2172
- this.opts.maxRunDurationMs ?? 7 * 24 * 60 * 6e4
2173
- );
2174
- if (this.opts.maxRunDurationMs !== void 0 && this.opts.maxRunDurationMs <= 0) {
2175
- clearTimeout(this.maxRunTimer);
2176
- this.maxRunTimer = null;
2177
- }
2178
- this.maxRunTimer?.unref?.();
2179
- if (this.opts.onProgress) {
2180
- this.progressInterval = setInterval(() => {
2181
- const progress = this.orchestrator?.getProgress();
2182
- if (progress) this.opts.onProgress?.(progress);
2183
- }, 2e3);
2184
- }
2185
- return this.graph;
2186
- }
2187
- pause() {
2188
- this.orchestrator?.pause();
2189
- }
2190
- resume() {
2191
- this.orchestrator?.resume();
2192
- }
2193
- stop() {
2194
- this.orchestrator?.stop();
2195
- this.cleanup();
2196
- }
2197
- getProgress() {
2198
- return this.orchestrator?.getProgress() ?? null;
2199
- }
2200
- getGraph() {
2201
- return this.graph;
2202
- }
2203
- isRunning() {
2204
- return this.orchestrator?.isRunning() ?? false;
2205
- }
2206
- isPaused() {
2207
- return this.orchestrator?.isPaused() ?? false;
2208
- }
2209
- assignAgent(phaseId, agentId) {
2210
- this.orchestrator?.assignAgent(phaseId, agentId);
2211
- }
2212
- releaseAgent(phaseId, agentId) {
2213
- this.orchestrator?.releaseAgent(phaseId, agentId);
2214
- }
2215
- cleanup() {
2216
- if (this.progressInterval) {
2217
- clearInterval(this.progressInterval);
2218
- this.progressInterval = null;
2219
- }
2220
- if (this.maxRunTimer) {
2221
- clearTimeout(this.maxRunTimer);
2222
- this.maxRunTimer = null;
2223
- }
2224
- this.unsubscribeCompleted?.();
2225
- this.unsubscribeCompleted = null;
2226
- this.unsubscribeFailed?.();
2227
- this.unsubscribeFailed = null;
2228
- }
2229
- };
2230
- async function createGoalRunnerFromTaskGraph(taskGraph, options) {
2231
- const graph = await PhaseGraphBuilder.fromTaskGraph(taskGraph, {
2232
- title: options.title ?? taskGraph.title,
2233
- tasksPerPhase: options.tasksPerPhase
2234
- });
2235
- const phases = Array.from(graph.phases.values()).map((p) => ({
2236
- name: p.name,
2237
- description: p.description,
2238
- priority: p.priority,
2239
- estimateHours: p.estimateHours,
2240
- parallelizable: p.parallelizable
2241
- }));
2242
- return new GoalRunner({
2243
- ...options,
2244
- title: options.title ?? taskGraph.title,
2245
- phases
2246
- });
2247
- }
2248
-
2249
- // src/utils/instruction-file.ts
2250
- import { readFileSync, statSync } from "node:fs";
2251
- import * as path2 from "node:path";
2252
- import { fileURLToPath } from "node:url";
2253
- var textCache = /* @__PURE__ */ new Map();
2254
- var rootCandidates;
2255
- function readBundledInstructionText(relativePath) {
2256
- const cached = textCache.get(relativePath);
2257
- if (cached !== void 0) return cached;
2258
- let resolved = "";
2259
- for (const root of instructionRootCandidates()) {
2260
- try {
2261
- resolved = readFileSync(path2.join(root, relativePath), "utf8").trimEnd();
2262
- break;
2263
- } catch {
2264
- }
2265
- }
2266
- textCache.set(relativePath, resolved);
2267
- return resolved;
2268
- }
2269
- function renderInstructionTemplate(template, values) {
2270
- return template.replace(
2271
- /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g,
2272
- (match, key) => Object.hasOwn(values, key) ? values[key] ?? "" : match
2273
- );
2274
- }
2275
- function instructionRootCandidates() {
2276
- if (rootCandidates !== void 0) return rootCandidates;
2277
- const here = path2.dirname(fileURLToPath(import.meta.url));
2278
- const candidates = [
2279
- path2.resolve(here, "../../instructions"),
2280
- path2.resolve(here, "../instructions"),
2281
- path2.resolve(here, "instructions")
2282
- ];
2283
- rootCandidates = candidates.sort((a, b) => Number(!isDirectory(a)) - Number(!isDirectory(b)));
2284
- return rootCandidates;
2285
- }
2286
- function isDirectory(candidate) {
2287
- try {
2288
- return statSync(candidate).isDirectory();
2289
- } catch {
2290
- return false;
2291
- }
2292
- }
2293
-
2294
- // src/goal/goal-planner.ts
2295
- var VALID_TASK_TYPES = /* @__PURE__ */ new Set([
2296
- "feature",
2297
- "bugfix",
2298
- "refactor",
2299
- "docs",
2300
- "test",
2301
- "chore"
2302
- ]);
2303
- var VALID_PRIORITIES = /* @__PURE__ */ new Set([
2304
- "critical",
2305
- "high",
2306
- "medium",
2307
- "low"
2308
- ]);
2309
- var GoalPlanner = class {
2310
- constructor(opts) {
2311
- this.opts = opts;
2312
- }
2313
- opts;
2314
- /** Convert the goal into a phase-and-todo plan. */
2315
- async plan() {
2316
- const prompt = this.buildPrompt();
2317
- const raw = await this.opts.runOnce(prompt);
2318
- const { phases, warnings } = this.parse(raw);
2319
- const totalTasks = phases.reduce((s, p) => s + (p.taskTemplates?.length ?? 0), 0);
2320
- const estimatedCostTokens = totalTasks > 0 ? totalTasks * 5 * 2e3 : void 0;
2321
- const maxTotal = this.opts.maxTotalTasks ?? 0;
2322
- let budgetExceeded = false;
2323
- if (maxTotal > 0 && totalTasks > maxTotal) {
2324
- warnings.push(
2325
- `Plan has ${totalTasks} total tasks, exceeding the limit of ${maxTotal}. Consider narrowing the scope or increasing the limit.`
2326
- );
2327
- budgetExceeded = true;
2328
- }
2329
- return { phases, raw, parseFailed: phases.length === 0, warnings, estimatedCostTokens, budgetExceeded };
2330
- }
2331
- /** Instruction prompt for the plan the model should produce. */
2332
- buildPrompt() {
2333
- const minP = this.opts.minPhases ?? 3;
2334
- const maxP = this.opts.maxPhases ?? 8;
2335
- const todos = this.opts.todosPerPhase ?? 6;
2336
- const ctx = this.opts.projectContext?.trim();
2337
- return renderInstructionTemplate(readBundledInstructionText("goal/phase-planner.md"), {
2338
- minPhases: String(minP),
2339
- maxPhases: String(maxP),
2340
- todosPerPhase: String(todos),
2341
- goal: this.opts.goal,
2342
- projectContext: ctx ? `
2343
- PROJECT CONTEXT:
2344
- ${ctx}
2345
- ` : ""
2346
- });
2347
- }
2348
- /** Extract JSON from raw output, validate it, and convert it to PhaseTemplate[].
2349
- * Returns `{ phases, warnings }` where warnings describe coerced defaults. */
2350
- parse(raw) {
2351
- const json = extractJSONArray(raw);
2352
- if (!json) return { phases: [], warnings: ["No JSON array found in model output"] };
2353
- let data;
2354
- try {
2355
- data = JSON.parse(json);
2356
- } catch {
2357
- return { phases: [], warnings: ["Failed to parse JSON from model output"] };
2358
- }
2359
- if (!Array.isArray(data)) return { phases: [], warnings: ["Model output is not a JSON array"] };
2360
- const warnings = [];
2361
- const phases = [];
2362
- for (const entry of data) {
2363
- const result = this.coercePhase(entry);
2364
- if (result) {
2365
- warnings.push(...result.warnings);
2366
- if (result.phase.taskTemplates?.length) {
2367
- phases.push(result.phase);
2368
- } else {
2369
- warnings.push(`Phase "${result.phase.name}" has no executable tasks and was rejected.`);
2370
- }
2371
- }
2372
- }
2373
- return { phases, warnings };
2374
- }
2375
- coercePhase(entry) {
2376
- if (!entry || typeof entry !== "object") return null;
2377
- const e = entry;
2378
- const name = typeof e.name === "string" ? e.name.trim() : "";
2379
- if (!name) return null;
2380
- const w = [];
2381
- const rawTasks = Array.isArray(e.tasks) ? e.tasks : Array.isArray(e.taskTemplates) ? e.taskTemplates : [];
2382
- const taskTemplates = [];
2383
- for (const t of rawTasks) {
2384
- const result = this.coerceTask(t);
2385
- if (result) {
2386
- taskTemplates.push(result.task);
2387
- w.push(...result.warnings);
2388
- }
2389
- }
2390
- const coercedPriority = coercePriority(e.priority);
2391
- if (typeof e.priority === "string" && coercedPriority !== e.priority) {
2392
- w.push('Phase "' + name + '": invalid priority "' + String(e.priority) + '", defaulting to "' + coercedPriority + '"');
2393
- }
2394
- return {
2395
- phase: {
2396
- name,
2397
- description: typeof e.description === "string" ? e.description : "",
2398
- priority: coercedPriority,
2399
- estimateHours: coerceHours(e.estimateHours, 4),
2400
- parallelizable: e.parallelizable === true,
2401
- taskTemplates
2402
- },
2403
- warnings: w
2404
- };
2405
- }
2406
- coerceTask(t) {
2407
- if (!t || typeof t !== "object") return null;
2408
- const o = t;
2409
- const title = typeof o.title === "string" ? o.title.trim() : "";
2410
- if (!title) return null;
2411
- const w = [];
2412
- let type = "feature";
2413
- if (typeof o.type === "string") {
2414
- if (VALID_TASK_TYPES.has(o.type)) {
2415
- type = o.type;
2416
- } else {
2417
- w.push('Task "' + title + '": invalid type "' + o.type + '", defaulting to "feature"');
2418
- }
2419
- }
2420
- const coercedPriority = coercePriority(o.priority);
2421
- if (typeof o.priority === "string" && coercedPriority !== o.priority) {
2422
- w.push('Task "' + title + '": invalid priority "' + String(o.priority) + '", defaulting to "' + coercedPriority + '"');
2423
- }
2424
- return {
2425
- task: {
2426
- title,
2427
- description: typeof o.description === "string" ? o.description : "",
2428
- type,
2429
- priority: coercedPriority,
2430
- estimateHours: coerceHours(o.estimateHours, 2),
2431
- tags: Array.isArray(o.tags) ? o.tags.map(String) : []
2432
- },
2433
- warnings: w
2434
- };
2435
- }
2436
- };
2437
- function coercePriority(value) {
2438
- return VALID_PRIORITIES.has(value) ? value : "medium";
2439
- }
2440
- function coerceHours(value, fallback) {
2441
- const n = Number(value);
2442
- return Number.isFinite(n) && n > 0 ? n : fallback;
2443
- }
2444
- function extractJSONArray(text) {
2445
- const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
2446
- const candidates = [];
2447
- if (fence?.[1]) candidates.push(fence[1]);
2448
- candidates.push(text);
2449
- for (const candidate of candidates) {
2450
- const balanced = firstBalancedArray(candidate);
2451
- if (balanced) return balanced;
2452
- }
2453
- return null;
2454
- }
2455
- function firstBalancedArray(text) {
2456
- const start = text.indexOf("[");
2457
- if (start === -1) return null;
2458
- let depth = 0;
2459
- let inString = false;
2460
- let escaped = false;
2461
- for (let i = start; i < text.length; i++) {
2462
- const ch = text[i] ?? "";
2463
- if (inString) {
2464
- if (escaped) {
2465
- escaped = false;
2466
- } else if (ch === "\\") {
2467
- escaped = true;
2468
- } else if (ch === '"') {
2469
- inString = false;
2470
- }
2471
- continue;
2472
- }
2473
- if (ch === '"') {
2474
- inString = true;
2475
- } else if (ch === "[") {
2476
- depth++;
2477
- } else if (ch === "]") {
2478
- depth--;
2479
- if (depth === 0) {
2480
- return text.slice(start, i + 1);
2481
- }
2482
- }
2483
- }
2484
- return null;
2485
- }
2486
-
2487
- // src/goal/goal-assessor.ts
2488
- var GoalAssessor = class {
2489
- constructor(opts) {
2490
- this.opts = opts;
2491
- }
2492
- opts;
2493
- /** Analyze the goal and return a structured realism assessment. */
2494
- async assess() {
2495
- const prompt = this.buildPrompt();
2496
- const raw = await this.opts.runOnce(prompt);
2497
- return this.parse(raw);
2498
- }
2499
- /** Build the instruction prompt for the LLM. */
2500
- buildPrompt() {
2501
- return renderInstructionTemplate(
2502
- readBundledInstructionText("goal/goal-assessor.md"),
2503
- { goal: this.opts.goal }
2504
- );
2505
- }
2506
- /** Extract and validate the JSON object from the raw LLM output. */
2507
- parse(raw) {
2508
- const json = extractJSONObject(raw);
2509
- if (!json) {
2510
- return {
2511
- realistic: true,
2512
- durationClaimed: null,
2513
- explanation: "",
2514
- recommendedDuration: null,
2515
- concerns: [],
2516
- raw,
2517
- parseFailed: true,
2518
- parseError: "No JSON object found in model output"
2519
- };
2520
- }
2521
- let data;
2522
- try {
2523
- data = JSON.parse(json);
2524
- } catch {
2525
- return {
2526
- realistic: true,
2527
- durationClaimed: null,
2528
- explanation: "",
2529
- recommendedDuration: null,
2530
- concerns: [],
2531
- raw,
2532
- parseFailed: true,
2533
- parseError: "Failed to parse JSON from model output"
2534
- };
2535
- }
2536
- return {
2537
- realistic: data.realistic === true,
2538
- durationClaimed: typeof data.durationClaimed === "string" ? data.durationClaimed : null,
2539
- explanation: typeof data.explanation === "string" ? data.explanation : "",
2540
- recommendedDuration: typeof data.recommendedDuration === "string" ? data.recommendedDuration : null,
2541
- concerns: Array.isArray(data.concerns) ? data.concerns.filter((c) => typeof c === "string") : [],
2542
- raw,
2543
- parseFailed: false
2544
- };
2545
- }
2546
- };
2547
- function extractJSONObject(text) {
2548
- const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
2549
- const candidates = [];
2550
- if (fence?.[1]) candidates.push(fence[1]);
2551
- candidates.push(text);
2552
- for (const candidate of candidates) {
2553
- const balanced = firstBalancedObject(candidate);
2554
- if (balanced) return balanced;
2555
- }
2556
- return null;
2557
- }
2558
- function firstBalancedObject(text) {
2559
- const start = text.indexOf("{");
2560
- if (start === -1) return null;
2561
- let depth = 0;
2562
- let inString = false;
2563
- let escaped = false;
2564
- for (let i = start; i < text.length; i++) {
2565
- const ch = text[i] ?? "";
2566
- if (inString) {
2567
- if (escaped) {
2568
- escaped = false;
2569
- } else if (ch === "\\") {
2570
- escaped = true;
2571
- } else if (ch === '"') {
2572
- inString = false;
2573
- }
2574
- continue;
2575
- }
2576
- if (ch === '"') {
2577
- inString = true;
2578
- } else if (ch === "{") {
2579
- depth++;
2580
- } else if (ch === "}") {
2581
- depth--;
2582
- if (depth === 0) {
2583
- return text.slice(start, i + 1);
2584
- }
2585
- }
2586
- }
2587
- return null;
2588
- }
2589
-
2590
- // src/goal/types.ts
2591
- var PHASE_EVENT_NAMES = [
2592
- "phase.statusChange",
2593
- "phase.started",
2594
- "phase.completed",
2595
- "phase.failed",
2596
- "phase.taskStarted",
2597
- "phase.taskCompleted",
2598
- "phase.taskFailed",
2599
- "phase.taskRetrying",
2600
- "phase.taskTimedOut",
2601
- "phase.allTasksDone",
2602
- "phase.verifying",
2603
- "phase.verifyFailed",
2604
- "phase.repairing",
2605
- "phase.conflictResolving",
2606
- "phase.conflictResolved",
2607
- "phase.taskMoved",
2608
- "phase.taskAssigned",
2609
- "phase.taskAdded",
2610
- "graph.completed",
2611
- "graph.failed",
2612
- "autonomous.tick",
2613
- "agent.assigned",
2614
- "agent.released"
2615
- ];
2616
-
2617
- // src/goal/phase-store.ts
2618
- import * as fsp2 from "node:fs/promises";
2619
- import * as path3 from "node:path";
2620
-
2621
- // src/utils/atomic-write.ts
2622
1
  import {
2623
- createPersistencePrimitives
2624
- } from "@wrongstack/persistence";
2625
- var primitives = createPersistencePrimitives({
2626
- createLockTimeoutError: ({ targetPath, timeoutMs }) => new FsError({
2627
- message: `Timed out waiting for file lock: ${targetPath}`,
2628
- code: "FS_ATOMIC_WRITE_FAILED",
2629
- path: targetPath,
2630
- context: { timeoutMs }
2631
- })
2632
- });
2633
- var atomicWrite = primitives.atomicWrite;
2634
- var atomicReplaceWithWriter = primitives.atomicReplaceWithWriter;
2635
- var ensureDir = primitives.ensureDir;
2636
- var withFileLock = primitives.withFileLock;
2637
-
2638
- // src/goal/phase-store.ts
2639
- var PHASE_STORE_VERSION = 1;
2640
- var PhaseStore = class {
2641
- baseDir;
2642
- legacyBaseDirs;
2643
- constructor(opts) {
2644
- this.baseDir = opts.baseDir;
2645
- this.legacyBaseDirs = (opts.legacyBaseDirs ?? []).filter(
2646
- (dir) => path3.resolve(dir) !== path3.resolve(this.baseDir)
2647
- );
2648
- }
2649
- async save(graph) {
2650
- const filePath = this.getFilePath(graph.id);
2651
- const serialized = this.serializeGraph(graph);
2652
- await withFileLock(filePath, async () => {
2653
- await atomicWrite(filePath, JSON.stringify(serialized, null, 2), { mode: 384 });
2654
- });
2655
- }
2656
- async load(graphId) {
2657
- const filePath = this.getFilePath(graphId);
2658
- const current = await this.loadFromPath(filePath);
2659
- if (current) return current;
2660
- for (const legacyDir of this.legacyBaseDirs) {
2661
- const legacyPath = path3.join(legacyDir, `${graphId}.json`);
2662
- const legacy = await this.loadFromPath(legacyPath);
2663
- if (!legacy) continue;
2664
- try {
2665
- await this.save(legacy);
2666
- await this.removeMigratedLegacyFile(legacyDir, legacyPath);
2667
- } catch {
2668
- return null;
2669
- }
2670
- return legacy;
2671
- }
2672
- return null;
2673
- }
2674
- async delete(graphId) {
2675
- const paths = [
2676
- this.getFilePath(graphId),
2677
- ...this.legacyBaseDirs.map((dir) => path3.join(dir, `${graphId}.json`))
2678
- ];
2679
- await Promise.all(paths.map((filePath) => fsp2.unlink(filePath).catch(() => void 0)));
2680
- }
2681
- async list() {
2682
- try {
2683
- await this.migrateLegacyGraphs();
2684
- const entries = await fsp2.readdir(this.baseDir, { withFileTypes: true });
2685
- const graphs = [];
2686
- for (const entry of entries) {
2687
- if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
2688
- try {
2689
- const raw = await fsp2.readFile(path3.join(this.baseDir, entry.name), "utf8");
2690
- const serialized = JSON.parse(raw);
2691
- const done = serialized.completedPhaseIds.length;
2692
- const total = serialized.phases.length;
2693
- graphs.push({
2694
- id: serialized.id,
2695
- title: serialized.title,
2696
- updatedAt: serialized.updatedAt,
2697
- status: done === total ? "completed" : done > 0 ? "in_progress" : "pending"
2698
- });
2699
- } catch {
2700
- }
2701
- }
2702
- return graphs.sort((a, b) => b.updatedAt - a.updatedAt);
2703
- } catch {
2704
- return [];
2705
- }
2706
- }
2707
- getFilePath(graphId) {
2708
- return path3.join(this.baseDir, `${graphId}.json`);
2709
- }
2710
- async loadFromPath(filePath) {
2711
- try {
2712
- const raw = await fsp2.readFile(filePath, "utf8");
2713
- const serialized = JSON.parse(raw);
2714
- return this.deserializeGraph(serialized);
2715
- } catch {
2716
- return null;
2717
- }
2718
- }
2719
- async migrateLegacyGraphs() {
2720
- for (const legacyDir of this.legacyBaseDirs) {
2721
- const entries = await fsp2.readdir(legacyDir, { withFileTypes: true }).catch(() => []);
2722
- for (const entry of entries) {
2723
- if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
2724
- const legacyPath = path3.join(legacyDir, entry.name);
2725
- const currentPath = this.getFilePath(path3.basename(entry.name, ".json"));
2726
- if (await this.pathExists(currentPath)) {
2727
- await this.removeMigratedLegacyFile(legacyDir, legacyPath);
2728
- continue;
2729
- }
2730
- const graph = await this.loadFromPath(legacyPath);
2731
- if (!graph) continue;
2732
- await this.save(graph);
2733
- await this.removeMigratedLegacyFile(legacyDir, legacyPath);
2734
- }
2735
- }
2736
- }
2737
- async pathExists(filePath) {
2738
- try {
2739
- await fsp2.access(filePath);
2740
- return true;
2741
- } catch {
2742
- return false;
2743
- }
2744
- }
2745
- async removeMigratedLegacyFile(legacyDir, legacyPath) {
2746
- await fsp2.unlink(legacyPath).catch(() => void 0);
2747
- await fsp2.rmdir(legacyDir).catch(() => void 0);
2748
- }
2749
- serializeGraph(graph) {
2750
- return {
2751
- version: PHASE_STORE_VERSION,
2752
- id: graph.id,
2753
- title: graph.title,
2754
- description: graph.description,
2755
- phases: Array.from(graph.phases.values()).map((p) => this.serializePhase(p)),
2756
- rootPhaseIds: graph.rootPhaseIds,
2757
- activePhaseIds: graph.activePhaseIds,
2758
- completedPhaseIds: graph.completedPhaseIds,
2759
- failedPhaseIds: graph.failedPhaseIds,
2760
- autonomous: graph.autonomous,
2761
- stopOnComplete: graph.stopOnComplete,
2762
- createdAt: graph.createdAt,
2763
- updatedAt: graph.updatedAt,
2764
- startedAt: graph.startedAt,
2765
- completedAt: graph.completedAt
2766
- };
2767
- }
2768
- serializePhase(phase) {
2769
- return {
2770
- id: phase.id,
2771
- name: phase.name,
2772
- description: phase.description,
2773
- status: phase.status,
2774
- taskGraph: this.serializeTaskGraph(phase.taskGraph),
2775
- dependsOn: phase.dependsOn,
2776
- nextPhases: phase.nextPhases,
2777
- parallelizable: phase.parallelizable,
2778
- priority: phase.priority,
2779
- estimateHours: phase.estimateHours,
2780
- actualDurationMs: phase.actualDurationMs,
2781
- startedAt: phase.startedAt,
2782
- completedAt: phase.completedAt,
2783
- assignedAgents: phase.assignedAgents,
2784
- metadata: phase.metadata,
2785
- createdAt: phase.createdAt,
2786
- updatedAt: phase.updatedAt
2787
- };
2788
- }
2789
- serializeTaskGraph(graph) {
2790
- return {
2791
- id: graph.id,
2792
- specId: graph.specId,
2793
- requiredRequirementIds: graph.requiredRequirementIds,
2794
- title: graph.title,
2795
- nodes: Array.from(graph.nodes.values()).map((n) => this.serializeTaskNode(n)),
2796
- edges: graph.edges,
2797
- rootNodes: graph.rootNodes,
2798
- createdAt: graph.createdAt,
2799
- updatedAt: graph.updatedAt
2800
- };
2801
- }
2802
- serializeTaskNode(node) {
2803
- return { ...node };
2804
- }
2805
- deserializeGraph(serialized) {
2806
- const fileVersion = serialized.version ?? 0;
2807
- if (fileVersion > PHASE_STORE_VERSION) {
2808
- throw new Error(
2809
- `Cannot load phase graph: file version ${fileVersion} is newer than supported version ${PHASE_STORE_VERSION}. Upgrade WrongStack to load this file.`
2810
- );
2811
- }
2812
- const phases = /* @__PURE__ */ new Map();
2813
- for (const sp of serialized.phases) {
2814
- phases.set(sp.id, this.deserializePhase(sp));
2815
- }
2816
- return {
2817
- id: serialized.id,
2818
- title: serialized.title,
2819
- description: serialized.description,
2820
- phases,
2821
- rootPhaseIds: serialized.rootPhaseIds,
2822
- activePhaseIds: serialized.activePhaseIds,
2823
- completedPhaseIds: serialized.completedPhaseIds,
2824
- failedPhaseIds: serialized.failedPhaseIds,
2825
- autonomous: serialized.autonomous,
2826
- stopOnComplete: serialized.stopOnComplete,
2827
- createdAt: serialized.createdAt,
2828
- updatedAt: serialized.updatedAt,
2829
- startedAt: serialized.startedAt,
2830
- completedAt: serialized.completedAt
2831
- };
2832
- }
2833
- deserializePhase(serialized) {
2834
- return {
2835
- id: serialized.id,
2836
- name: serialized.name,
2837
- description: serialized.description,
2838
- status: serialized.status,
2839
- taskGraph: this.deserializeTaskGraph(serialized.taskGraph),
2840
- dependsOn: serialized.dependsOn,
2841
- nextPhases: serialized.nextPhases,
2842
- parallelizable: serialized.parallelizable,
2843
- priority: serialized.priority,
2844
- estimateHours: serialized.estimateHours,
2845
- actualDurationMs: serialized.actualDurationMs,
2846
- startedAt: serialized.startedAt,
2847
- completedAt: serialized.completedAt,
2848
- assignedAgents: serialized.assignedAgents,
2849
- metadata: serialized.metadata,
2850
- createdAt: serialized.createdAt,
2851
- updatedAt: serialized.updatedAt
2852
- };
2853
- }
2854
- deserializeTaskGraph(serialized) {
2855
- const nodes = /* @__PURE__ */ new Map();
2856
- for (const sn of serialized.nodes) {
2857
- nodes.set(sn.id, sn);
2858
- }
2859
- return {
2860
- id: serialized.id,
2861
- specId: serialized.specId,
2862
- requiredRequirementIds: serialized.requiredRequirementIds,
2863
- title: serialized.title,
2864
- nodes,
2865
- edges: serialized.edges ?? [],
2866
- rootNodes: serialized.rootNodes ?? [],
2867
- createdAt: serialized.createdAt,
2868
- updatedAt: serialized.updatedAt
2869
- };
2870
- }
2871
- };
2872
-
2873
- // src/storage/goal-store.ts
2874
- import * as fsp3 from "node:fs/promises";
2875
-
2876
- // src/utils/wstack-paths.ts
2877
- import { createHash } from "node:crypto";
2878
- import * as fs from "node:fs";
2879
- import * as os from "node:os";
2880
- import * as path4 from "node:path";
2881
- function canonicalProjectRoot(absRoot) {
2882
- const checkoutRoot = path4.resolve(absRoot);
2883
- const dotGit = path4.join(checkoutRoot, ".git");
2884
- try {
2885
- if (!fs.statSync(dotGit).isFile()) return checkoutRoot;
2886
- const gitDirLine = fs.readFileSync(dotGit, "utf8").trim();
2887
- const match = /^gitdir:\s*(.+)$/i.exec(gitDirLine);
2888
- if (!match?.[1]) return checkoutRoot;
2889
- const gitDir = path4.resolve(checkoutRoot, match[1].trim());
2890
- const commonDirFile = path4.join(gitDir, "commondir");
2891
- if (!fs.statSync(commonDirFile).isFile()) return checkoutRoot;
2892
- const commonDir = path4.resolve(gitDir, fs.readFileSync(commonDirFile, "utf8").trim());
2893
- const worktreesDir = path4.dirname(gitDir);
2894
- if (path4.basename(worktreesDir).toLowerCase() !== "worktrees") return checkoutRoot;
2895
- if (path4.resolve(worktreesDir, "..") !== commonDir) return checkoutRoot;
2896
- if (path4.basename(commonDir).toLowerCase() !== ".git") return checkoutRoot;
2897
- return path4.dirname(commonDir);
2898
- } catch {
2899
- return checkoutRoot;
2900
- }
2901
- }
2902
- function projectHash(absRoot) {
2903
- return createHash("sha256").update(canonicalProjectRoot(absRoot)).digest("hex").slice(0, 12);
2904
- }
2905
- function projectSlug(absRoot) {
2906
- const identityRoot = canonicalProjectRoot(absRoot);
2907
- const base = slugify(path4.basename(identityRoot));
2908
- const hash = createHash("sha256").update(identityRoot).digest("hex").slice(0, 6);
2909
- return `${base}-${hash}`;
2910
- }
2911
- function slugify(name) {
2912
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
2913
- }
2914
- function safeProfileName(name) {
2915
- const safe = (name ?? "").replace(/[/\\:]/g, "_").replace(/\.\./g, "_").trim();
2916
- return safe || "default";
2917
- }
2918
- function bootstrapProfileName(globalRoot) {
2919
- try {
2920
- const parsed = JSON.parse(fs.readFileSync(path4.join(globalRoot, "config.json"), "utf8"));
2921
- return safeProfileName(
2922
- typeof parsed.activeProfile === "string" ? parsed.activeProfile : void 0
2923
- );
2924
- } catch {
2925
- return "default";
2926
- }
2927
- }
2928
- function wstackGlobalRoot() {
2929
- const fromEnv = process.env["WRONGSTACK_HOME"];
2930
- if (fromEnv && fromEnv.trim().length > 0) return path4.resolve(fromEnv);
2931
- return path4.join(os.homedir(), ".wrongstack");
2932
- }
2933
- function resolveWstackPaths(opts) {
2934
- const globalRoot = opts.globalRoot ?? (opts.userHome ? path4.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
2935
- const homeDir = opts.userHome ?? os.homedir();
2936
- const profileName = safeProfileName(opts.profileName ?? bootstrapProfileName(globalRoot));
2937
- const profileDir = path4.join(globalRoot, "profiles", profileName);
2938
- const hash = projectHash(opts.projectRoot);
2939
- const slug = projectSlug(opts.projectRoot);
2940
- const projectDir = path4.join(globalRoot, "projects", slug);
2941
- return {
2942
- globalRoot,
2943
- profileName,
2944
- profileDir,
2945
- projectRoot: opts.projectRoot,
2946
- homeDir,
2947
- configDir: profileDir,
2948
- globalConfig: path4.join(globalRoot, "config.json"),
2949
- profilesDir: path4.join(globalRoot, "profiles"),
2950
- profileConfig: (name) => {
2951
- return path4.join(globalRoot, "profiles", safeProfileName(name), "config.json");
2952
- },
2953
- profileStatuslineConfig: (name) => {
2954
- return path4.join(globalRoot, "profiles", safeProfileName(name), "statusline.json");
2955
- },
2956
- profileModeConfig: (name) => {
2957
- return path4.join(globalRoot, "profiles", safeProfileName(name), "mode.json");
2958
- },
2959
- profileProviderStatus: (name) => {
2960
- return path4.join(globalRoot, "profiles", safeProfileName(name), "provider-status.json");
2961
- },
2962
- profileUpdateCache: (name) => {
2963
- return path4.join(globalRoot, "profiles", safeProfileName(name), "update-cache.json");
2964
- },
2965
- secretsKey: path4.join(globalRoot, ".key"),
2966
- globalMemory: path4.join(profileDir, "memory.md"),
2967
- globalSkills: path4.join(profileDir, "skills"),
2968
- globalClaudeSkills: path4.join(homeDir, ".claude", "skills"),
2969
- globalDesignKits: path4.join(profileDir, "design-kits"),
2970
- globalPrompts: path4.join(profileDir, "prompts"),
2971
- globalInstructions: path4.join(profileDir, "instructions"),
2972
- promptUsage: path4.join(profileDir, "prompt-usage.json"),
2973
- cacheDir: path4.join(globalRoot, "cache"),
2974
- modelsCache: path4.join(globalRoot, "cache", "models.dev.json"),
2975
- modelsOverlayCache: path4.join(globalRoot, "cache", "models-overlay.json"),
2976
- historyFile: path4.join(profileDir, "history"),
2977
- logFile: path4.join(globalRoot, "logs", "wrongstack.log"),
2978
- projectDir,
2979
- projectCodebaseIndex: path4.join(projectDir, "codebase-index"),
2980
- projectMemory: path4.join(projectDir, "memory.md"),
2981
- projectSessions: path4.join(projectDir, "sessions"),
2982
- projectTrust: path4.join(projectDir, "trust.json"),
2983
- projectMeta: path4.join(projectDir, "meta.json"),
2984
- projectLocalConfig: path4.join(projectDir, "config.local.json"),
2985
- inProjectConfig: path4.join(opts.projectRoot, ".wrongstack", "config.json"),
2986
- inProjectAgentsFile: path4.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
2987
- inProjectSkills: path4.join(opts.projectRoot, ".wrongstack", "skills"),
2988
- inProjectClaudeSkills: path4.join(opts.projectRoot, ".claude", "skills"),
2989
- inProjectPrompts: path4.join(opts.projectRoot, ".wrongstack", "prompts"),
2990
- inProjectInstructions: path4.join(opts.projectRoot, ".wrongstack", "instructions"),
2991
- inProjectDesignKits: path4.join(opts.projectRoot, ".wrongstack", "design-kits"),
2992
- inProjectWorktrees: path4.join(opts.projectRoot, ".wrongstack", "worktrees"),
2993
- projectHash: hash,
2994
- projectSlug: slug,
2995
- projectGoal: path4.join(projectDir, "goal.json"),
2996
- projectInputHistory: path4.join(projectDir, "input-history.json"),
2997
- projectSpecs: path4.join(projectDir, "specs"),
2998
- projectTaskGraphs: path4.join(projectDir, "task-graphs"),
2999
- projectSddSession: path4.join(projectDir, "sdd-session.json"),
3000
- projectPlan: path4.join(projectDir, "plan.json"),
3001
- projectAutophase: path4.join(projectDir, "autophase"),
3002
- projectSddBoards: path4.join(projectDir, "sdd-boards"),
3003
- projectRequirementIntakes: path4.join(projectDir, "requirement-intakes"),
3004
- syncConfig: path4.join(profileDir, "sync.json"),
3005
- configHistoryDir: path4.join(globalRoot, "config-history"),
3006
- projectStatus: (statusProjectSlug) => {
3007
- if (!/^[a-z0-9](?:[a-z0-9-]{0,39})-[a-f0-9]{6}$/.test(statusProjectSlug)) {
3008
- throw new Error(`Invalid project slug: ${statusProjectSlug}`);
3009
- }
3010
- return path4.join(globalRoot, "projects", statusProjectSlug, "status.json");
3011
- }
3012
- };
3013
- }
3014
-
3015
- // src/storage/goal-store.ts
3016
- var MAX_JOURNAL_ENTRIES = 500;
3017
- function goalFilePath(projectRoot) {
3018
- return resolveWstackPaths({ projectRoot }).projectGoal;
3019
- }
3020
- async function loadGoal(filePath, events, warn) {
3021
- const t0 = Date.now();
3022
- let raw;
3023
- try {
3024
- raw = await fsp3.readFile(filePath, "utf8");
3025
- } catch (err) {
3026
- const code = err.code;
3027
- if (code === "ENOENT") {
3028
- events?.emit("storage.read", {
3029
- sessionId: "~boot~",
3030
- store: "goal",
3031
- filePath,
3032
- operation: "load",
3033
- outcome: "success",
3034
- durationMs: Date.now() - t0
3035
- });
3036
- return null;
3037
- }
3038
- events?.emit("storage.error", {
3039
- sessionId: "~boot~",
3040
- store: "goal",
3041
- filePath,
3042
- operation: "load",
3043
- error: toErrorMessage(err),
3044
- recoverable: false
3045
- });
3046
- throw err;
3047
- }
3048
- try {
3049
- const parsed = JSON.parse(raw);
3050
- if (parsed?.version !== 1 || typeof parsed.goal !== "string" || !Array.isArray(parsed.journal)) {
3051
- (warn ?? ((msg) => console.warn(JSON.stringify({ level: "warn", event: "goal_store.invalid_schema", path: filePath, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }))))(
3052
- "invalid schema \u2014 consider deleting and re-creating"
3053
- );
3054
- events?.emit("storage.read", {
3055
- sessionId: "~boot~",
3056
- store: "goal",
3057
- filePath,
3058
- operation: "load",
3059
- outcome: "failure",
3060
- durationMs: Date.now() - t0,
3061
- error: "invalid_schema"
3062
- });
3063
- return null;
3064
- }
3065
- events?.emit("storage.read", {
3066
- sessionId: "~boot~",
3067
- store: "goal",
3068
- filePath,
3069
- operation: "load",
3070
- outcome: "success",
3071
- durationMs: Date.now() - t0
3072
- });
3073
- return parsed;
3074
- } catch {
3075
- (warn ?? ((msg) => console.warn(JSON.stringify({ level: "warn", event: "goal_store.parse_failed", path: filePath, message: msg, timestamp: (/* @__PURE__ */ new Date()).toISOString() }))))(
3076
- "JSON parse failed \u2014 consider deleting and re-creating"
3077
- );
3078
- events?.emit("storage.read", {
3079
- sessionId: "~boot~",
3080
- store: "goal",
3081
- filePath,
3082
- operation: "load",
3083
- outcome: "failure",
3084
- durationMs: Date.now() - t0,
3085
- error: "parse_failed"
3086
- });
3087
- return null;
3088
- }
3089
- }
3090
- async function saveGoal(filePath, goal, events) {
3091
- const t0 = Date.now();
3092
- try {
3093
- await atomicWrite(filePath, JSON.stringify(goal, null, 2), { mode: 384 });
3094
- events?.emit("storage.write", {
3095
- sessionId: "~boot~",
3096
- store: "goal",
3097
- filePath,
3098
- operation: "save",
3099
- outcome: "success",
3100
- durationMs: Date.now() - t0
3101
- });
3102
- } catch (err) {
3103
- events?.emit("storage.error", {
3104
- sessionId: "~boot~",
3105
- store: "goal",
3106
- filePath,
3107
- operation: "save",
3108
- error: toErrorMessage(err),
3109
- recoverable: false
3110
- });
3111
- throw new FsError({
3112
- message: toErrorMessage(err),
3113
- code: ERROR_CODES.FS_ATOMIC_WRITE_FAILED,
3114
- path: filePath,
3115
- cause: err
3116
- });
3117
- }
3118
- }
3119
- function emptyGoal(goal) {
3120
- const now = (/* @__PURE__ */ new Date()).toISOString();
3121
- return {
3122
- version: 1,
3123
- goal,
3124
- setAt: now,
3125
- lastActivityAt: now,
3126
- iterations: 0,
3127
- engineState: "idle",
3128
- goalState: "active",
3129
- todoAttempts: {},
3130
- journal: []
3131
- };
3132
- }
3133
- function setProgress(goal, progress, note) {
3134
- const clamped = Math.min(100, Math.max(0, progress));
3135
- return {
3136
- ...goal,
3137
- progress: clamped,
3138
- progressNote: note ?? clamped + "% complete"
3139
- };
3140
- }
3141
- function appendJournal(goal, entry) {
3142
- const iteration = goal.iterations + 1;
3143
- const at = (/* @__PURE__ */ new Date()).toISOString();
3144
- const full = { ...entry, iteration, at };
3145
- const journal = [...goal.journal, full];
3146
- const trimmed = journal.length > MAX_JOURNAL_ENTRIES ? journal.slice(journal.length - MAX_JOURNAL_ENTRIES) : journal;
3147
- return {
3148
- ...goal,
3149
- iterations: iteration,
3150
- lastActivityAt: at,
3151
- journal: trimmed
3152
- };
3153
- }
3154
- function summarizeUsage(goal) {
3155
- let totalCostUsd = 0;
3156
- let totalInputTokens = 0;
3157
- let totalOutputTokens = 0;
3158
- let iterationsWithUsage = 0;
3159
- for (const e of goal.journal) {
3160
- if (typeof e.costUsd === "number") totalCostUsd += e.costUsd;
3161
- if (e.tokens) {
3162
- totalInputTokens += e.tokens.input;
3163
- totalOutputTokens += e.tokens.output;
3164
- }
3165
- if (typeof e.costUsd === "number" || e.tokens) iterationsWithUsage++;
3166
- }
3167
- return { totalCostUsd, totalInputTokens, totalOutputTokens, iterationsWithUsage };
3168
- }
3169
- var DOLLAR = "$";
3170
- function formatGoal(goal, journalLimit = 10) {
3171
- const lines = [];
3172
- const displayGoal = goal.refinedGoal || goal.goal;
3173
- lines.push(color.bold("Goal") + ": " + displayGoal);
3174
- if (goal.refinedGoal && goal.refinedGoal !== goal.goal) {
3175
- const snippet = goal.goal.length > 60 ? goal.goal.slice(0, 60) + "\u2026" : goal.goal;
3176
- lines.push(color.dim(' (original: "' + snippet + '")'));
3177
- }
3178
- if (typeof goal.progress === "number") {
3179
- const pct = Math.min(100, Math.max(0, Math.round(goal.progress)));
3180
- const filled = Math.round(pct / 5);
3181
- const empty = 20 - filled;
3182
- const bar = color.green("\u2588".repeat(filled)) + color.dim("\u2591".repeat(empty));
3183
- lines.push("Progress: " + bar + " " + color.bold(pct + "%"));
3184
- if (goal.progressNote) {
3185
- lines.push(" " + color.dim(goal.progressNote));
3186
- }
3187
- if (goal.progressTrend) {
3188
- const trendIcon = goal.progressTrend === "accelerating" ? "\u{1F680}" : goal.progressTrend === "stalling" ? "\u26A0\uFE0F" : "\u27A1\uFE0F";
3189
- lines.push(" Trend: " + trendIcon + " " + goal.progressTrend);
3190
- }
3191
- }
3192
- if (goal.deliverables && goal.deliverables.length > 0) {
3193
- lines.push("");
3194
- lines.push(color.bold("Deliverables:"));
3195
- for (const d of goal.deliverables) {
3196
- const done = /^\[[x✓]\]|✅|\(done\)/i.test(d);
3197
- const marker = done ? color.green("\u2713") : color.dim("\u25CB");
3198
- lines.push(" " + marker + " " + d);
3199
- }
3200
- }
3201
- lines.push("");
3202
- lines.push("Set: " + goal.setAt);
3203
- lines.push("Last activity: " + goal.lastActivityAt);
3204
- lines.push("Iterations: " + goal.iterations);
3205
- const stateLabel = goal.goalState ?? "active";
3206
- lines.push("State: " + stateLabel + (goal.iterations > 0 ? " (iteration #" + goal.iterations + ")" : ""));
3207
- lines.push("Engine: " + goal.engineState);
3208
- const usage = summarizeUsage(goal);
3209
- if (usage.iterationsWithUsage > 0) {
3210
- const spent = "Spent: " + DOLLAR + usage.totalCostUsd.toFixed(4) + " (in " + usage.totalInputTokens + " / out " + usage.totalOutputTokens + " tokens across " + usage.iterationsWithUsage + " iterations)";
3211
- lines.push(spent);
3212
- }
3213
- if (goal.journal.length > 0) {
3214
- lines.push("");
3215
- lines.push("Recent journal (last " + Math.min(journalLimit, goal.journal.length) + "):");
3216
- const tail = goal.journal.slice(-journalLimit);
3217
- for (const e of tail) {
3218
- const mark = e.status === "success" ? "\u2713" : e.status === "failure" ? "\u2717" : e.status === "aborted" ? "\u2298" : "\xB7";
3219
- const note = e.note ? " \u2014 " + e.note : "";
3220
- const cost = typeof e.costUsd === "number" ? " (" + DOLLAR + e.costUsd.toFixed(4) + ")" : "";
3221
- lines.push(" #" + e.iteration + " " + mark + " [" + e.source + "] " + e.task + cost + note);
3222
- }
3223
- }
3224
- return lines.join("\n");
3225
- }
3226
- function parseProgressFromText(text) {
3227
- const re = /\[progress:\s*(\d{1,3})%\]\s*(?:[—-]\s*(.+))?/i;
3228
- const m = text.match(re);
3229
- if (!m) return null;
3230
- const progress = Math.min(100, Math.max(0, Number.parseInt(m[1] ?? "0", 10)));
3231
- const note = m[2]?.trim() || void 0;
3232
- return note === void 0 ? { progress } : { progress, note };
3233
- }
3234
- function recordProgress(goal, progress, note) {
3235
- const clamped = Math.min(100, Math.max(0, progress));
3236
- const history = [...goal.progressHistory ?? [], { at: (/* @__PURE__ */ new Date()).toISOString(), progress: clamped, note }];
3237
- const trimmed = history.length > 200 ? history.slice(-200) : history;
3238
- return {
3239
- ...goal,
3240
- progress: clamped,
3241
- progressNote: note ?? `${clamped}% complete`,
3242
- progressHistory: trimmed,
3243
- progressTrend: computeTrend(trimmed)
3244
- };
3245
- }
3246
- var MAX_PROGRESS_HISTORY = 200;
3247
- function computeTrend(history) {
3248
- if (history.length < 3) return void 0;
3249
- const recent = history.slice(-5);
3250
- const deltas = [];
3251
- for (let i = 1; i < recent.length; i++) {
3252
- deltas.push((recent[i]?.progress ?? 0) - (recent[i - 1]?.progress ?? 0));
3253
- }
3254
- if (deltas.length < 2) return void 0;
3255
- const avgDelta = deltas.reduce((a, b) => a + b, 0) / deltas.length;
3256
- if (avgDelta > 2) return "accelerating";
3257
- if (avgDelta < -1) return "stalling";
3258
- return "steady";
3259
- }
3260
-
3261
- // src/goal/checkpoint.ts
3262
- import * as fsp4 from "node:fs/promises";
3263
- import * as path5 from "node:path";
3264
- var CheckpointManager = class {
3265
- store;
3266
- maxCheckpoints;
3267
- maxCheckpointAgeMs;
3268
- checkpoints = /* @__PURE__ */ new Map();
3269
- baseDir;
3270
- constructor(opts) {
3271
- this.store = opts.store;
3272
- this.maxCheckpoints = opts.maxCheckpoints ?? 10;
3273
- this.maxCheckpointAgeMs = opts.maxCheckpointAgeMs ?? 7 * 24 * 60 * 60 * 1e3;
3274
- this.baseDir = opts.baseDir ?? path5.join(opts.store.baseDir, ".checkpoints");
3275
- }
3276
- async initialize() {
3277
- await fsp4.mkdir(this.baseDir, { recursive: true });
3278
- await this.loadFromDisk();
3279
- }
3280
- async saveCheckpoint(graph, label) {
3281
- await this.store.save(graph);
3282
- const activePhase = Array.from(graph.phases.values()).find(
3283
- (p) => p.status === "running" || p.status === "paused"
3284
- );
3285
- const checkpoint = {
3286
- id: crypto.randomUUID(),
3287
- graphId: graph.id,
3288
- phaseId: activePhase?.id ?? graph.rootPhaseIds[0] ?? "",
3289
- phaseStatus: activePhase?.status ?? "pending",
3290
- taskStatuses: activePhase ? Array.from(activePhase.taskGraph.nodes.values()).map((t) => ({
3291
- taskId: t.id,
3292
- status: t.status,
3293
- title: t.title
3294
- })) : [],
3295
- timestamp: Date.now(),
3296
- label
3297
- };
3298
- this.checkpoints.set(checkpoint.id, checkpoint);
3299
- await this.saveToDisk(checkpoint);
3300
- await this.pruneCheckpoints(graph.id);
3301
- return checkpoint;
3302
- }
3303
- async restoreCheckpoint(checkpointId) {
3304
- let checkpoint = this.checkpoints.get(checkpointId);
3305
- if (!checkpoint) {
3306
- await this.loadFromDisk();
3307
- checkpoint = this.checkpoints.get(checkpointId);
3308
- }
3309
- if (!checkpoint) return null;
3310
- const graph = await this.store.load(checkpoint.graphId);
3311
- if (!graph) return null;
3312
- const phase = graph.phases.get(checkpoint.phaseId);
3313
- if (phase) {
3314
- phase.status = checkpoint.phaseStatus;
3315
- phase.updatedAt = Date.now();
3316
- for (const ts of checkpoint.taskStatuses) {
3317
- const task = phase.taskGraph.nodes.get(ts.taskId);
3318
- if (task) {
3319
- task.status = ts.status;
3320
- task.updatedAt = Date.now();
3321
- }
3322
- }
3323
- }
3324
- graph.updatedAt = Date.now();
3325
- return graph;
3326
- }
3327
- listCheckpoints(graphId) {
3328
- const all = Array.from(this.checkpoints.values());
3329
- const filtered = graphId ? all.filter((c) => c.graphId === graphId) : all;
3330
- return filtered.sort((a, b) => b.timestamp - a.timestamp);
3331
- }
3332
- async deleteCheckpoint(checkpointId) {
3333
- const checkpoint = this.checkpoints.get(checkpointId);
3334
- if (!checkpoint) return false;
3335
- this.checkpoints.delete(checkpointId);
3336
- await this.deleteFromDisk(checkpointId);
3337
- return true;
3338
- }
3339
- async saveToDisk(checkpoint) {
3340
- const filePath = path5.join(this.baseDir, `${checkpoint.graphId}.json`);
3341
- const serialized = {
3342
- ...checkpoint
3343
- };
3344
- await withFileLock(filePath, async () => {
3345
- let existing = [];
3346
- try {
3347
- const raw = await fsp4.readFile(filePath, "utf8");
3348
- const parsed = JSON.parse(raw);
3349
- if (Array.isArray(parsed)) {
3350
- existing = parsed;
3351
- }
3352
- } catch {
3353
- }
3354
- existing.push(serialized);
3355
- await atomicWrite(filePath, JSON.stringify(existing, null, 2), { mode: 384 });
3356
- });
3357
- }
3358
- async deleteFromDisk(checkpointId) {
3359
- let entries;
3360
- try {
3361
- entries = await fsp4.readdir(this.baseDir);
3362
- } catch {
3363
- return;
3364
- }
3365
- for (const filename of entries) {
3366
- if (!filename.endsWith(".json")) continue;
3367
- const filePath = path5.join(this.baseDir, filename);
3368
- try {
3369
- await withFileLock(filePath, async () => {
3370
- const raw = await fsp4.readFile(filePath, "utf8");
3371
- const parsed = JSON.parse(raw);
3372
- if (!Array.isArray(parsed)) return;
3373
- const existing = parsed;
3374
- const filtered = existing.filter((c) => c.id !== checkpointId);
3375
- if (filtered.length !== existing.length) {
3376
- if (filtered.length === 0) {
3377
- await fsp4.unlink(filePath);
3378
- } else {
3379
- await atomicWrite(filePath, JSON.stringify(filtered, null, 2), { mode: 384 });
3380
- }
3381
- }
3382
- });
3383
- } catch {
3384
- }
3385
- }
3386
- }
3387
- async loadFromDisk() {
3388
- let entries;
3389
- try {
3390
- entries = await fsp4.readdir(this.baseDir);
3391
- } catch {
3392
- return;
3393
- }
3394
- for (const filename of entries) {
3395
- if (!filename.endsWith(".json")) continue;
3396
- const filePath = path5.join(this.baseDir, filename);
3397
- try {
3398
- const raw = await fsp4.readFile(filePath, "utf8");
3399
- const parsed = JSON.parse(raw);
3400
- if (!Array.isArray(parsed)) continue;
3401
- const checkpoints = parsed;
3402
- for (const sc of checkpoints) {
3403
- const checkpoint = {
3404
- id: sc.id,
3405
- graphId: sc.graphId,
3406
- phaseId: sc.phaseId,
3407
- phaseStatus: sc.phaseStatus,
3408
- taskStatuses: sc.taskStatuses,
3409
- timestamp: sc.timestamp,
3410
- label: sc.label
3411
- };
3412
- this.checkpoints.set(checkpoint.id, checkpoint);
3413
- }
3414
- } catch {
3415
- }
3416
- }
3417
- }
3418
- async pruneCheckpoints(graphId) {
3419
- const now = Date.now();
3420
- const graphCheckpoints = Array.from(this.checkpoints.values()).filter((checkpoint) => checkpoint.graphId === graphId).sort((a, b) => a.timestamp - b.timestamp);
3421
- for (const cp of graphCheckpoints) {
3422
- if (now - cp.timestamp > this.maxCheckpointAgeMs) {
3423
- this.checkpoints.delete(cp.id);
3424
- await this.deleteFromDisk(cp.id);
3425
- }
3426
- }
3427
- const remaining = Array.from(this.checkpoints.values()).filter((checkpoint) => checkpoint.graphId === graphId).sort((a, b) => a.timestamp - b.timestamp);
3428
- while (remaining.length > this.maxCheckpoints) {
3429
- const oldest = remaining.shift();
3430
- if (oldest) {
3431
- this.checkpoints.delete(oldest.id);
3432
- await this.deleteFromDisk(oldest.id);
3433
- }
3434
- }
3435
- }
3436
- };
2
+ CheckpointManager,
3
+ GoalAssessor,
4
+ GoalPlanner,
5
+ GoalRunner,
6
+ PHASE_EVENT_NAMES,
7
+ PhaseGraphBuilder,
8
+ PhaseOrchestrator,
9
+ PhaseStore,
10
+ createGoalRunnerFromTaskGraph,
11
+ extractJSONArray
12
+ } from "../chunk-XQ2XGIBF.js";
13
+ import "../chunk-K4YUNKU7.js";
14
+ import {
15
+ MAX_JOURNAL_ENTRIES,
16
+ MAX_PROGRESS_HISTORY,
17
+ appendJournal,
18
+ emptyGoal,
19
+ formatGoal,
20
+ goalFilePath,
21
+ loadGoal,
22
+ parseProgressFromText,
23
+ recordProgress,
24
+ saveGoal,
25
+ setProgress,
26
+ summarizeUsage
27
+ } from "../chunk-LZDDLNCZ.js";
28
+ import "../chunk-WBPVKEEE.js";
29
+ import "../chunk-5S437PRC.js";
30
+ import "../chunk-4VWO3ZXL.js";
31
+ import "../chunk-7ZHZE5EO.js";
32
+ import "../chunk-YNCVSFGF.js";
33
+ import "../chunk-WOEZV3Q4.js";
34
+ import "../chunk-TBRL2RCC.js";
3437
35
  export {
3438
36
  CheckpointManager,
3439
37
  GoalAssessor,