opencastle 0.27.1 → 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 (66) hide show
  1. package/dist/cli/convoy/dashboard-types.d.ts +146 -0
  2. package/dist/cli/convoy/dashboard-types.d.ts.map +1 -0
  3. package/dist/cli/convoy/dashboard-types.js +2 -0
  4. package/dist/cli/convoy/dashboard-types.js.map +1 -0
  5. package/dist/cli/convoy/engine.d.ts +0 -1
  6. package/dist/cli/convoy/engine.d.ts.map +1 -1
  7. package/dist/cli/convoy/engine.js +31 -99
  8. package/dist/cli/convoy/engine.js.map +1 -1
  9. package/dist/cli/convoy/engine.test.js +88 -1
  10. package/dist/cli/convoy/engine.test.js.map +1 -1
  11. package/dist/cli/convoy/event-schemas.d.ts +9 -0
  12. package/dist/cli/convoy/event-schemas.d.ts.map +1 -0
  13. package/dist/cli/convoy/event-schemas.js +185 -0
  14. package/dist/cli/convoy/event-schemas.js.map +1 -0
  15. package/dist/cli/convoy/events.d.ts +8 -0
  16. package/dist/cli/convoy/events.d.ts.map +1 -1
  17. package/dist/cli/convoy/events.js +117 -5
  18. package/dist/cli/convoy/events.js.map +1 -1
  19. package/dist/cli/convoy/events.test.js +173 -3
  20. package/dist/cli/convoy/events.test.js.map +1 -1
  21. package/dist/cli/convoy/log-merge.test.d.ts +2 -0
  22. package/dist/cli/convoy/log-merge.test.d.ts.map +1 -0
  23. package/dist/cli/convoy/log-merge.test.js +147 -0
  24. package/dist/cli/convoy/log-merge.test.js.map +1 -0
  25. package/dist/cli/convoy/store.d.ts +52 -2
  26. package/dist/cli/convoy/store.d.ts.map +1 -1
  27. package/dist/cli/convoy/store.js +244 -17
  28. package/dist/cli/convoy/store.js.map +1 -1
  29. package/dist/cli/convoy/store.test.js +481 -22
  30. package/dist/cli/convoy/store.test.js.map +1 -1
  31. package/dist/cli/convoy/types.d.ts +271 -3
  32. package/dist/cli/convoy/types.d.ts.map +1 -1
  33. package/dist/cli/convoy/types.js +42 -1
  34. package/dist/cli/convoy/types.js.map +1 -1
  35. package/dist/cli/log.d.ts +11 -0
  36. package/dist/cli/log.d.ts.map +1 -1
  37. package/dist/cli/log.js +114 -2
  38. package/dist/cli/log.js.map +1 -1
  39. package/package.json +5 -1
  40. package/src/cli/convoy/TELEMETRY.md +203 -0
  41. package/src/cli/convoy/dashboard-types.ts +141 -0
  42. package/src/cli/convoy/engine.test.ts +99 -1
  43. package/src/cli/convoy/engine.ts +27 -96
  44. package/src/cli/convoy/event-schemas.ts +195 -0
  45. package/src/cli/convoy/events.test.ts +207 -3
  46. package/src/cli/convoy/events.ts +119 -5
  47. package/src/cli/convoy/log-merge.test.ts +179 -0
  48. package/src/cli/convoy/store.test.ts +545 -22
  49. package/src/cli/convoy/store.ts +274 -21
  50. package/src/cli/convoy/types.ts +108 -3
  51. package/src/cli/log.ts +120 -2
  52. package/src/dashboard/dist/_astro/{index.DtnyD8a5.css → index.6L3_HsPT.css} +1 -1
  53. package/src/dashboard/dist/data/.gitkeep +0 -0
  54. package/src/dashboard/dist/data/convoy-list.json +1 -0
  55. package/src/dashboard/dist/data/overall-stats.json +24 -0
  56. package/src/dashboard/dist/index.html +701 -3
  57. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  58. package/src/dashboard/public/data/.gitkeep +0 -0
  59. package/src/dashboard/public/data/convoy-list.json +1 -0
  60. package/src/dashboard/public/data/overall-stats.json +24 -0
  61. package/src/dashboard/scripts/etl.test.ts +210 -0
  62. package/src/dashboard/scripts/etl.ts +108 -0
  63. package/src/dashboard/scripts/integration-test.ts +504 -0
  64. package/src/dashboard/src/pages/index.astro +854 -15
  65. package/src/dashboard/src/styles/dashboard.css +557 -1
  66. package/src/orchestrator/prompts/generate-convoy.prompt.md +212 -13
@@ -1,25 +1,25 @@
1
1
  {
2
- "hash": "d3546064",
2
+ "hash": "31be5bae",
3
3
  "configHash": "30f8ea04",
4
- "lockfileHash": "2237f08d",
5
- "browserHash": "25793c93",
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": "e7dc1213",
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": "f017ff62",
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": "2b8684e9",
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
+ }