opencastle 0.27.3 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -3
- package/bin/cli.mjs +13 -5
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +2 -11
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +2 -1
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/export.d.ts +1 -3
- package/dist/cli/convoy/export.d.ts.map +1 -1
- package/dist/cli/convoy/export.js +9 -88
- package/dist/cli/convoy/export.js.map +1 -1
- package/dist/cli/convoy/export.test.js +7 -186
- package/dist/cli/convoy/export.test.js.map +1 -1
- package/dist/cli/convoy/issues.js +3 -3
- package/dist/cli/convoy/issues.js.map +1 -1
- package/dist/cli/convoy/issues.test.js +4 -3
- package/dist/cli/convoy/issues.test.js.map +1 -1
- package/dist/cli/convoy/pipeline.d.ts.map +1 -1
- package/dist/cli/convoy/pipeline.js +0 -21
- package/dist/cli/convoy/pipeline.js.map +1 -1
- package/dist/cli/convoy/pipeline.test.js +0 -21
- package/dist/cli/convoy/pipeline.test.js.map +1 -1
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +32 -8
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/destroy.d.ts.map +1 -1
- package/dist/cli/destroy.js +13 -0
- package/dist/cli/destroy.js.map +1 -1
- package/dist/cli/dispute.d.ts +3 -0
- package/dist/cli/dispute.d.ts.map +1 -0
- package/dist/cli/dispute.js +25 -0
- package/dist/cli/dispute.js.map +1 -0
- package/dist/cli/doctor.d.ts +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +14 -1
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/eject.d.ts.map +1 -1
- package/dist/cli/eject.js +14 -0
- package/dist/cli/eject.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +14 -0
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/log.d.ts +0 -11
- package/dist/cli/log.d.ts.map +1 -1
- package/dist/cli/log.js +2 -114
- package/dist/cli/log.js.map +1 -1
- package/dist/cli/pipeline.d.ts +3 -0
- package/dist/cli/pipeline.d.ts.map +1 -0
- package/dist/cli/pipeline.js +321 -0
- package/dist/cli/pipeline.js.map +1 -0
- package/dist/cli/plan.d.ts +37 -0
- package/dist/cli/plan.d.ts.map +1 -1
- package/dist/cli/plan.js +321 -161
- package/dist/cli/plan.js.map +1 -1
- package/dist/cli/run.js +2 -2
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +16 -0
- package/dist/cli/update.js.map +1 -1
- package/dist/cli/validate.d.ts +3 -0
- package/dist/cli/validate.d.ts.map +1 -0
- package/dist/cli/validate.js +60 -0
- package/dist/cli/validate.js.map +1 -0
- package/dist/cli/watch.d.ts.map +1 -1
- package/dist/cli/watch.js +1 -3
- package/dist/cli/watch.js.map +1 -1
- package/package.json +5 -4
- package/src/cli/convoy/engine.test.ts +2 -1
- package/src/cli/convoy/engine.ts +2 -5
- package/src/cli/convoy/export.test.ts +7 -224
- package/src/cli/convoy/export.ts +10 -106
- package/src/cli/convoy/issues.test.ts +3 -2
- package/src/cli/convoy/issues.ts +3 -3
- package/src/cli/convoy/pipeline.test.ts +0 -25
- package/src/cli/convoy/pipeline.ts +0 -19
- package/src/cli/dashboard.ts +33 -8
- package/src/cli/destroy.ts +15 -0
- package/src/cli/dispute.ts +28 -0
- package/src/cli/doctor.ts +16 -1
- package/src/cli/eject.ts +16 -0
- package/src/cli/init.ts +16 -0
- package/src/cli/log.ts +2 -120
- package/src/cli/pipeline.ts +362 -0
- package/src/cli/plan.ts +357 -153
- package/src/cli/run.ts +2 -2
- package/src/cli/update.ts +18 -0
- package/src/cli/validate.ts +65 -0
- package/src/cli/watch.ts +1 -3
- package/src/dashboard/dist/_astro/index.Je1YjU_y.css +1 -0
- package/src/dashboard/dist/data/convoy-list.json +54 -9
- package/src/dashboard/dist/data/convoys/demo-api-v2.json +177 -0
- package/src/dashboard/dist/data/convoys/demo-auth-revamp.json +239 -0
- package/src/dashboard/dist/data/convoys/demo-dashboard-ui.json +328 -0
- package/src/dashboard/dist/data/convoys/demo-data-pipeline.json +187 -0
- package/src/dashboard/dist/data/convoys/demo-deploy-ci.json +153 -0
- package/src/dashboard/dist/data/convoys/demo-docs-update.json +154 -0
- package/src/dashboard/dist/data/convoys/demo-perf-opt.json +227 -0
- package/src/dashboard/dist/data/events.ndjson +115 -0
- package/src/dashboard/dist/data/overall-stats.json +56 -13
- package/src/dashboard/dist/data/pipelines.ndjson +5285 -0
- package/src/dashboard/dist/index.html +165 -1392
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/public/data/convoy-list.json +54 -9
- package/src/dashboard/public/data/convoys/demo-api-v2.json +177 -0
- package/src/dashboard/public/data/convoys/demo-auth-revamp.json +239 -0
- package/src/dashboard/public/data/convoys/demo-dashboard-ui.json +328 -0
- package/src/dashboard/public/data/convoys/demo-data-pipeline.json +187 -0
- package/src/dashboard/public/data/convoys/demo-deploy-ci.json +153 -0
- package/src/dashboard/public/data/convoys/demo-docs-update.json +154 -0
- package/src/dashboard/public/data/convoys/demo-perf-opt.json +227 -0
- package/src/dashboard/public/data/events.ndjson +115 -0
- package/src/dashboard/public/data/overall-stats.json +56 -13
- package/src/dashboard/public/data/pipelines.ndjson +5285 -0
- package/src/dashboard/scripts/etl.test.ts +4 -62
- package/src/dashboard/scripts/etl.ts +11 -10
- package/src/dashboard/scripts/generate-demo-db.ts +482 -115
- package/src/dashboard/src/pages/index.astro +235 -1638
- package/src/dashboard/src/styles/dashboard.css +473 -7
- package/src/orchestrator/prompts/brainstorm.prompt.md +1 -0
- package/src/orchestrator/prompts/fix-convoy.prompt.md +79 -0
- package/src/orchestrator/prompts/generate-convoy.prompt.md +60 -58
- package/src/orchestrator/prompts/generate-prd.prompt.md +126 -0
- package/src/orchestrator/prompts/validate-convoy.prompt.md +89 -0
- package/src/orchestrator/prompts/validate-prd.prompt.md +83 -0
- package/dist/cli/convoy/log-merge.test.d.ts +0 -2
- package/dist/cli/convoy/log-merge.test.d.ts.map +0 -1
- package/dist/cli/convoy/log-merge.test.js +0 -147
- package/dist/cli/convoy/log-merge.test.js.map +0 -1
- package/src/cli/convoy/log-merge.test.ts +0 -179
- package/src/dashboard/dist/_astro/index.6L3_HsPT.css +0 -1
|
@@ -1,228 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
5
|
-
import { exportConvoyToNdjson } from './export.js'
|
|
6
|
-
import { createConvoyStore } from './store.js'
|
|
7
|
-
import type { ConvoyStore } from './store.js'
|
|
8
|
-
import type { ConvoyTaskStatus } from './types.js'
|
|
1
|
+
// export.test.ts — exportConvoyToNdjson and exportPipelineToNdjson have been
|
|
2
|
+
// removed from export.ts (see that file for rationale). No tests needed.
|
|
9
3
|
|
|
10
|
-
|
|
11
|
-
appendEvent: vi.fn().mockResolvedValue(undefined),
|
|
12
|
-
}))
|
|
4
|
+
import { describe, it } from 'vitest'
|
|
13
5
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
function insertConvoy(id: string, name = 'Test Convoy') {
|
|
20
|
-
store.insertConvoy({
|
|
21
|
-
id,
|
|
22
|
-
name,
|
|
23
|
-
spec_hash: 'abc123',
|
|
24
|
-
status: 'done',
|
|
25
|
-
branch: 'main',
|
|
26
|
-
created_at: NOW,
|
|
27
|
-
spec_yaml: 'name: test',
|
|
28
|
-
})
|
|
29
|
-
store.updateConvoyStatus(id, 'done', { started_at: NOW, finished_at: NOW })
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function insertTask(
|
|
33
|
-
taskId: string,
|
|
34
|
-
convoyId: string,
|
|
35
|
-
phase = 1,
|
|
36
|
-
status: ConvoyTaskStatus = 'done',
|
|
37
|
-
) {
|
|
38
|
-
store.insertTask({
|
|
39
|
-
id: taskId,
|
|
40
|
-
convoy_id: convoyId,
|
|
41
|
-
phase,
|
|
42
|
-
prompt: 'Do something',
|
|
43
|
-
agent: 'developer',
|
|
44
|
-
adapter: 'claude-code',
|
|
45
|
-
model: null,
|
|
46
|
-
timeout_ms: 1_800_000,
|
|
47
|
-
status,
|
|
48
|
-
retries: 0,
|
|
49
|
-
max_retries: 1,
|
|
50
|
-
files: null,
|
|
51
|
-
depends_on: null,
|
|
52
|
-
gates: null,
|
|
53
|
-
})
|
|
54
|
-
if (status !== 'pending') {
|
|
55
|
-
store.updateTaskStatus(taskId, convoyId, status, {
|
|
56
|
-
started_at: NOW,
|
|
57
|
-
finished_at: NOW,
|
|
58
|
-
retries: 0,
|
|
59
|
-
})
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function insertEvent(convoyId: string) {
|
|
64
|
-
store.insertEvent({
|
|
65
|
-
convoy_id: convoyId,
|
|
66
|
-
task_id: null,
|
|
67
|
-
worker_id: null,
|
|
68
|
-
type: 'convoy.started',
|
|
69
|
-
data: null,
|
|
70
|
-
created_at: NOW,
|
|
71
|
-
})
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
beforeEach(() => {
|
|
75
|
-
tmpDir = realpathSync(mkdtempSync(join(tmpdir(), 'convoy-export-test-')))
|
|
76
|
-
store = createConvoyStore(join(tmpDir, 'convoy.db'))
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
afterEach(() => {
|
|
80
|
-
store.close()
|
|
81
|
-
rmSync(tmpDir, { recursive: true, force: true })
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
describe('exportConvoyToNdjson', () => {
|
|
85
|
-
it('creates convoys.ndjson with valid NDJSON', async () => {
|
|
86
|
-
insertConvoy('c1')
|
|
87
|
-
insertTask('t1', 'c1')
|
|
88
|
-
const logsDir = join(tmpDir, 'logs')
|
|
89
|
-
|
|
90
|
-
await exportConvoyToNdjson(store, 'c1', logsDir)
|
|
91
|
-
|
|
92
|
-
const outFile = join(logsDir, 'convoys.ndjson')
|
|
93
|
-
expect(existsSync(outFile)).toBe(true)
|
|
94
|
-
const line = readFileSync(outFile, 'utf8').trimEnd()
|
|
95
|
-
expect(() => JSON.parse(line)).not.toThrow()
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('appends on multiple exports (2 convoys -> 2 lines)', async () => {
|
|
99
|
-
insertConvoy('c1')
|
|
100
|
-
insertConvoy('c2', 'Second Convoy')
|
|
101
|
-
const logsDir = join(tmpDir, 'logs')
|
|
102
|
-
|
|
103
|
-
await exportConvoyToNdjson(store, 'c1', logsDir)
|
|
104
|
-
await exportConvoyToNdjson(store, 'c2', logsDir)
|
|
105
|
-
|
|
106
|
-
const content = readFileSync(join(logsDir, 'convoys.ndjson'), 'utf8')
|
|
107
|
-
const lines = content.trim().split('\n').filter(Boolean)
|
|
108
|
-
expect(lines).toHaveLength(2)
|
|
109
|
-
expect(JSON.parse(lines[0]).id).toBe('c1')
|
|
110
|
-
expect(JSON.parse(lines[1]).id).toBe('c2')
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('required fields present', async () => {
|
|
114
|
-
insertConvoy('c1')
|
|
115
|
-
insertTask('t1', 'c1', 1, 'done')
|
|
116
|
-
insertTask('t2', 'c1', 1, 'failed')
|
|
117
|
-
insertEvent('c1')
|
|
118
|
-
const logsDir = join(tmpDir, 'logs')
|
|
119
|
-
|
|
120
|
-
await exportConvoyToNdjson(store, 'c1', logsDir)
|
|
121
|
-
|
|
122
|
-
const record = JSON.parse(readFileSync(join(logsDir, 'convoys.ndjson'), 'utf8').trim())
|
|
123
|
-
expect(record.id).toBe('c1')
|
|
124
|
-
expect(record.name).toBe('Test Convoy')
|
|
125
|
-
expect(record.status).toBe('done')
|
|
126
|
-
expect(Array.isArray(record.tasks)).toBe(true)
|
|
127
|
-
expect(record.tasks).toHaveLength(2)
|
|
128
|
-
expect(record.tasks[0]).toMatchObject({
|
|
129
|
-
id: 't1',
|
|
130
|
-
phase: 1,
|
|
131
|
-
agent: 'developer',
|
|
132
|
-
adapter: 'claude-code',
|
|
133
|
-
status: 'done',
|
|
134
|
-
retries: 0,
|
|
135
|
-
})
|
|
136
|
-
expect(record.summary).toMatchObject({ total: 2, done: 1, failed: 1, skipped: 0, timedOut: 0 })
|
|
137
|
-
expect(record.events_count).toBe(1)
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
it('missing convoy -> no error, no file', async () => {
|
|
141
|
-
const logsDir = join(tmpDir, 'logs')
|
|
142
|
-
await expect(exportConvoyToNdjson(store, 'nonexistent', logsDir)).resolves.toBeUndefined()
|
|
143
|
-
expect(existsSync(join(logsDir, 'convoys.ndjson'))).toBe(false)
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
it('creates directory if missing', async () => {
|
|
147
|
-
insertConvoy('c1')
|
|
148
|
-
const logsDir = join(tmpDir, 'deep', 'nested', 'logs')
|
|
149
|
-
|
|
150
|
-
await exportConvoyToNdjson(store, 'c1', logsDir)
|
|
151
|
-
|
|
152
|
-
expect(existsSync(join(logsDir, 'convoys.ndjson'))).toBe(true)
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
it('respects custom logsDir', async () => {
|
|
156
|
-
insertConvoy('c1')
|
|
157
|
-
const customDir = join(tmpDir, 'custom-output')
|
|
158
|
-
|
|
159
|
-
await exportConvoyToNdjson(store, 'c1', customDir)
|
|
160
|
-
|
|
161
|
-
const outFile = join(customDir, 'convoys.ndjson')
|
|
162
|
-
expect(existsSync(outFile)).toBe(true)
|
|
163
|
-
const record = JSON.parse(readFileSync(outFile, 'utf8').trim())
|
|
164
|
-
expect(record.id).toBe('c1')
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
it('never throws on store error — writes warning to stderr', async () => {
|
|
168
|
-
const broken = {
|
|
169
|
-
getConvoy: () => { throw new Error('db exploded') },
|
|
170
|
-
} as unknown as ConvoyStore
|
|
171
|
-
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true)
|
|
172
|
-
|
|
173
|
-
await expect(exportConvoyToNdjson(broken, 'c1', join(tmpDir, 'logs'))).resolves.toBeUndefined()
|
|
174
|
-
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('exportConvoyToNdjson warning'))
|
|
175
|
-
|
|
176
|
-
stderrSpy.mockRestore()
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
it('defaults to .opencastle/logs when logsDir is omitted', async () => {
|
|
180
|
-
insertConvoy('c1')
|
|
181
|
-
const originalCwd = process.cwd()
|
|
182
|
-
process.chdir(tmpDir)
|
|
183
|
-
try {
|
|
184
|
-
await exportConvoyToNdjson(store, 'c1')
|
|
185
|
-
const outFile = join(tmpDir, '.opencastle', 'logs', 'convoys.ndjson')
|
|
186
|
-
expect(existsSync(outFile)).toBe(true)
|
|
187
|
-
} finally {
|
|
188
|
-
process.chdir(originalCwd)
|
|
189
|
-
}
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
it('includes cost fields (prompt_tokens, completion_tokens, total_tokens) per task when present', async () => {
|
|
193
|
-
insertConvoy('c1')
|
|
194
|
-
insertTask('t1', 'c1')
|
|
195
|
-
// Manually set token cost on the task record
|
|
196
|
-
store.updateTaskStatus('t1', 'c1', 'done', {
|
|
197
|
-
prompt_tokens: 200,
|
|
198
|
-
completion_tokens: 100,
|
|
199
|
-
total_tokens: 300,
|
|
200
|
-
})
|
|
201
|
-
// Set convoy-level totals
|
|
202
|
-
store.updateConvoyStatus('c1', 'done', { total_tokens: 300 })
|
|
203
|
-
const logsDir = join(tmpDir, 'logs')
|
|
204
|
-
|
|
205
|
-
await exportConvoyToNdjson(store, 'c1', logsDir)
|
|
206
|
-
|
|
207
|
-
const record = JSON.parse(readFileSync(join(logsDir, 'convoys.ndjson'), 'utf8').trim())
|
|
208
|
-
expect(record.tasks[0].prompt_tokens).toBe(200)
|
|
209
|
-
expect(record.tasks[0].completion_tokens).toBe(100)
|
|
210
|
-
expect(record.tasks[0].total_tokens).toBe(300)
|
|
211
|
-
expect(record.total_tokens).toBe(300)
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
it('cost fields are null when no usage data recorded', async () => {
|
|
215
|
-
insertConvoy('c1')
|
|
216
|
-
insertTask('t1', 'c1')
|
|
217
|
-
const logsDir = join(tmpDir, 'logs')
|
|
218
|
-
|
|
219
|
-
await exportConvoyToNdjson(store, 'c1', logsDir)
|
|
220
|
-
|
|
221
|
-
const record = JSON.parse(readFileSync(join(logsDir, 'convoys.ndjson'), 'utf8').trim())
|
|
222
|
-
expect(record.tasks[0].prompt_tokens).toBeNull()
|
|
223
|
-
expect(record.tasks[0].completion_tokens).toBeNull()
|
|
224
|
-
expect(record.tasks[0].total_tokens).toBeNull()
|
|
225
|
-
expect(record.total_tokens).toBeNull()
|
|
226
|
-
expect(record.total_cost_usd).toBeNull()
|
|
6
|
+
describe('export', () => {
|
|
7
|
+
it('export functions removed — data access goes through SQLite store', () => {
|
|
8
|
+
// The monolithic NDJSON export functions were removed to prevent unbounded
|
|
9
|
+
// file growth. All convoy/pipeline data is in convoy.db (SQLite).
|
|
227
10
|
})
|
|
228
11
|
})
|
package/src/cli/convoy/export.ts
CHANGED
|
@@ -1,106 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const pipeline = store.getPipeline(pipelineId)
|
|
12
|
-
if (!pipeline) return
|
|
13
|
-
|
|
14
|
-
const convoys = store.getConvoysByPipeline(pipelineId)
|
|
15
|
-
|
|
16
|
-
const record = {
|
|
17
|
-
id: pipeline.id,
|
|
18
|
-
name: pipeline.name,
|
|
19
|
-
status: pipeline.status,
|
|
20
|
-
branch: pipeline.branch,
|
|
21
|
-
created_at: pipeline.created_at,
|
|
22
|
-
started_at: pipeline.started_at,
|
|
23
|
-
finished_at: pipeline.finished_at,
|
|
24
|
-
total_tokens: pipeline.total_tokens,
|
|
25
|
-
total_cost_usd: pipeline.total_cost_usd,
|
|
26
|
-
convoy_count: convoys.length,
|
|
27
|
-
convoys: convoys.map(c => ({
|
|
28
|
-
id: c.id,
|
|
29
|
-
name: c.name,
|
|
30
|
-
status: c.status,
|
|
31
|
-
started_at: c.started_at,
|
|
32
|
-
finished_at: c.finished_at,
|
|
33
|
-
total_tokens: c.total_tokens,
|
|
34
|
-
})),
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const dir = logsDir ?? resolve(process.cwd(), '.opencastle', 'logs')
|
|
38
|
-
mkdirSync(dir, { recursive: true })
|
|
39
|
-
appendFileSync(resolve(dir, 'pipelines.ndjson'), JSON.stringify(record) + '\n', 'utf8')
|
|
40
|
-
} catch (err) {
|
|
41
|
-
process.stderr.write(`[opencastle] exportPipelineToNdjson warning: ${String(err)}\n`)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function exportConvoyToNdjson(
|
|
46
|
-
store: ConvoyStore,
|
|
47
|
-
convoyId: string,
|
|
48
|
-
logsDir?: string,
|
|
49
|
-
): Promise<void> {
|
|
50
|
-
try {
|
|
51
|
-
const convoy = store.getConvoy(convoyId)
|
|
52
|
-
if (!convoy) return
|
|
53
|
-
|
|
54
|
-
const tasks = store.getTasksByConvoy(convoyId)
|
|
55
|
-
const eventsCount = store.getEvents(convoyId).length
|
|
56
|
-
|
|
57
|
-
const summary = {
|
|
58
|
-
total: tasks.length,
|
|
59
|
-
done: tasks.filter((t) => t.status === 'done').length,
|
|
60
|
-
failed: tasks.filter((t) => t.status === 'failed').length,
|
|
61
|
-
skipped: tasks.filter((t) => t.status === 'skipped').length,
|
|
62
|
-
timedOut: tasks.filter((t) => t.status === 'timed-out').length,
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const durationSec =
|
|
66
|
-
convoy.started_at && convoy.finished_at
|
|
67
|
-
? Math.round(
|
|
68
|
-
(new Date(convoy.finished_at).getTime() - new Date(convoy.started_at).getTime()) / 1_000,
|
|
69
|
-
)
|
|
70
|
-
: undefined
|
|
71
|
-
|
|
72
|
-
const record = {
|
|
73
|
-
id: convoy.id,
|
|
74
|
-
name: convoy.name,
|
|
75
|
-
status: convoy.status,
|
|
76
|
-
branch: convoy.branch,
|
|
77
|
-
created_at: convoy.created_at,
|
|
78
|
-
started_at: convoy.started_at,
|
|
79
|
-
finished_at: convoy.finished_at,
|
|
80
|
-
duration_sec: durationSec,
|
|
81
|
-
summary,
|
|
82
|
-
tasks: tasks.map((t) => ({
|
|
83
|
-
id: t.id,
|
|
84
|
-
phase: t.phase,
|
|
85
|
-
agent: t.agent,
|
|
86
|
-
adapter: t.adapter,
|
|
87
|
-
status: t.status,
|
|
88
|
-
started_at: t.started_at,
|
|
89
|
-
finished_at: t.finished_at,
|
|
90
|
-
retries: t.retries,
|
|
91
|
-
prompt_tokens: t.prompt_tokens,
|
|
92
|
-
completion_tokens: t.completion_tokens,
|
|
93
|
-
total_tokens: t.total_tokens,
|
|
94
|
-
})),
|
|
95
|
-
events_count: eventsCount,
|
|
96
|
-
total_tokens: convoy.total_tokens,
|
|
97
|
-
total_cost_usd: convoy.total_cost_usd,
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const dir = logsDir ?? resolve(process.cwd(), '.opencastle', 'logs')
|
|
101
|
-
mkdirSync(dir, { recursive: true })
|
|
102
|
-
appendFileSync(resolve(dir, 'convoys.ndjson'), JSON.stringify(record) + '\n', 'utf8')
|
|
103
|
-
} catch (err) {
|
|
104
|
-
process.stderr.write(`[opencastle] exportConvoyToNdjson warning: ${String(err)}\n`)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
1
|
+
// Convoy data exports are handled by the dashboard ETL (src/dashboard/scripts/etl.ts)
|
|
2
|
+
// which reads directly from the SQLite convoy store.
|
|
3
|
+
//
|
|
4
|
+
// Per-convoy event logs are in .opencastle/logs/convoys/{convoy-id}.ndjson
|
|
5
|
+
// (written by the event emitter in events.ts, one file per convoy run).
|
|
6
|
+
//
|
|
7
|
+
// export functions removed to prevent unbounded NDJSON growth.
|
|
8
|
+
// All convoy and pipeline data is queryable from the SQLite store at .opencastle/convoy.db.
|
|
9
|
+
|
|
10
|
+
export {}
|
|
@@ -8,14 +8,15 @@ vi.mock('./gates.js', () => ({
|
|
|
8
8
|
scanForSecrets: vi.fn(() => ({ clean: true, findings: [] })),
|
|
9
9
|
}))
|
|
10
10
|
|
|
11
|
-
const DISCOVERED_REL = 'DISCOVERED-ISSUES.md'
|
|
12
|
-
const KNOWN_REL = 'KNOWN-ISSUES.md'
|
|
11
|
+
const DISCOVERED_REL = '.opencastle/DISCOVERED-ISSUES.md'
|
|
12
|
+
const KNOWN_REL = '.opencastle/KNOWN-ISSUES.md'
|
|
13
13
|
|
|
14
14
|
const DISCOVERED_HEADER = '# Discovered Issues\n\n'
|
|
15
15
|
const KNOWN_HEADER = '# Known Issues\n\n'
|
|
16
16
|
|
|
17
17
|
function makeBase(): string {
|
|
18
18
|
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'issues-test-')))
|
|
19
|
+
mkdirSync(join(dir, '.opencastle'), { recursive: true })
|
|
19
20
|
return dir
|
|
20
21
|
}
|
|
21
22
|
|
package/src/cli/convoy/issues.ts
CHANGED
|
@@ -3,12 +3,12 @@ import { join } from 'node:path'
|
|
|
3
3
|
import { scanForSecrets } from './gates.js'
|
|
4
4
|
import type { ConvoyEventEmitter } from './events.js'
|
|
5
5
|
|
|
6
|
-
const DISCOVERED_PATH = 'DISCOVERED-ISSUES.md'
|
|
7
|
-
const KNOWN_PATH = 'KNOWN-ISSUES.md'
|
|
6
|
+
const DISCOVERED_PATH = '.opencastle/DISCOVERED-ISSUES.md'
|
|
7
|
+
const KNOWN_PATH = '.opencastle/KNOWN-ISSUES.md'
|
|
8
8
|
|
|
9
9
|
const INJECT_INSTRUCTION =
|
|
10
10
|
'IMPORTANT: After completing your task, if you notice any pre-existing bugs or issues ' +
|
|
11
|
-
'unrelated to your task, append them to DISCOVERED-ISSUES.md in the format:\n\n' +
|
|
11
|
+
'unrelated to your task, append them to .opencastle/DISCOVERED-ISSUES.md in the format:\n\n' +
|
|
12
12
|
'### ISSUE: [title]\n' +
|
|
13
13
|
'- **File:** [filepath]\n' +
|
|
14
14
|
'- **Description:** [description]\n' +
|
|
@@ -747,31 +747,6 @@ describe('getCurrentBranch fallback', () => {
|
|
|
747
747
|
})
|
|
748
748
|
|
|
749
749
|
|
|
750
|
-
describe('NDJSON export', () => {
|
|
751
|
-
it('writes pipelines.ndjson to logsDir after run', async () => {
|
|
752
|
-
const factory = makeEngineFactory([makeConvoyResult()])
|
|
753
|
-
const logsDir = join(tmpDir, 'logs')
|
|
754
|
-
|
|
755
|
-
await createPipelineOrchestrator({
|
|
756
|
-
spec: makePipelineSpec({ depends_on_convoy: ['./a.yaml'] }),
|
|
757
|
-
specYaml: 'name: pipeline',
|
|
758
|
-
adapter: makeAdapter(),
|
|
759
|
-
dbPath,
|
|
760
|
-
logsDir,
|
|
761
|
-
_createConvoyEngine: factory,
|
|
762
|
-
}).run()
|
|
763
|
-
|
|
764
|
-
const { existsSync, readFileSync } = await import('node:fs')
|
|
765
|
-
const ndjsonPath = join(logsDir, 'pipelines.ndjson')
|
|
766
|
-
expect(existsSync(ndjsonPath)).toBe(true)
|
|
767
|
-
|
|
768
|
-
const parsed = JSON.parse(readFileSync(ndjsonPath, 'utf8').trim())
|
|
769
|
-
expect(parsed.status).toBe('done')
|
|
770
|
-
expect(typeof parsed.id).toBe('string')
|
|
771
|
-
expect(Array.isArray(parsed.convoys)).toBe(true)
|
|
772
|
-
})
|
|
773
|
-
})
|
|
774
|
-
|
|
775
750
|
// ── 13. Path traversal protection ────────────────────────────────────────────
|
|
776
751
|
|
|
777
752
|
describe('path traversal protection', () => {
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
type ConvoyResult,
|
|
13
13
|
type ConvoyEngineOptions,
|
|
14
14
|
} from './engine.js'
|
|
15
|
-
import { exportPipelineToNdjson } from './export.js'
|
|
16
15
|
import type { PipelineStatus } from './types.js'
|
|
17
16
|
import { formatDuration } from '../run/executor.js'
|
|
18
17
|
|
|
@@ -240,15 +239,6 @@ export function createPipelineOrchestrator(
|
|
|
240
239
|
updateStore.close()
|
|
241
240
|
}
|
|
242
241
|
|
|
243
|
-
try {
|
|
244
|
-
const exportStore = createConvoyStore(dbPath)
|
|
245
|
-
try {
|
|
246
|
-
await exportPipelineToNdjson(exportStore, pipelineId, options.logsDir)
|
|
247
|
-
} finally {
|
|
248
|
-
exportStore.close()
|
|
249
|
-
}
|
|
250
|
-
} catch { /* silent */ }
|
|
251
|
-
|
|
252
242
|
return {
|
|
253
243
|
pipelineId,
|
|
254
244
|
status: finalStatus,
|
|
@@ -407,15 +397,6 @@ export function createPipelineOrchestrator(
|
|
|
407
397
|
updateStore.close()
|
|
408
398
|
}
|
|
409
399
|
|
|
410
|
-
try {
|
|
411
|
-
const exportStore = createConvoyStore(dbPath)
|
|
412
|
-
try {
|
|
413
|
-
await exportPipelineToNdjson(exportStore, pipelineId, options.logsDir)
|
|
414
|
-
} finally {
|
|
415
|
-
exportStore.close()
|
|
416
|
-
}
|
|
417
|
-
} catch { /* silent */ }
|
|
418
|
-
|
|
419
400
|
return {
|
|
420
401
|
pipelineId,
|
|
421
402
|
status: finalStatus,
|
package/src/cli/dashboard.ts
CHANGED
|
@@ -20,7 +20,6 @@ const MIME_TYPES: Record<string, string> = {
|
|
|
20
20
|
|
|
21
21
|
const DATA_FILES = [
|
|
22
22
|
'events.ndjson',
|
|
23
|
-
'convoys.ndjson',
|
|
24
23
|
'pipelines.ndjson',
|
|
25
24
|
// Legacy individual files — kept for backwards compatibility
|
|
26
25
|
'sessions.ndjson',
|
|
@@ -35,6 +34,7 @@ interface DashboardArgs {
|
|
|
35
34
|
openBrowser: boolean
|
|
36
35
|
seed: boolean
|
|
37
36
|
convoyId?: string
|
|
37
|
+
help: boolean
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export interface DashboardServerOptions {
|
|
@@ -51,14 +51,30 @@ export interface DashboardServerResult {
|
|
|
51
51
|
url: string
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
const DASHBOARD_HELP = `
|
|
55
|
+
opencastle dashboard [options]
|
|
56
|
+
|
|
57
|
+
Start the observability dashboard server.
|
|
58
|
+
|
|
59
|
+
Options:
|
|
60
|
+
--port <number> Port to listen on (default: 4300, auto-increments if busy)
|
|
61
|
+
--no-open Don't auto-open the browser
|
|
62
|
+
--seed Show demo data instead of project logs
|
|
63
|
+
--convoy <id> Filter dashboard to a specific convoy
|
|
64
|
+
--help, -h Show this help
|
|
65
|
+
`
|
|
66
|
+
|
|
54
67
|
function parseArgs(args: string[]): DashboardArgs {
|
|
55
68
|
let port = 4300
|
|
56
69
|
let openBrowser = true
|
|
57
70
|
let seed = false
|
|
58
71
|
let convoyId: string | undefined
|
|
72
|
+
let help = false
|
|
59
73
|
|
|
60
74
|
for (let i = 0; i < args.length; i++) {
|
|
61
|
-
if (args[i] === '--
|
|
75
|
+
if (args[i] === '--help' || args[i] === '-h') {
|
|
76
|
+
help = true
|
|
77
|
+
} else if (args[i] === '--port' && args[i + 1]) {
|
|
62
78
|
port = parseInt(args[i + 1], 10)
|
|
63
79
|
i++
|
|
64
80
|
} else if (args[i] === '--no-open') {
|
|
@@ -71,7 +87,7 @@ function parseArgs(args: string[]): DashboardArgs {
|
|
|
71
87
|
}
|
|
72
88
|
}
|
|
73
89
|
|
|
74
|
-
return { port, openBrowser, seed, convoyId }
|
|
90
|
+
return { port, openBrowser, seed, convoyId, help }
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
function openUrl(url: string): void {
|
|
@@ -234,19 +250,28 @@ export default async function dashboard({
|
|
|
234
250
|
pkgRoot,
|
|
235
251
|
args,
|
|
236
252
|
}: CliContext): Promise<void> {
|
|
237
|
-
const { port, openBrowser, seed, convoyId } = parseArgs(args)
|
|
253
|
+
const { port, openBrowser, seed, convoyId, help } = parseArgs(args)
|
|
254
|
+
|
|
255
|
+
if (help) {
|
|
256
|
+
console.log(DASHBOARD_HELP)
|
|
257
|
+
return
|
|
258
|
+
}
|
|
238
259
|
|
|
239
260
|
// Check if any log files exist (for messaging)
|
|
240
261
|
let hasLogs = false
|
|
241
262
|
if (!seed) {
|
|
242
263
|
const projectRoot = process.cwd()
|
|
264
|
+
const convoyLogsDir2 = resolve(projectRoot, '.opencastle', 'logs')
|
|
243
265
|
const logsDir = resolve(projectRoot, '.github', 'customizations', 'logs')
|
|
244
266
|
const checkFiles = ['events.ndjson', ...DATA_FILES]
|
|
245
|
-
for (const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
267
|
+
for (const dir of [convoyLogsDir2, logsDir]) {
|
|
268
|
+
for (const f of checkFiles) {
|
|
269
|
+
if (await fileExists(join(dir, f))) {
|
|
270
|
+
hasLogs = true
|
|
271
|
+
break
|
|
272
|
+
}
|
|
249
273
|
}
|
|
274
|
+
if (hasLogs) break
|
|
250
275
|
}
|
|
251
276
|
}
|
|
252
277
|
|
package/src/cli/destroy.ts
CHANGED
|
@@ -7,10 +7,25 @@ import { removeGitignoreBlock } from './gitignore.js'
|
|
|
7
7
|
import { confirm, closePrompts, c } from './prompt.js'
|
|
8
8
|
import type { CliContext } from './types.js'
|
|
9
9
|
|
|
10
|
+
const DESTROY_HELP = `
|
|
11
|
+
opencastle destroy [options]
|
|
12
|
+
|
|
13
|
+
Remove ALL OpenCastle files from your project (reverse of init).
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--dry-run Preview what would be removed without deleting files
|
|
17
|
+
--help, -h Show this help
|
|
18
|
+
`
|
|
19
|
+
|
|
10
20
|
export default async function destroy({
|
|
11
21
|
pkgRoot: _pkgRoot,
|
|
12
22
|
args,
|
|
13
23
|
}: CliContext): Promise<void> {
|
|
24
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
25
|
+
console.log(DESTROY_HELP)
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
const projectRoot = process.cwd()
|
|
15
30
|
const dryRun = args.includes('--dry-run') || args.includes('--dryRun')
|
|
16
31
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CliContext } from './types.js'
|
|
2
|
+
|
|
3
|
+
const HELP = `
|
|
4
|
+
opencastle dispute [options]
|
|
5
|
+
|
|
6
|
+
Manage convoy dispute resolution — view, create, and resolve disputes
|
|
7
|
+
that arise when panel reviews repeatedly block a task.
|
|
8
|
+
|
|
9
|
+
Subcommands:
|
|
10
|
+
list List all disputes
|
|
11
|
+
show <id> Show dispute details
|
|
12
|
+
resolve <id> Mark a dispute as resolved
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--convoy <id> Filter by convoy ID
|
|
16
|
+
--help, -h Show this help
|
|
17
|
+
`
|
|
18
|
+
|
|
19
|
+
export default async function dispute({ args }: CliContext): Promise<void> {
|
|
20
|
+
if (args.includes('--help') || args.includes('-h') || args.length === 0) {
|
|
21
|
+
console.log(HELP)
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.error(' ✗ Dispute management is not yet implemented.')
|
|
26
|
+
console.log(HELP)
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
package/src/cli/doctor.ts
CHANGED
|
@@ -159,7 +159,22 @@ export function checkMcpFromPaths(projectRoot: string, mcpPaths: string[]): Chec
|
|
|
159
159
|
|
|
160
160
|
// ── Main doctor command ───────────────────────────────────────
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
const DOCTOR_HELP = `
|
|
163
|
+
opencastle doctor [options]
|
|
164
|
+
|
|
165
|
+
Validate your OpenCastle setup — checks manifest, customizations, skills,
|
|
166
|
+
logs, MCP configuration, and IDE-specific rules.
|
|
167
|
+
|
|
168
|
+
Options:
|
|
169
|
+
--help, -h Show this help
|
|
170
|
+
`
|
|
171
|
+
|
|
172
|
+
export default async function doctor({ args }: CliContext): Promise<void> {
|
|
173
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
174
|
+
console.log(DOCTOR_HELP)
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
163
178
|
const projectRoot = process.cwd();
|
|
164
179
|
|
|
165
180
|
console.log(`\n 🏰 ${BOLD('OpenCastle Doctor')}\n`);
|
package/src/cli/eject.ts
CHANGED
|
@@ -4,10 +4,26 @@ import { readManifest } from './manifest.js'
|
|
|
4
4
|
import { confirm, closePrompts } from './prompt.js'
|
|
5
5
|
import type { CliContext } from './types.js'
|
|
6
6
|
|
|
7
|
+
const EJECT_HELP = `
|
|
8
|
+
opencastle eject [options]
|
|
9
|
+
|
|
10
|
+
Remove the OpenCastle dependency while keeping all framework files
|
|
11
|
+
standalone in your project.
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--dry-run Preview what would be changed without writing files
|
|
15
|
+
--help, -h Show this help
|
|
16
|
+
`
|
|
17
|
+
|
|
7
18
|
export default async function eject({
|
|
8
19
|
pkgRoot: _pkgRoot,
|
|
9
20
|
args,
|
|
10
21
|
}: CliContext): Promise<void> {
|
|
22
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
23
|
+
console.log(EJECT_HELP)
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
11
27
|
const projectRoot = process.cwd()
|
|
12
28
|
const dryRun = args.includes('--dry-run') || args.includes('--dryRun')
|
|
13
29
|
|
package/src/cli/init.ts
CHANGED
|
@@ -13,7 +13,23 @@ import { IDE_LABELS } from './types.js'
|
|
|
13
13
|
import type { CliContext, IdeChoice, TechTool, TeamTool, StackConfig } from './types.js'
|
|
14
14
|
import { bootstrapCustomizations } from './bootstrap.js'
|
|
15
15
|
|
|
16
|
+
const INIT_HELP = `
|
|
17
|
+
opencastle init [options]
|
|
18
|
+
|
|
19
|
+
Set up OpenCastle in your project — copies framework files, configures
|
|
20
|
+
IDE adapters, and creates the .opencastle/ directory.
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--dry-run Preview what would be changed without writing files
|
|
24
|
+
--help, -h Show this help
|
|
25
|
+
`
|
|
26
|
+
|
|
16
27
|
export default async function init({ pkgRoot, args }: CliContext): Promise<void> {
|
|
28
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
29
|
+
console.log(INIT_HELP)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
17
33
|
const projectRoot = process.cwd()
|
|
18
34
|
const dryRun = args.includes('--dry-run') || args.includes('--dryRun')
|
|
19
35
|
|