opencastle 0.27.0 → 0.27.2

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 (242) hide show
  1. package/bin/cli.mjs +6 -0
  2. package/dist/cli/agents.d.ts +3 -0
  3. package/dist/cli/agents.d.ts.map +1 -0
  4. package/dist/cli/agents.js +161 -0
  5. package/dist/cli/agents.js.map +1 -0
  6. package/dist/cli/baselines.d.ts +3 -0
  7. package/dist/cli/baselines.d.ts.map +1 -0
  8. package/dist/cli/baselines.js +128 -0
  9. package/dist/cli/baselines.js.map +1 -0
  10. package/dist/cli/convoy/dashboard-types.d.ts +146 -0
  11. package/dist/cli/convoy/dashboard-types.d.ts.map +1 -0
  12. package/dist/cli/convoy/dashboard-types.js +2 -0
  13. package/dist/cli/convoy/dashboard-types.js.map +1 -0
  14. package/dist/cli/convoy/engine.d.ts +67 -2
  15. package/dist/cli/convoy/engine.d.ts.map +1 -1
  16. package/dist/cli/convoy/engine.js +2036 -28
  17. package/dist/cli/convoy/engine.js.map +1 -1
  18. package/dist/cli/convoy/engine.test.js +1659 -70
  19. package/dist/cli/convoy/engine.test.js.map +1 -1
  20. package/dist/cli/convoy/event-schemas.d.ts +9 -0
  21. package/dist/cli/convoy/event-schemas.d.ts.map +1 -0
  22. package/dist/cli/convoy/event-schemas.js +185 -0
  23. package/dist/cli/convoy/event-schemas.js.map +1 -0
  24. package/dist/cli/convoy/events.d.ts +12 -1
  25. package/dist/cli/convoy/events.d.ts.map +1 -1
  26. package/dist/cli/convoy/events.js +186 -13
  27. package/dist/cli/convoy/events.js.map +1 -1
  28. package/dist/cli/convoy/events.test.js +325 -28
  29. package/dist/cli/convoy/events.test.js.map +1 -1
  30. package/dist/cli/convoy/expertise.d.ts +16 -0
  31. package/dist/cli/convoy/expertise.d.ts.map +1 -0
  32. package/dist/cli/convoy/expertise.js +121 -0
  33. package/dist/cli/convoy/expertise.js.map +1 -0
  34. package/dist/cli/convoy/expertise.test.d.ts +2 -0
  35. package/dist/cli/convoy/expertise.test.d.ts.map +1 -0
  36. package/dist/cli/convoy/expertise.test.js +96 -0
  37. package/dist/cli/convoy/expertise.test.js.map +1 -0
  38. package/dist/cli/convoy/export.test.js +1 -0
  39. package/dist/cli/convoy/export.test.js.map +1 -1
  40. package/dist/cli/convoy/formula.d.ts +19 -0
  41. package/dist/cli/convoy/formula.d.ts.map +1 -0
  42. package/dist/cli/convoy/formula.js +142 -0
  43. package/dist/cli/convoy/formula.js.map +1 -0
  44. package/dist/cli/convoy/formula.test.d.ts +2 -0
  45. package/dist/cli/convoy/formula.test.d.ts.map +1 -0
  46. package/dist/cli/convoy/formula.test.js +342 -0
  47. package/dist/cli/convoy/formula.test.js.map +1 -0
  48. package/dist/cli/convoy/gates.d.ts +128 -0
  49. package/dist/cli/convoy/gates.d.ts.map +1 -0
  50. package/dist/cli/convoy/gates.js +606 -0
  51. package/dist/cli/convoy/gates.js.map +1 -0
  52. package/dist/cli/convoy/gates.test.d.ts +2 -0
  53. package/dist/cli/convoy/gates.test.d.ts.map +1 -0
  54. package/dist/cli/convoy/gates.test.js +976 -0
  55. package/dist/cli/convoy/gates.test.js.map +1 -0
  56. package/dist/cli/convoy/health.d.ts +11 -0
  57. package/dist/cli/convoy/health.d.ts.map +1 -1
  58. package/dist/cli/convoy/health.js +54 -0
  59. package/dist/cli/convoy/health.js.map +1 -1
  60. package/dist/cli/convoy/health.test.js +56 -1
  61. package/dist/cli/convoy/health.test.js.map +1 -1
  62. package/dist/cli/convoy/issues.d.ts +8 -0
  63. package/dist/cli/convoy/issues.d.ts.map +1 -0
  64. package/dist/cli/convoy/issues.js +98 -0
  65. package/dist/cli/convoy/issues.js.map +1 -0
  66. package/dist/cli/convoy/issues.test.d.ts +2 -0
  67. package/dist/cli/convoy/issues.test.d.ts.map +1 -0
  68. package/dist/cli/convoy/issues.test.js +107 -0
  69. package/dist/cli/convoy/issues.test.js.map +1 -0
  70. package/dist/cli/convoy/knowledge.d.ts +5 -0
  71. package/dist/cli/convoy/knowledge.d.ts.map +1 -0
  72. package/dist/cli/convoy/knowledge.js +116 -0
  73. package/dist/cli/convoy/knowledge.js.map +1 -0
  74. package/dist/cli/convoy/knowledge.test.d.ts +2 -0
  75. package/dist/cli/convoy/knowledge.test.d.ts.map +1 -0
  76. package/dist/cli/convoy/knowledge.test.js +87 -0
  77. package/dist/cli/convoy/knowledge.test.js.map +1 -0
  78. package/dist/cli/convoy/lessons.d.ts +17 -0
  79. package/dist/cli/convoy/lessons.d.ts.map +1 -0
  80. package/dist/cli/convoy/lessons.js +149 -0
  81. package/dist/cli/convoy/lessons.js.map +1 -0
  82. package/dist/cli/convoy/lessons.test.d.ts +2 -0
  83. package/dist/cli/convoy/lessons.test.d.ts.map +1 -0
  84. package/dist/cli/convoy/lessons.test.js +135 -0
  85. package/dist/cli/convoy/lessons.test.js.map +1 -0
  86. package/dist/cli/convoy/lock.d.ts +13 -0
  87. package/dist/cli/convoy/lock.d.ts.map +1 -0
  88. package/dist/cli/convoy/lock.js +88 -0
  89. package/dist/cli/convoy/lock.js.map +1 -0
  90. package/dist/cli/convoy/lock.test.d.ts +2 -0
  91. package/dist/cli/convoy/lock.test.d.ts.map +1 -0
  92. package/dist/cli/convoy/lock.test.js +136 -0
  93. package/dist/cli/convoy/lock.test.js.map +1 -0
  94. package/dist/cli/convoy/log-merge.test.d.ts +2 -0
  95. package/dist/cli/convoy/log-merge.test.d.ts.map +1 -0
  96. package/dist/cli/convoy/log-merge.test.js +147 -0
  97. package/dist/cli/convoy/log-merge.test.js.map +1 -0
  98. package/dist/cli/convoy/merge.d.ts +4 -0
  99. package/dist/cli/convoy/merge.d.ts.map +1 -1
  100. package/dist/cli/convoy/merge.js +18 -1
  101. package/dist/cli/convoy/merge.js.map +1 -1
  102. package/dist/cli/convoy/merge.test.js +6 -7
  103. package/dist/cli/convoy/merge.test.js.map +1 -1
  104. package/dist/cli/convoy/partition.d.ts +51 -0
  105. package/dist/cli/convoy/partition.d.ts.map +1 -0
  106. package/dist/cli/convoy/partition.js +186 -0
  107. package/dist/cli/convoy/partition.js.map +1 -0
  108. package/dist/cli/convoy/partition.test.d.ts +2 -0
  109. package/dist/cli/convoy/partition.test.d.ts.map +1 -0
  110. package/dist/cli/convoy/partition.test.js +315 -0
  111. package/dist/cli/convoy/partition.test.js.map +1 -0
  112. package/dist/cli/convoy/pipeline.test.js +6 -0
  113. package/dist/cli/convoy/pipeline.test.js.map +1 -1
  114. package/dist/cli/convoy/store.d.ts +99 -7
  115. package/dist/cli/convoy/store.d.ts.map +1 -1
  116. package/dist/cli/convoy/store.js +764 -31
  117. package/dist/cli/convoy/store.js.map +1 -1
  118. package/dist/cli/convoy/store.test.js +1810 -18
  119. package/dist/cli/convoy/store.test.js.map +1 -1
  120. package/dist/cli/convoy/types.d.ts +427 -5
  121. package/dist/cli/convoy/types.d.ts.map +1 -1
  122. package/dist/cli/convoy/types.js +42 -1
  123. package/dist/cli/convoy/types.js.map +1 -1
  124. package/dist/cli/log.d.ts +11 -0
  125. package/dist/cli/log.d.ts.map +1 -1
  126. package/dist/cli/log.js +114 -2
  127. package/dist/cli/log.js.map +1 -1
  128. package/dist/cli/run/adapters/claude.d.ts +2 -0
  129. package/dist/cli/run/adapters/claude.d.ts.map +1 -1
  130. package/dist/cli/run/adapters/claude.js +89 -49
  131. package/dist/cli/run/adapters/claude.js.map +1 -1
  132. package/dist/cli/run/adapters/claude.test.d.ts +2 -0
  133. package/dist/cli/run/adapters/claude.test.d.ts.map +1 -0
  134. package/dist/cli/run/adapters/claude.test.js +205 -0
  135. package/dist/cli/run/adapters/claude.test.js.map +1 -0
  136. package/dist/cli/run/adapters/copilot.d.ts +1 -0
  137. package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
  138. package/dist/cli/run/adapters/copilot.js +84 -46
  139. package/dist/cli/run/adapters/copilot.js.map +1 -1
  140. package/dist/cli/run/adapters/copilot.test.d.ts +2 -0
  141. package/dist/cli/run/adapters/copilot.test.d.ts.map +1 -0
  142. package/dist/cli/run/adapters/copilot.test.js +195 -0
  143. package/dist/cli/run/adapters/copilot.test.js.map +1 -0
  144. package/dist/cli/run/adapters/cursor.d.ts +1 -0
  145. package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
  146. package/dist/cli/run/adapters/cursor.js +83 -47
  147. package/dist/cli/run/adapters/cursor.js.map +1 -1
  148. package/dist/cli/run/adapters/cursor.test.d.ts +2 -0
  149. package/dist/cli/run/adapters/cursor.test.d.ts.map +1 -0
  150. package/dist/cli/run/adapters/cursor.test.js +129 -0
  151. package/dist/cli/run/adapters/cursor.test.js.map +1 -0
  152. package/dist/cli/run/adapters/opencode.d.ts +1 -0
  153. package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
  154. package/dist/cli/run/adapters/opencode.js +81 -47
  155. package/dist/cli/run/adapters/opencode.js.map +1 -1
  156. package/dist/cli/run/adapters/opencode.test.d.ts +2 -0
  157. package/dist/cli/run/adapters/opencode.test.d.ts.map +1 -0
  158. package/dist/cli/run/adapters/opencode.test.js +119 -0
  159. package/dist/cli/run/adapters/opencode.test.js.map +1 -0
  160. package/dist/cli/run/executor.js +1 -1
  161. package/dist/cli/run/executor.js.map +1 -1
  162. package/dist/cli/run/schema.d.ts.map +1 -1
  163. package/dist/cli/run/schema.js +245 -4
  164. package/dist/cli/run/schema.js.map +1 -1
  165. package/dist/cli/run/schema.test.js +669 -0
  166. package/dist/cli/run/schema.test.js.map +1 -1
  167. package/dist/cli/run.d.ts.map +1 -1
  168. package/dist/cli/run.js +362 -22
  169. package/dist/cli/run.js.map +1 -1
  170. package/dist/cli/types.d.ts +85 -2
  171. package/dist/cli/types.d.ts.map +1 -1
  172. package/dist/cli/types.js.map +1 -1
  173. package/dist/cli/watch.d.ts +15 -0
  174. package/dist/cli/watch.d.ts.map +1 -0
  175. package/dist/cli/watch.js +279 -0
  176. package/dist/cli/watch.js.map +1 -0
  177. package/package.json +5 -1
  178. package/src/cli/agents.ts +177 -0
  179. package/src/cli/baselines.ts +143 -0
  180. package/src/cli/convoy/TELEMETRY.md +203 -0
  181. package/src/cli/convoy/dashboard-types.ts +141 -0
  182. package/src/cli/convoy/engine.test.ts +1937 -70
  183. package/src/cli/convoy/engine.ts +2350 -40
  184. package/src/cli/convoy/event-schemas.ts +195 -0
  185. package/src/cli/convoy/events.test.ts +384 -39
  186. package/src/cli/convoy/events.ts +202 -16
  187. package/src/cli/convoy/expertise.test.ts +128 -0
  188. package/src/cli/convoy/expertise.ts +163 -0
  189. package/src/cli/convoy/export.test.ts +1 -0
  190. package/src/cli/convoy/formula.test.ts +405 -0
  191. package/src/cli/convoy/formula.ts +174 -0
  192. package/src/cli/convoy/gates.test.ts +1169 -0
  193. package/src/cli/convoy/gates.ts +774 -0
  194. package/src/cli/convoy/health.test.ts +64 -2
  195. package/src/cli/convoy/health.ts +80 -2
  196. package/src/cli/convoy/issues.test.ts +143 -0
  197. package/src/cli/convoy/issues.ts +136 -0
  198. package/src/cli/convoy/knowledge.test.ts +101 -0
  199. package/src/cli/convoy/knowledge.ts +132 -0
  200. package/src/cli/convoy/lessons.test.ts +188 -0
  201. package/src/cli/convoy/lessons.ts +164 -0
  202. package/src/cli/convoy/lock.test.ts +181 -0
  203. package/src/cli/convoy/lock.ts +103 -0
  204. package/src/cli/convoy/log-merge.test.ts +179 -0
  205. package/src/cli/convoy/merge.test.ts +6 -7
  206. package/src/cli/convoy/merge.ts +19 -1
  207. package/src/cli/convoy/partition.test.ts +423 -0
  208. package/src/cli/convoy/partition.ts +232 -0
  209. package/src/cli/convoy/pipeline.test.ts +6 -0
  210. package/src/cli/convoy/store.test.ts +2041 -20
  211. package/src/cli/convoy/store.ts +945 -46
  212. package/src/cli/convoy/types.ts +278 -4
  213. package/src/cli/log.ts +120 -2
  214. package/src/cli/run/adapters/claude.test.ts +234 -0
  215. package/src/cli/run/adapters/claude.ts +45 -5
  216. package/src/cli/run/adapters/copilot.test.ts +224 -0
  217. package/src/cli/run/adapters/copilot.ts +34 -4
  218. package/src/cli/run/adapters/cursor.test.ts +144 -0
  219. package/src/cli/run/adapters/cursor.ts +33 -2
  220. package/src/cli/run/adapters/opencode.test.ts +135 -0
  221. package/src/cli/run/adapters/opencode.ts +30 -2
  222. package/src/cli/run/executor.ts +1 -1
  223. package/src/cli/run/schema.test.ts +758 -0
  224. package/src/cli/run/schema.ts +300 -25
  225. package/src/cli/run.ts +341 -21
  226. package/src/cli/types.ts +86 -1
  227. package/src/cli/watch.ts +298 -0
  228. package/src/dashboard/dist/_astro/{index.DtnyD8a5.css → index.6L3_HsPT.css} +1 -1
  229. package/src/dashboard/dist/data/.gitkeep +0 -0
  230. package/src/dashboard/dist/data/convoy-list.json +1 -0
  231. package/src/dashboard/dist/data/overall-stats.json +24 -0
  232. package/src/dashboard/dist/index.html +701 -3
  233. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  234. package/src/dashboard/public/data/.gitkeep +0 -0
  235. package/src/dashboard/public/data/convoy-list.json +1 -0
  236. package/src/dashboard/public/data/overall-stats.json +24 -0
  237. package/src/dashboard/scripts/etl.test.ts +210 -0
  238. package/src/dashboard/scripts/etl.ts +108 -0
  239. package/src/dashboard/scripts/integration-test.ts +504 -0
  240. package/src/dashboard/src/pages/index.astro +854 -15
  241. package/src/dashboard/src/styles/dashboard.css +557 -1
  242. package/src/orchestrator/prompts/generate-convoy.prompt.md +212 -13
@@ -1,25 +1,25 @@
1
1
  {
2
- "hash": "46302718",
2
+ "hash": "31be5bae",
3
3
  "configHash": "30f8ea04",
4
- "lockfileHash": "0bec59b6",
5
- "browserHash": "455152ca",
4
+ "lockfileHash": "68ecee18",
5
+ "browserHash": "fb400a2a",
6
6
  "optimized": {
7
7
  "astro > cssesc": {
8
8
  "src": "../../../../../node_modules/cssesc/cssesc.js",
9
9
  "file": "astro___cssesc.js",
10
- "fileHash": "5409385e",
10
+ "fileHash": "05ee9b55",
11
11
  "needsInterop": true
12
12
  },
13
13
  "astro > aria-query": {
14
14
  "src": "../../../../../node_modules/aria-query/lib/index.js",
15
15
  "file": "astro___aria-query.js",
16
- "fileHash": "5fd7c72e",
16
+ "fileHash": "7eac81b0",
17
17
  "needsInterop": true
18
18
  },
19
19
  "astro > axobject-query": {
20
20
  "src": "../../../../../node_modules/axobject-query/lib/index.js",
21
21
  "file": "astro___axobject-query.js",
22
- "fileHash": "530306fc",
22
+ "fileHash": "891e021d",
23
23
  "needsInterop": true
24
24
  }
25
25
  },
File without changes
@@ -0,0 +1,24 @@
1
+ {
2
+ "convoyCounts": {
3
+ "total": 0,
4
+ "running": 0,
5
+ "done": 0,
6
+ "failed": 0,
7
+ "gate_failed": 0
8
+ },
9
+ "durationStats": {
10
+ "avg_sec": null,
11
+ "p95_sec": null,
12
+ "max_sec": null
13
+ },
14
+ "tokenCostTotals": {
15
+ "total_tokens": 0,
16
+ "total_cost_usd": 0
17
+ },
18
+ "topAgents": [],
19
+ "topModels": [],
20
+ "dlqSummary": {
21
+ "count": 0,
22
+ "top_failure_types": []
23
+ }
24
+ }
@@ -0,0 +1,210 @@
1
+ import { mkdtempSync, rmSync, realpathSync, readFileSync, existsSync, mkdirSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
5
+ import { runEtl } from './etl.js'
6
+ import { createConvoyStore } from '../../cli/convoy/store.js'
7
+
8
+ function makeTmpDir(): string {
9
+ return realpathSync(mkdtempSync(join(tmpdir(), 'etl-test-')))
10
+ }
11
+
12
+ let tmpDir: string
13
+ let outputDir: string
14
+
15
+ beforeEach(() => {
16
+ tmpDir = makeTmpDir()
17
+ outputDir = join(tmpDir, 'data')
18
+ })
19
+
20
+ afterEach(() => {
21
+ rmSync(tmpDir, { recursive: true, force: true })
22
+ })
23
+
24
+ describe('runEtl — no database', () => {
25
+ it('writes empty overall-stats.json when db is missing', async () => {
26
+ const dbPath = join(tmpDir, 'nonexistent.db')
27
+ await runEtl({ dbPath, outputDir })
28
+ const stats = JSON.parse(readFileSync(join(outputDir, 'overall-stats.json'), 'utf8'))
29
+ expect(stats).toMatchObject({
30
+ convoyCounts: { total: 0, running: 0, done: 0, failed: 0, gate_failed: 0 },
31
+ durationStats: { avg_sec: null, p95_sec: null, max_sec: null },
32
+ tokenCostTotals: { total_tokens: 0, total_cost_usd: 0 },
33
+ topAgents: [],
34
+ topModels: [],
35
+ dlqSummary: { count: 0, top_failure_types: [] },
36
+ })
37
+ })
38
+
39
+ it('writes empty convoy-list.json when db is missing', async () => {
40
+ const dbPath = join(tmpDir, 'nonexistent.db')
41
+ await runEtl({ dbPath, outputDir })
42
+ const list = JSON.parse(readFileSync(join(outputDir, 'convoy-list.json'), 'utf8'))
43
+ expect(Array.isArray(list)).toBe(true)
44
+ expect(list).toHaveLength(0)
45
+ })
46
+
47
+ it('returns zero counts when db is missing', async () => {
48
+ const dbPath = join(tmpDir, 'nonexistent.db')
49
+ const result = await runEtl({ dbPath, outputDir })
50
+ expect(result).toEqual({ convoyCount: 0, taskCount: 0 })
51
+ })
52
+
53
+ it('creates the output directory structure even when db is missing', async () => {
54
+ const dbPath = join(tmpDir, 'nonexistent.db')
55
+ await runEtl({ dbPath, outputDir })
56
+ expect(existsSync(outputDir)).toBe(true)
57
+ expect(existsSync(join(outputDir, 'convoys'))).toBe(true)
58
+ })
59
+ })
60
+
61
+ describe('runEtl — with seeded database', () => {
62
+ let dbPath: string
63
+
64
+ beforeEach(() => {
65
+ dbPath = join(tmpDir, 'convoy.db')
66
+ const store = createConvoyStore(dbPath)
67
+ try {
68
+ store.insertConvoy({
69
+ id: 'convoy-abc',
70
+ name: 'Test Convoy',
71
+ spec_hash: 'abc123',
72
+ status: 'done',
73
+ branch: 'main',
74
+ created_at: '2026-03-01T10:00:00.000Z',
75
+ spec_yaml: 'tasks: []',
76
+ })
77
+ store.insertConvoy({
78
+ id: 'convoy-def',
79
+ name: 'Second Convoy',
80
+ spec_hash: 'def456',
81
+ status: 'failed',
82
+ branch: null,
83
+ created_at: '2026-03-02T10:00:00.000Z',
84
+ spec_yaml: 'tasks: []',
85
+ })
86
+ store.insertTask({
87
+ id: 'task-001',
88
+ convoy_id: 'convoy-abc',
89
+ phase: 1,
90
+ prompt: 'Do the thing',
91
+ agent: 'developer',
92
+ adapter: null,
93
+ model: 'claude-opus-4-6',
94
+ timeout_ms: 30000,
95
+ status: 'done',
96
+ retries: 0,
97
+ depends_on: null,
98
+ files: null,
99
+ gates: null,
100
+ max_retries: 3,
101
+ })
102
+ store.insertTask({
103
+ id: 'task-002',
104
+ convoy_id: 'convoy-abc',
105
+ phase: 2,
106
+ prompt: 'Do another thing',
107
+ agent: 'reviewer',
108
+ adapter: null,
109
+ model: 'claude-opus-4-6',
110
+ timeout_ms: 30000,
111
+ status: 'done',
112
+ retries: 1,
113
+ depends_on: null,
114
+ files: null,
115
+ gates: null,
116
+ max_retries: 3,
117
+ })
118
+ } finally {
119
+ store.close()
120
+ }
121
+ })
122
+
123
+ it('returns correct convoy and task counts', async () => {
124
+ const result = await runEtl({ dbPath, outputDir })
125
+ expect(result.convoyCount).toBe(2)
126
+ expect(result.taskCount).toBe(2)
127
+ })
128
+
129
+ it('overall-stats.json has correct convoy counts', async () => {
130
+ await runEtl({ dbPath, outputDir })
131
+ const stats = JSON.parse(readFileSync(join(outputDir, 'overall-stats.json'), 'utf8'))
132
+ expect(stats.convoyCounts).toMatchObject({ total: 2 })
133
+ expect(stats.durationStats).toHaveProperty('avg_sec')
134
+ expect(stats.tokenCostTotals).toHaveProperty('total_tokens')
135
+ expect(Array.isArray(stats.topAgents)).toBe(true)
136
+ expect(Array.isArray(stats.topModels)).toBe(true)
137
+ expect(stats.dlqSummary).toHaveProperty('count')
138
+ })
139
+
140
+ it('convoy-list.json contains all convoys with required fields', async () => {
141
+ await runEtl({ dbPath, outputDir })
142
+ const list = JSON.parse(readFileSync(join(outputDir, 'convoy-list.json'), 'utf8'))
143
+ expect(list).toHaveLength(2)
144
+ for (const item of list) {
145
+ expect(item).toHaveProperty('id')
146
+ expect(item).toHaveProperty('name')
147
+ expect(item).toHaveProperty('status')
148
+ expect(item).toHaveProperty('created_at')
149
+ expect(item).toHaveProperty('finished_at')
150
+ expect(item).toHaveProperty('total_tokens')
151
+ expect(item).toHaveProperty('total_cost_usd')
152
+ }
153
+ })
154
+
155
+ it('creates per-convoy detail JSON files', async () => {
156
+ await runEtl({ dbPath, outputDir })
157
+ const detailPath = join(outputDir, 'convoys', 'convoy-abc.json')
158
+ expect(existsSync(detailPath)).toBe(true)
159
+ const detail = JSON.parse(readFileSync(detailPath, 'utf8'))
160
+ expect(detail.convoy.id).toBe('convoy-abc')
161
+ expect(detail.convoy.name).toBe('Test Convoy')
162
+ expect(detail.convoy.status).toBe('done')
163
+ expect(detail.convoy).toHaveProperty('branch')
164
+ expect(detail.convoy).toHaveProperty('total_tokens')
165
+ expect(detail.convoy).toHaveProperty('total_cost_usd')
166
+ expect(detail).toHaveProperty('taskSummary')
167
+ expect(detail.taskSummary).toHaveProperty('total')
168
+ expect(Array.isArray(detail.tasks)).toBe(true)
169
+ })
170
+
171
+ it('detail file has correct task fields', async () => {
172
+ await runEtl({ dbPath, outputDir })
173
+ const detail = JSON.parse(
174
+ readFileSync(join(outputDir, 'convoys', 'convoy-abc.json'), 'utf8'),
175
+ )
176
+ expect(detail.tasks).toHaveLength(2)
177
+ for (const task of detail.tasks) {
178
+ expect(task).toHaveProperty('id')
179
+ expect(task).toHaveProperty('phase')
180
+ expect(task).toHaveProperty('agent')
181
+ expect(task).toHaveProperty('model')
182
+ expect(task).toHaveProperty('status')
183
+ expect(task).toHaveProperty('retries')
184
+ expect(task).toHaveProperty('started_at')
185
+ expect(task).toHaveProperty('finished_at')
186
+ expect(task).toHaveProperty('total_tokens')
187
+ expect(task).toHaveProperty('cost_usd')
188
+ expect(task).toHaveProperty('review_level')
189
+ expect(task).toHaveProperty('review_verdict')
190
+ expect(task).toHaveProperty('drift_score')
191
+ }
192
+ })
193
+
194
+ it('creates detail file for each convoy', async () => {
195
+ await runEtl({ dbPath, outputDir })
196
+ expect(existsSync(join(outputDir, 'convoys', 'convoy-abc.json'))).toBe(true)
197
+ expect(existsSync(join(outputDir, 'convoys', 'convoy-def.json'))).toBe(true)
198
+ })
199
+
200
+ it('detail file includes artifacts and events fields', async () => {
201
+ await runEtl({ dbPath, outputDir })
202
+ const detail = JSON.parse(
203
+ readFileSync(join(outputDir, 'convoys', 'convoy-abc.json'), 'utf8'),
204
+ )
205
+ expect(Array.isArray(detail.artifacts)).toBe(true)
206
+ expect(typeof detail.artifact_count).toBe('number')
207
+ expect(typeof detail.has_more_events).toBe('boolean')
208
+ expect(Array.isArray(detail.events)).toBe(true)
209
+ })
210
+ })
@@ -0,0 +1,108 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
2
+ import { resolve, dirname } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ const __filename = fileURLToPath(import.meta.url)
6
+ const __dirname = dirname(__filename)
7
+
8
+ export interface EtlOptions {
9
+ dbPath: string
10
+ outputDir: string
11
+ }
12
+
13
+ export interface EtlResult {
14
+ convoyCount: number
15
+ taskCount: number
16
+ }
17
+
18
+ const EMPTY_OVERALL_STATS = {
19
+ convoyCounts: { total: 0, running: 0, done: 0, failed: 0, gate_failed: 0 },
20
+ durationStats: { avg_sec: null, p95_sec: null, max_sec: null },
21
+ tokenCostTotals: { total_tokens: 0, total_cost_usd: 0 },
22
+ topAgents: [] as unknown[],
23
+ topModels: [] as unknown[],
24
+ dlqSummary: { count: 0, top_failure_types: [] as unknown[] },
25
+ }
26
+
27
+ export async function runEtl(options: EtlOptions): Promise<EtlResult> {
28
+ const { dbPath, outputDir } = options
29
+
30
+ mkdirSync(outputDir, { recursive: true })
31
+ mkdirSync(resolve(outputDir, 'convoys'), { recursive: true })
32
+
33
+ if (!existsSync(dbPath)) {
34
+ console.warn(` \u26a0 No convoy database found at ${dbPath}. Writing empty JSON files.`)
35
+ writeFileSync(
36
+ resolve(outputDir, 'overall-stats.json'),
37
+ JSON.stringify(EMPTY_OVERALL_STATS, null, 2),
38
+ 'utf8',
39
+ )
40
+ writeFileSync(resolve(outputDir, 'convoy-list.json'), JSON.stringify([], null, 2), 'utf8')
41
+ return { convoyCount: 0, taskCount: 0 }
42
+ }
43
+
44
+ const { createConvoyStore } = await import('../../cli/convoy/store.js')
45
+ const store = createConvoyStore(dbPath)
46
+
47
+ try {
48
+ const overallStats = {
49
+ convoyCounts: store.getConvoyCounts(),
50
+ durationStats: store.getConvoyDurationStats(),
51
+ tokenCostTotals: store.getTokenAndCostTotals(),
52
+ topAgents: store.getTopAgents(5),
53
+ topModels: store.getTopModels(5),
54
+ dlqSummary: store.getDlqSummary(),
55
+ }
56
+ writeFileSync(
57
+ resolve(outputDir, 'overall-stats.json'),
58
+ JSON.stringify(overallStats, null, 2),
59
+ 'utf8',
60
+ )
61
+
62
+ const allConvoys = store.getConvoyList(1000, 0)
63
+ const convoyList = allConvoys.map(c => ({
64
+ id: c.id,
65
+ name: c.name,
66
+ status: c.status,
67
+ created_at: c.created_at,
68
+ finished_at: c.finished_at,
69
+ total_tokens: c.total_tokens,
70
+ total_cost_usd: c.total_cost_usd,
71
+ }))
72
+ writeFileSync(
73
+ resolve(outputDir, 'convoy-list.json'),
74
+ JSON.stringify(convoyList, null, 2),
75
+ 'utf8',
76
+ )
77
+
78
+ let totalTasks = 0
79
+ for (const convoy of allConvoys) {
80
+ const detail = store.getConvoyDetails(convoy.id)
81
+ if (detail) {
82
+ totalTasks += detail.tasks.length
83
+ writeFileSync(
84
+ resolve(outputDir, 'convoys', `${convoy.id}.json`),
85
+ JSON.stringify(detail, null, 2),
86
+ 'utf8',
87
+ )
88
+ }
89
+ }
90
+
91
+ console.log(`ETL complete: ${allConvoys.length} convoys exported, ${totalTasks} tasks.`)
92
+ return { convoyCount: allConvoys.length, taskCount: totalTasks }
93
+ } finally {
94
+ store.close()
95
+ }
96
+ }
97
+
98
+ const isMain =
99
+ process.argv[1] != null &&
100
+ fileURLToPath(import.meta.url) === resolve(process.argv[1])
101
+ if (isMain) {
102
+ const dbPath = resolve(process.cwd(), '.opencastle', 'convoy.db')
103
+ const outputDir = resolve(__dirname, '..', 'public', 'data')
104
+ runEtl({ dbPath, outputDir }).catch((err: unknown) => {
105
+ console.error('ETL failed:', (err as Error).message)
106
+ process.exit(1)
107
+ })
108
+ }