opencastle 0.33.5 → 0.33.7
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/dist/cli/convoy/engine.d.ts +5 -0
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +85 -16
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +10 -12
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/pipeline.d.ts +3 -0
- package/dist/cli/convoy/pipeline.d.ts.map +1 -1
- package/dist/cli/convoy/pipeline.js +88 -18
- package/dist/cli/convoy/pipeline.js.map +1 -1
- package/dist/cli/convoy/store.d.ts.map +1 -1
- package/dist/cli/convoy/store.js +74 -3
- package/dist/cli/convoy/store.js.map +1 -1
- package/dist/cli/convoy/store.test.js +26 -14
- package/dist/cli/convoy/store.test.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +1 -123
- package/dist/cli/run.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/convoy/engine.test.ts +10 -12
- package/src/cli/convoy/engine.ts +84 -16
- package/src/cli/convoy/pipeline.ts +81 -19
- package/src/cli/convoy/store.test.ts +28 -15
- package/src/cli/convoy/store.ts +74 -3
- package/src/cli/run.ts +0 -118
- package/src/dashboard/dist/data/convoys/demo-api-v2.json +3 -3
- package/src/dashboard/dist/data/convoys/demo-auth-revamp.json +10 -10
- package/src/dashboard/dist/data/convoys/demo-dashboard-ui.json +18 -18
- package/src/dashboard/dist/data/convoys/demo-data-pipeline.json +3 -3
- package/src/dashboard/dist/data/convoys/demo-deploy-ci.json +1 -1
- package/src/dashboard/dist/data/convoys/demo-docs-update.json +3 -3
- package/src/dashboard/dist/data/convoys/demo-perf-opt.json +4 -4
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/public/data/convoys/demo-api-v2.json +3 -3
- package/src/dashboard/public/data/convoys/demo-auth-revamp.json +10 -10
- package/src/dashboard/public/data/convoys/demo-dashboard-ui.json +18 -18
- package/src/dashboard/public/data/convoys/demo-data-pipeline.json +3 -3
- package/src/dashboard/public/data/convoys/demo-deploy-ci.json +1 -1
- package/src/dashboard/public/data/convoys/demo-docs-update.json +3 -3
- package/src/dashboard/public/data/convoys/demo-perf-opt.json +4 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
2
|
import { mkdirSync } from 'node:fs'
|
|
3
|
-
import { resolve, dirname, relative, isAbsolute, sep } from 'node:path'
|
|
3
|
+
import { resolve, join, dirname, relative, isAbsolute, sep } from 'node:path'
|
|
4
4
|
import { execFile as execFileCb } from 'node:child_process'
|
|
5
5
|
import { promisify } from 'node:util'
|
|
6
6
|
import type { TaskSpec, AgentAdapter } from '../types.js'
|
|
@@ -8,7 +8,6 @@ import { parseTaskSpecText } from '../run/schema.js'
|
|
|
8
8
|
import { createConvoyStore } from './store.js'
|
|
9
9
|
import {
|
|
10
10
|
createConvoyEngine,
|
|
11
|
-
ensureBranch,
|
|
12
11
|
type ConvoyEngine,
|
|
13
12
|
type ConvoyResult,
|
|
14
13
|
type ConvoyEngineOptions,
|
|
@@ -51,6 +50,9 @@ export interface PipelineOrchestratorOptions {
|
|
|
51
50
|
_createConvoyEngine?: (opts: ConvoyEngineOptions) => ConvoyEngine
|
|
52
51
|
/** Injectable branch handler (used in tests). */
|
|
53
52
|
_ensureBranch?: (branchName: string, basePath: string) => Promise<void>
|
|
53
|
+
/** Pass `null` to skip pipeline-level worktree creation (test mode).
|
|
54
|
+
* Also skipped when `_ensureBranch` is provided (backward-compat for tests). */
|
|
55
|
+
_convoyWorktreeDir?: string | null
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
@@ -87,7 +89,6 @@ export function createPipelineOrchestrator(
|
|
|
87
89
|
const basePath = resolve(options.basePath ?? process.cwd())
|
|
88
90
|
const dbPath = options.dbPath ?? resolve(basePath, '.opencastle', 'convoy.db')
|
|
89
91
|
const engineFactory = options._createConvoyEngine ?? createConvoyEngine
|
|
90
|
-
const branchFn = options._ensureBranch ?? ensureBranch
|
|
91
92
|
|
|
92
93
|
async function getCurrentBranch(): Promise<string> {
|
|
93
94
|
try {
|
|
@@ -118,7 +119,7 @@ export function createPipelineOrchestrator(
|
|
|
118
119
|
specPath: string,
|
|
119
120
|
pipelineId: string,
|
|
120
121
|
branch: string,
|
|
121
|
-
|
|
122
|
+
effectiveBase: string = basePath,
|
|
122
123
|
): Promise<ConvoyResult> {
|
|
123
124
|
const absPath = resolveSpecPath(specPath)
|
|
124
125
|
const convoyYaml = await readFile(absPath, 'utf8')
|
|
@@ -129,16 +130,13 @@ export function createPipelineOrchestrator(
|
|
|
129
130
|
spec: overriddenSpec,
|
|
130
131
|
specYaml: convoyYaml,
|
|
131
132
|
adapter,
|
|
132
|
-
basePath,
|
|
133
|
+
basePath: effectiveBase,
|
|
133
134
|
dbPath,
|
|
134
135
|
logsDir: options.logsDir,
|
|
135
136
|
verbose,
|
|
136
137
|
pipelineId,
|
|
138
|
+
_convoyWorktreeDir: null,
|
|
137
139
|
}
|
|
138
|
-
if (skipDirtyCheck) {
|
|
139
|
-
engineOpts._ensureBranch = (b, base) => ensureBranch(b, base, true)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
140
|
const engine = engineFactory(engineOpts)
|
|
143
141
|
return engine.run()
|
|
144
142
|
}
|
|
@@ -149,10 +147,33 @@ export function createPipelineOrchestrator(
|
|
|
149
147
|
const branch = spec.branch ?? (await getCurrentBranch())
|
|
150
148
|
const convoySpecs = spec.depends_on_convoy ?? []
|
|
151
149
|
|
|
152
|
-
//
|
|
153
|
-
//
|
|
150
|
+
// Create pipeline-level worktree for branch isolation.
|
|
151
|
+
// Skipped when _convoyWorktreeDir is null or _ensureBranch is injected (test mode).
|
|
152
|
+
let effectiveBasePath = basePath
|
|
153
|
+
let convoyWorktreeDir: string | undefined
|
|
154
154
|
if (spec.branch !== undefined) {
|
|
155
|
-
|
|
155
|
+
const skipWorktree = options._convoyWorktreeDir === null || options._ensureBranch !== undefined
|
|
156
|
+
if (!skipWorktree) {
|
|
157
|
+
if (typeof options._convoyWorktreeDir === 'string') {
|
|
158
|
+
effectiveBasePath = options._convoyWorktreeDir
|
|
159
|
+
convoyWorktreeDir = options._convoyWorktreeDir
|
|
160
|
+
} else {
|
|
161
|
+
const worktreeId = `convoy-root-${Date.now()}`
|
|
162
|
+
convoyWorktreeDir = join(basePath, '.opencastle', 'worktrees', worktreeId)
|
|
163
|
+
mkdirSync(dirname(convoyWorktreeDir), { recursive: true })
|
|
164
|
+
let branchExists = false
|
|
165
|
+
try {
|
|
166
|
+
await execFile('git', ['rev-parse', '--verify', spec.branch], { cwd: basePath })
|
|
167
|
+
branchExists = true
|
|
168
|
+
} catch { /* branch doesn't exist */ }
|
|
169
|
+
if (branchExists) {
|
|
170
|
+
await execFile('git', ['worktree', 'add', convoyWorktreeDir, spec.branch], { cwd: basePath })
|
|
171
|
+
} else {
|
|
172
|
+
await execFile('git', ['worktree', 'add', '-b', spec.branch, convoyWorktreeDir], { cwd: basePath })
|
|
173
|
+
}
|
|
174
|
+
effectiveBasePath = convoyWorktreeDir
|
|
175
|
+
}
|
|
176
|
+
}
|
|
156
177
|
}
|
|
157
178
|
|
|
158
179
|
mkdirSync(dirname(dbPath), { recursive: true })
|
|
@@ -187,10 +208,7 @@ export function createPipelineOrchestrator(
|
|
|
187
208
|
|
|
188
209
|
let convoyResult: ConvoyResult
|
|
189
210
|
try {
|
|
190
|
-
|
|
191
|
-
// handled the stash prompt, and insertPipeline() writes to convoy.db
|
|
192
|
-
// before the first convoy runs (which would cause a false dirty-check failure).
|
|
193
|
-
convoyResult = await runConvoySpecFile(specPath, pipelineId, branch, true)
|
|
211
|
+
convoyResult = await runConvoySpecFile(specPath, pipelineId, branch, effectiveBasePath)
|
|
194
212
|
} catch (err) {
|
|
195
213
|
process.stderr.write(
|
|
196
214
|
` ✗ Convoy spec "${specPath}" failed to load: ${(err as Error).message}\n`,
|
|
@@ -218,11 +236,12 @@ export function createPipelineOrchestrator(
|
|
|
218
236
|
spec: { ...spec, branch },
|
|
219
237
|
specYaml,
|
|
220
238
|
adapter,
|
|
221
|
-
basePath,
|
|
239
|
+
basePath: effectiveBasePath,
|
|
222
240
|
dbPath,
|
|
223
241
|
logsDir: options.logsDir,
|
|
224
242
|
verbose,
|
|
225
243
|
pipelineId,
|
|
244
|
+
_convoyWorktreeDir: null,
|
|
226
245
|
})
|
|
227
246
|
const hybridResult = await hybridEngine.run()
|
|
228
247
|
convoyResults.push(hybridResult)
|
|
@@ -238,6 +257,12 @@ export function createPipelineOrchestrator(
|
|
|
238
257
|
failStore.close()
|
|
239
258
|
}
|
|
240
259
|
throw err
|
|
260
|
+
} finally {
|
|
261
|
+
if (convoyWorktreeDir) {
|
|
262
|
+
try {
|
|
263
|
+
await execFile('git', ['worktree', 'remove', convoyWorktreeDir, '--force'], { cwd: basePath })
|
|
264
|
+
} catch { /* ignore cleanup errors */ }
|
|
265
|
+
}
|
|
241
266
|
}
|
|
242
267
|
|
|
243
268
|
const totalTokens = aggregateTokens(convoyResults)
|
|
@@ -283,6 +308,36 @@ export function createPipelineOrchestrator(
|
|
|
283
308
|
const convoySpecs: string[] = JSON.parse(pipeline.convoy_specs) as string[]
|
|
284
309
|
const branch = pipeline.branch ?? spec.branch ?? (await getCurrentBranch())
|
|
285
310
|
|
|
311
|
+
// Create pipeline-level worktree for branch isolation.
|
|
312
|
+
// Skipped when _convoyWorktreeDir is null or _ensureBranch is injected (test mode).
|
|
313
|
+
let effectiveBasePath = basePath
|
|
314
|
+
let convoyWorktreeDir: string | undefined
|
|
315
|
+
const pipelineBranch = pipeline.branch ?? spec.branch
|
|
316
|
+
if (pipelineBranch !== undefined) {
|
|
317
|
+
const skipWorktree = options._convoyWorktreeDir === null || options._ensureBranch !== undefined
|
|
318
|
+
if (!skipWorktree) {
|
|
319
|
+
if (typeof options._convoyWorktreeDir === 'string') {
|
|
320
|
+
effectiveBasePath = options._convoyWorktreeDir
|
|
321
|
+
convoyWorktreeDir = options._convoyWorktreeDir
|
|
322
|
+
} else {
|
|
323
|
+
const worktreeId = `convoy-root-${Date.now()}`
|
|
324
|
+
convoyWorktreeDir = join(basePath, '.opencastle', 'worktrees', worktreeId)
|
|
325
|
+
mkdirSync(dirname(convoyWorktreeDir), { recursive: true })
|
|
326
|
+
let branchExists = false
|
|
327
|
+
try {
|
|
328
|
+
await execFile('git', ['rev-parse', '--verify', pipelineBranch], { cwd: basePath })
|
|
329
|
+
branchExists = true
|
|
330
|
+
} catch { /* branch doesn't exist */ }
|
|
331
|
+
if (branchExists) {
|
|
332
|
+
await execFile('git', ['worktree', 'add', convoyWorktreeDir, pipelineBranch], { cwd: basePath })
|
|
333
|
+
} else {
|
|
334
|
+
await execFile('git', ['worktree', 'add', '-b', pipelineBranch, convoyWorktreeDir], { cwd: basePath })
|
|
335
|
+
}
|
|
336
|
+
effectiveBasePath = convoyWorktreeDir
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
286
341
|
// Load all convoys linked to this pipeline, sorted by creation time
|
|
287
342
|
const convoyStore = createConvoyStore(dbPath)
|
|
288
343
|
let existingConvoys
|
|
@@ -350,11 +405,12 @@ export function createPipelineOrchestrator(
|
|
|
350
405
|
spec: overriddenSpec,
|
|
351
406
|
specYaml: convoyYaml,
|
|
352
407
|
adapter,
|
|
353
|
-
basePath,
|
|
408
|
+
basePath: effectiveBasePath,
|
|
354
409
|
dbPath,
|
|
355
410
|
logsDir: options.logsDir,
|
|
356
411
|
verbose,
|
|
357
412
|
pipelineId,
|
|
413
|
+
_convoyWorktreeDir: null,
|
|
358
414
|
})
|
|
359
415
|
|
|
360
416
|
if (existing.status === 'failed') {
|
|
@@ -366,7 +422,7 @@ export function createPipelineOrchestrator(
|
|
|
366
422
|
} else {
|
|
367
423
|
// Run fresh
|
|
368
424
|
try {
|
|
369
|
-
convoyResult = await runConvoySpecFile(specPath, pipelineId, branch,
|
|
425
|
+
convoyResult = await runConvoySpecFile(specPath, pipelineId, branch, effectiveBasePath)
|
|
370
426
|
} catch (err) {
|
|
371
427
|
process.stderr.write(
|
|
372
428
|
` ✗ Convoy spec "${specPath}" failed to load: ${(err as Error).message}\n`,
|
|
@@ -399,6 +455,12 @@ export function createPipelineOrchestrator(
|
|
|
399
455
|
failStore.close()
|
|
400
456
|
}
|
|
401
457
|
throw err
|
|
458
|
+
} finally {
|
|
459
|
+
if (convoyWorktreeDir) {
|
|
460
|
+
try {
|
|
461
|
+
await execFile('git', ['worktree', 'remove', convoyWorktreeDir, '--force'], { cwd: basePath })
|
|
462
|
+
} catch { /* ignore cleanup errors */ }
|
|
463
|
+
}
|
|
402
464
|
}
|
|
403
465
|
|
|
404
466
|
const totalTokens = aggregateTokens(convoyResults)
|
|
@@ -99,11 +99,11 @@ describe('DB creation', () => {
|
|
|
99
99
|
expect(row.journal_mode).toBe('wal')
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
-
it('sets schema version to
|
|
102
|
+
it('sets schema version to 12', () => {
|
|
103
103
|
const db = new DatabaseSync(dbPath)
|
|
104
104
|
const row = db.prepare('PRAGMA user_version').get() as { user_version: number }
|
|
105
105
|
db.close()
|
|
106
|
-
expect(row.user_version).toBe(
|
|
106
|
+
expect(row.user_version).toBe(12)
|
|
107
107
|
})
|
|
108
108
|
|
|
109
109
|
it('creates all required tables', () => {
|
|
@@ -131,7 +131,7 @@ describe('DB creation', () => {
|
|
|
131
131
|
store2.close()
|
|
132
132
|
// Reassign so afterEach does not double-close
|
|
133
133
|
store = createConvoyStore(dbPath)
|
|
134
|
-
expect(row.user_version).toBe(
|
|
134
|
+
expect(row.user_version).toBe(12)
|
|
135
135
|
})
|
|
136
136
|
})
|
|
137
137
|
|
|
@@ -208,8 +208,8 @@ describe('schema migration', () => {
|
|
|
208
208
|
verifyDb.close()
|
|
209
209
|
|
|
210
210
|
expect(cols.map(c => c.name)).toContain('adapter')
|
|
211
|
-
// v1 chains through v2→v3→v4→...→v7→v8→v9→v10→v11 in one init, so final version is
|
|
212
|
-
expect(version.user_version).toBe(
|
|
211
|
+
// v1 chains through v2→v3→v4→...→v7→v8→v9→v10→v11→v12 in one init, so final version is 12
|
|
212
|
+
expect(version.user_version).toBe(12)
|
|
213
213
|
})
|
|
214
214
|
|
|
215
215
|
it('schema migration v2 to v3 adds cost columns', () => {
|
|
@@ -295,7 +295,7 @@ describe('schema migration', () => {
|
|
|
295
295
|
expect(convoyColNames).toContain('total_tokens')
|
|
296
296
|
expect(convoyColNames).toContain('total_cost_usd')
|
|
297
297
|
|
|
298
|
-
expect(version.user_version).toBe(
|
|
298
|
+
expect(version.user_version).toBe(12)
|
|
299
299
|
})
|
|
300
300
|
|
|
301
301
|
it('schema migration v1 to v3 chains correctly in a single init', () => {
|
|
@@ -381,7 +381,7 @@ describe('schema migration', () => {
|
|
|
381
381
|
expect(convoyColNames).toContain('total_tokens')
|
|
382
382
|
expect(convoyColNames).toContain('total_cost_usd')
|
|
383
383
|
|
|
384
|
-
expect(version.user_version).toBe(
|
|
384
|
+
expect(version.user_version).toBe(12)
|
|
385
385
|
})
|
|
386
386
|
|
|
387
387
|
it('schema migration v3 to v4 creates pipeline table and adds pipeline_id to convoy', () => {
|
|
@@ -464,7 +464,7 @@ describe('schema migration', () => {
|
|
|
464
464
|
|
|
465
465
|
expect(convoyCols.map(c => c.name)).toContain('pipeline_id')
|
|
466
466
|
expect(tables.map(t => t.name)).toContain('pipeline')
|
|
467
|
-
expect(version.user_version).toBe(
|
|
467
|
+
expect(version.user_version).toBe(12)
|
|
468
468
|
})
|
|
469
469
|
})
|
|
470
470
|
|
|
@@ -580,6 +580,20 @@ describe('task CRUD', () => {
|
|
|
580
580
|
expect(tasks[1].phase).toBe(1)
|
|
581
581
|
})
|
|
582
582
|
|
|
583
|
+
it('allows same task ID in different convoys', () => {
|
|
584
|
+
store.insertConvoy(makeConvoy({ id: 'convoy-a' }))
|
|
585
|
+
store.insertConvoy(makeConvoy({ id: 'convoy-b' }))
|
|
586
|
+
store.insertTask(makeTask({ id: 'shared-task', convoy_id: 'convoy-a' }))
|
|
587
|
+
store.insertTask(makeTask({ id: 'shared-task', convoy_id: 'convoy-b' }))
|
|
588
|
+
|
|
589
|
+
const taskA = store.getTask('shared-task', 'convoy-a')
|
|
590
|
+
const taskB = store.getTask('shared-task', 'convoy-b')
|
|
591
|
+
expect(taskA).toBeDefined()
|
|
592
|
+
expect(taskB).toBeDefined()
|
|
593
|
+
expect(taskA!.convoy_id).toBe('convoy-a')
|
|
594
|
+
expect(taskB!.convoy_id).toBe('convoy-b')
|
|
595
|
+
})
|
|
596
|
+
|
|
583
597
|
it('updates task status', () => {
|
|
584
598
|
store.insertTask(makeTask())
|
|
585
599
|
store.updateTaskStatus('task-1', 'convoy-1', 'running')
|
|
@@ -1444,7 +1458,7 @@ describe('schema migration v5 → v6', () => {
|
|
|
1444
1458
|
v5Verify.close()
|
|
1445
1459
|
migratedStore.close()
|
|
1446
1460
|
|
|
1447
|
-
expect(row.user_version).toBe(
|
|
1461
|
+
expect(row.user_version).toBe(12)
|
|
1448
1462
|
expect(taskStepTable?.name).toBe('task_step')
|
|
1449
1463
|
expect(convoy?.id).toBe('convoy-auto')
|
|
1450
1464
|
expect(task?.id).toBe('task-auto')
|
|
@@ -1614,7 +1628,7 @@ describe('schema migration v6→v7 (drift detection columns)', () => {
|
|
|
1614
1628
|
|
|
1615
1629
|
expect(cols.map(c => c.name)).toContain('drift_score')
|
|
1616
1630
|
expect(cols.map(c => c.name)).toContain('drift_retried')
|
|
1617
|
-
expect(version.user_version).toBe(
|
|
1631
|
+
expect(version.user_version).toBe(12)
|
|
1618
1632
|
})
|
|
1619
1633
|
|
|
1620
1634
|
it('new databases include drift_score and drift_retried in CREATE TABLE', () => {
|
|
@@ -1846,10 +1860,9 @@ describe('migration full chain v4→v10', () => {
|
|
|
1846
1860
|
migratedStore.close()
|
|
1847
1861
|
|
|
1848
1862
|
const verifyDb = new DatabaseSync(chainDbPath)
|
|
1849
|
-
|
|
1850
|
-
// Verify user_version = 11
|
|
1863
|
+
verifyDb.exec('PRAGMA foreign_keys = 0')
|
|
1851
1864
|
const version = (verifyDb.prepare('PRAGMA user_version').get() as { user_version: number }).user_version
|
|
1852
|
-
expect(version).toBe(
|
|
1865
|
+
expect(version).toBe(12)
|
|
1853
1866
|
|
|
1854
1867
|
// Verify all new tables exist
|
|
1855
1868
|
const tables = (verifyDb.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as Array<{ name: string }>).map(t => t.name)
|
|
@@ -1884,7 +1897,7 @@ describe('migration full chain v4→v10', () => {
|
|
|
1884
1897
|
const eventCount = (verifyDb.prepare('SELECT COUNT(*) AS cnt FROM event WHERE convoy_id = :id').get({ id: 'convoy-chain' }) as { cnt: number }).cnt
|
|
1885
1898
|
expect(eventCount).toBe(1)
|
|
1886
1899
|
|
|
1887
|
-
// Verify
|
|
1900
|
+
// Verify task_step table accepts inserts
|
|
1888
1901
|
expect(() => {
|
|
1889
1902
|
verifyDb.prepare(
|
|
1890
1903
|
`INSERT INTO task_step (task_id, step_index, prompt, gates, status)
|
|
@@ -2528,7 +2541,7 @@ describe('v9→v10 migration', () => {
|
|
|
2528
2541
|
// Verify version = 11
|
|
2529
2542
|
const verifyDb = new DatabaseSync(migDb)
|
|
2530
2543
|
const version = (verifyDb.prepare('PRAGMA user_version').get() as { user_version: number }).user_version
|
|
2531
|
-
expect(version).toBe(
|
|
2544
|
+
expect(version).toBe(12)
|
|
2532
2545
|
|
|
2533
2546
|
// Verify new REAL columns exist
|
|
2534
2547
|
const convoyCols = (verifyDb.prepare('PRAGMA table_info(convoy)').all() as Array<{ name: string }>).map(c => c.name)
|
package/src/cli/convoy/store.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type {
|
|
|
17
17
|
TaskStepRecord,
|
|
18
18
|
} from './types.js'
|
|
19
19
|
|
|
20
|
-
const SCHEMA_VERSION =
|
|
20
|
+
const SCHEMA_VERSION = 12
|
|
21
21
|
|
|
22
22
|
// ── Size limits (bytes) ────────────────────────────────────────────────────────
|
|
23
23
|
const LIMIT_SPEC_YAML = 256 * 1024 // 256 KB
|
|
@@ -178,6 +178,7 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
178
178
|
constructor(dbPath: string) {
|
|
179
179
|
this.dbPath = dbPath
|
|
180
180
|
this.db = new DatabaseSync(dbPath)
|
|
181
|
+
this.db.exec('PRAGMA foreign_keys = 0')
|
|
181
182
|
this.db.exec('PRAGMA journal_mode = WAL')
|
|
182
183
|
this.db.exec('PRAGMA synchronous = NORMAL')
|
|
183
184
|
this.initSchema()
|
|
@@ -222,7 +223,7 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
222
223
|
);
|
|
223
224
|
|
|
224
225
|
CREATE TABLE IF NOT EXISTS task (
|
|
225
|
-
id TEXT
|
|
226
|
+
id TEXT NOT NULL,
|
|
226
227
|
convoy_id TEXT NOT NULL REFERENCES convoy(id),
|
|
227
228
|
phase INTEGER NOT NULL,
|
|
228
229
|
prompt TEXT NOT NULL,
|
|
@@ -265,7 +266,8 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
265
266
|
outputs TEXT,
|
|
266
267
|
inputs TEXT,
|
|
267
268
|
discovered_issues TEXT,
|
|
268
|
-
contract_result TEXT
|
|
269
|
+
contract_result TEXT,
|
|
270
|
+
PRIMARY KEY (id, convoy_id)
|
|
269
271
|
);
|
|
270
272
|
|
|
271
273
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_task_idempotency ON task(convoy_id, idempotency_key)
|
|
@@ -424,6 +426,10 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
424
426
|
migrateSchema(this.db, this.dbPath, 10, 11)
|
|
425
427
|
version = 11
|
|
426
428
|
}
|
|
429
|
+
if (version === 11) {
|
|
430
|
+
migrateSchema(this.db, this.dbPath, 11, 12)
|
|
431
|
+
version = 12
|
|
432
|
+
}
|
|
427
433
|
}
|
|
428
434
|
|
|
429
435
|
insertConvoy(
|
|
@@ -1356,6 +1362,71 @@ export function migrateSchema(db: DatabaseSync, dbPath: string, fromVersion: num
|
|
|
1356
1362
|
ALTER TABLE task ADD COLUMN compaction_count INTEGER NOT NULL DEFAULT 0;
|
|
1357
1363
|
`)
|
|
1358
1364
|
}
|
|
1365
|
+
if (v === 11) {
|
|
1366
|
+
db.exec(`
|
|
1367
|
+
CREATE TABLE task_new (
|
|
1368
|
+
id TEXT NOT NULL,
|
|
1369
|
+
convoy_id TEXT NOT NULL REFERENCES convoy(id),
|
|
1370
|
+
phase INTEGER NOT NULL,
|
|
1371
|
+
prompt TEXT NOT NULL,
|
|
1372
|
+
agent TEXT NOT NULL DEFAULT 'developer',
|
|
1373
|
+
adapter TEXT,
|
|
1374
|
+
model TEXT,
|
|
1375
|
+
timeout_ms INTEGER NOT NULL DEFAULT 1800000,
|
|
1376
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
1377
|
+
worker_id TEXT,
|
|
1378
|
+
worktree TEXT,
|
|
1379
|
+
output TEXT,
|
|
1380
|
+
exit_code INTEGER,
|
|
1381
|
+
started_at TEXT,
|
|
1382
|
+
finished_at TEXT,
|
|
1383
|
+
retries INTEGER NOT NULL DEFAULT 0,
|
|
1384
|
+
max_retries INTEGER NOT NULL DEFAULT 1,
|
|
1385
|
+
files TEXT,
|
|
1386
|
+
depends_on TEXT,
|
|
1387
|
+
prompt_tokens INTEGER,
|
|
1388
|
+
completion_tokens INTEGER,
|
|
1389
|
+
total_tokens INTEGER,
|
|
1390
|
+
cost_usd TEXT,
|
|
1391
|
+
cost_usd_num REAL,
|
|
1392
|
+
gates TEXT,
|
|
1393
|
+
on_exhausted TEXT NOT NULL DEFAULT 'dlq',
|
|
1394
|
+
injected INTEGER NOT NULL DEFAULT 0,
|
|
1395
|
+
provenance TEXT,
|
|
1396
|
+
idempotency_key TEXT,
|
|
1397
|
+
current_step INTEGER,
|
|
1398
|
+
total_steps INTEGER,
|
|
1399
|
+
review_level TEXT,
|
|
1400
|
+
review_verdict TEXT,
|
|
1401
|
+
review_tokens INTEGER,
|
|
1402
|
+
review_model TEXT,
|
|
1403
|
+
panel_attempts INTEGER NOT NULL DEFAULT 0,
|
|
1404
|
+
dispute_id TEXT,
|
|
1405
|
+
drift_score REAL,
|
|
1406
|
+
drift_retried INTEGER NOT NULL DEFAULT 0,
|
|
1407
|
+
compaction_count INTEGER NOT NULL DEFAULT 0,
|
|
1408
|
+
outputs TEXT,
|
|
1409
|
+
inputs TEXT,
|
|
1410
|
+
discovered_issues TEXT,
|
|
1411
|
+
contract_result TEXT,
|
|
1412
|
+
PRIMARY KEY (id, convoy_id)
|
|
1413
|
+
);
|
|
1414
|
+
INSERT INTO task_new SELECT
|
|
1415
|
+
id, convoy_id, phase, prompt, agent, adapter, model, timeout_ms,
|
|
1416
|
+
status, worker_id, worktree, output, exit_code, started_at, finished_at,
|
|
1417
|
+
retries, max_retries, files, depends_on, prompt_tokens, completion_tokens,
|
|
1418
|
+
total_tokens, cost_usd, cost_usd_num, gates, on_exhausted, injected,
|
|
1419
|
+
provenance, idempotency_key, current_step, total_steps, review_level,
|
|
1420
|
+
review_verdict, review_tokens, review_model, panel_attempts, dispute_id,
|
|
1421
|
+
drift_score, drift_retried, compaction_count, outputs, inputs,
|
|
1422
|
+
discovered_issues, contract_result
|
|
1423
|
+
FROM task;
|
|
1424
|
+
DROP TABLE task;
|
|
1425
|
+
ALTER TABLE task_new RENAME TO task;
|
|
1426
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_task_idempotency ON task(convoy_id, idempotency_key)
|
|
1427
|
+
WHERE idempotency_key IS NOT NULL;
|
|
1428
|
+
`)
|
|
1429
|
+
}
|
|
1359
1430
|
db.exec('COMMIT')
|
|
1360
1431
|
} catch (err) {
|
|
1361
1432
|
try { db.exec('ROLLBACK') } catch { /* ignore */ }
|
package/src/cli/run.ts
CHANGED
|
@@ -871,54 +871,6 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
871
871
|
if (spec.branch) console.log(` Branch: ${spec.branch}`)
|
|
872
872
|
if (spec.gates?.length) console.log(` Gates: ${spec.gates.length} validation commands`)
|
|
873
873
|
|
|
874
|
-
// ── Pre-flight: handle uncommitted changes before branch switch ──
|
|
875
|
-
let pipelineDidStash = false
|
|
876
|
-
if (spec.branch) {
|
|
877
|
-
const { execFile: execFileCb } = await import('node:child_process')
|
|
878
|
-
const { promisify } = await import('node:util')
|
|
879
|
-
const execFile = promisify(execFileCb)
|
|
880
|
-
let statusOut: string
|
|
881
|
-
try {
|
|
882
|
-
const result = await execFile('git', ['status', '--porcelain'], {
|
|
883
|
-
cwd: process.cwd(),
|
|
884
|
-
})
|
|
885
|
-
statusOut = result.stdout
|
|
886
|
-
} catch {
|
|
887
|
-
console.error(` ✗ Git repository not found. A git repo is required when using the \`branch\` option.`)
|
|
888
|
-
console.error(` Run \`git init\` to initialize a repository.`)
|
|
889
|
-
console.log(`\n ${c.dim('Resume:')} npx opencastle run -f ${opts.file}`)
|
|
890
|
-
process.exit(1)
|
|
891
|
-
}
|
|
892
|
-
// Untracked files (??) don't block branch checkout — ignore them
|
|
893
|
-
const trackedChanges = statusOut
|
|
894
|
-
.split('\n')
|
|
895
|
-
.filter(line => line.trim() && !line.startsWith('??'))
|
|
896
|
-
.join('\n')
|
|
897
|
-
if (trackedChanges) {
|
|
898
|
-
console.log(`\n ${c.yellow('⚠')} Uncommitted changes detected.`)
|
|
899
|
-
const shouldStash = await confirm('Stash changes and continue?', true)
|
|
900
|
-
if (!shouldStash) {
|
|
901
|
-
console.log(' Aborted. Commit or stash your changes manually, then retry.')
|
|
902
|
-
console.log(`\n ${c.dim('Resume:')} npx opencastle run -f ${opts.file}`)
|
|
903
|
-
closePrompts()
|
|
904
|
-
process.exit(1)
|
|
905
|
-
}
|
|
906
|
-
try {
|
|
907
|
-
await execFile('git', ['stash', 'push', '-m', 'opencastle: auto-stash before pipeline'], {
|
|
908
|
-
cwd: process.cwd(),
|
|
909
|
-
})
|
|
910
|
-
pipelineDidStash = true
|
|
911
|
-
console.log(` ${c.green('✓')} Changes stashed.`)
|
|
912
|
-
} catch {
|
|
913
|
-
console.log(` ${c.yellow('⚠')} Could not stash changes automatically.`)
|
|
914
|
-
console.log(` Commit or stash your changes manually, then resume:\n`)
|
|
915
|
-
console.log(` ${c.dim('Resume:')} npx opencastle run -f ${opts.file}`)
|
|
916
|
-
closePrompts()
|
|
917
|
-
process.exit(1)
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
|
|
922
874
|
const { startDashboardServer } = await import('./dashboard.js')
|
|
923
875
|
let pipelineDashboardResult: { server: import('node:http').Server; port: number; url: string } | null = null
|
|
924
876
|
try {
|
|
@@ -953,17 +905,6 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
953
905
|
throw err
|
|
954
906
|
}
|
|
955
907
|
printPipelineResult(pipelineResult)
|
|
956
|
-
if (pipelineDidStash) {
|
|
957
|
-
const { execFile: execFileCb } = await import('node:child_process')
|
|
958
|
-
const { promisify } = await import('node:util')
|
|
959
|
-
const execFile = promisify(execFileCb)
|
|
960
|
-
try {
|
|
961
|
-
await execFile('git', ['stash', 'pop'], { cwd: process.cwd() })
|
|
962
|
-
console.log(` ${c.green('✓')} Stashed changes restored.`)
|
|
963
|
-
} catch {
|
|
964
|
-
console.log(` ${c.yellow('⚠')} Could not restore stash automatically. Run \`git stash pop\` manually.`)
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
908
|
if (pipelineDashboardResult) {
|
|
968
909
|
console.log(`\n ${c.dim('Results saved to .opencastle/convoy.db')}`)
|
|
969
910
|
console.log(` ${c.dim('Dashboard:')} ${pipelineDashboardResult.url}`)
|
|
@@ -996,54 +937,6 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
996
937
|
if (spec.branch) console.log(` Branch: ${spec.branch}`)
|
|
997
938
|
if (spec.gates?.length) console.log(` Gates: ${spec.gates.length} validation commands`)
|
|
998
939
|
|
|
999
|
-
// ── Pre-flight: handle uncommitted changes before branch switch ──
|
|
1000
|
-
let didStash = false
|
|
1001
|
-
if (spec.branch) {
|
|
1002
|
-
const { execFile: execFileCb } = await import('node:child_process')
|
|
1003
|
-
const { promisify } = await import('node:util')
|
|
1004
|
-
const execFile = promisify(execFileCb)
|
|
1005
|
-
let statusOut: string
|
|
1006
|
-
try {
|
|
1007
|
-
const result = await execFile('git', ['status', '--porcelain'], {
|
|
1008
|
-
cwd: process.cwd(),
|
|
1009
|
-
})
|
|
1010
|
-
statusOut = result.stdout
|
|
1011
|
-
} catch {
|
|
1012
|
-
console.error(` ✗ Git repository not found. A git repo is required when using the \`branch\` option.`)
|
|
1013
|
-
console.error(` Run \`git init\` to initialize a repository.`)
|
|
1014
|
-
console.log(`\n ${c.dim('Resume:')} npx opencastle run -f ${opts.file}`)
|
|
1015
|
-
process.exit(1)
|
|
1016
|
-
}
|
|
1017
|
-
// Untracked files (??) don't block branch checkout — ignore them
|
|
1018
|
-
const trackedChanges = statusOut
|
|
1019
|
-
.split('\n')
|
|
1020
|
-
.filter(line => line.trim() && !line.startsWith('??'))
|
|
1021
|
-
.join('\n')
|
|
1022
|
-
if (trackedChanges) {
|
|
1023
|
-
console.log(`\n ${c.yellow('⚠')} Uncommitted changes detected.`)
|
|
1024
|
-
const shouldStash = await confirm('Stash changes and continue?', true)
|
|
1025
|
-
if (!shouldStash) {
|
|
1026
|
-
console.log(' Aborted. Commit or stash your changes manually, then retry.')
|
|
1027
|
-
console.log(`\n ${c.dim('Resume:')} npx opencastle run -f ${opts.file}`)
|
|
1028
|
-
closePrompts()
|
|
1029
|
-
process.exit(1)
|
|
1030
|
-
}
|
|
1031
|
-
try {
|
|
1032
|
-
await execFile('git', ['stash', 'push', '-m', 'opencastle: auto-stash before convoy'], {
|
|
1033
|
-
cwd: process.cwd(),
|
|
1034
|
-
})
|
|
1035
|
-
didStash = true
|
|
1036
|
-
console.log(` ${c.green('✓')} Changes stashed.`)
|
|
1037
|
-
} catch {
|
|
1038
|
-
console.log(` ${c.yellow('⚠')} Could not stash changes automatically.`)
|
|
1039
|
-
console.log(` Commit or stash your changes manually, then resume:\n`)
|
|
1040
|
-
console.log(` ${c.dim('Resume:')} npx opencastle run -f ${opts.file}`)
|
|
1041
|
-
closePrompts()
|
|
1042
|
-
process.exit(1)
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
940
|
const { startDashboardServer } = await import('./dashboard.js')
|
|
1048
941
|
let dashboardResult: { server: import('node:http').Server; port: number; url: string } | null = null
|
|
1049
942
|
try {
|
|
@@ -1095,17 +988,6 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
1095
988
|
throw err
|
|
1096
989
|
}
|
|
1097
990
|
printConvoyResult(result)
|
|
1098
|
-
if (didStash) {
|
|
1099
|
-
const { execFile: execFileCb } = await import('node:child_process')
|
|
1100
|
-
const { promisify } = await import('node:util')
|
|
1101
|
-
const execFile = promisify(execFileCb)
|
|
1102
|
-
try {
|
|
1103
|
-
await execFile('git', ['stash', 'pop'], { cwd: process.cwd() })
|
|
1104
|
-
console.log(` ${c.green('✓')} Stashed changes restored.`)
|
|
1105
|
-
} catch {
|
|
1106
|
-
console.log(` ${c.yellow('⚠')} Could not restore stash automatically. Run \`git stash pop\` manually.`)
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
991
|
if (dashboardResult) {
|
|
1110
992
|
console.log(`\n ${c.dim('Results saved to .opencastle/convoy.db')}`)
|
|
1111
993
|
console.log(` ${c.dim('Dashboard:')} ${dashboardResult.url}`)
|
|
@@ -51,21 +51,21 @@
|
|
|
51
51
|
"name": "docs/api-v2-contract.json",
|
|
52
52
|
"type": "json",
|
|
53
53
|
"task_id": "api-t1",
|
|
54
|
-
"created_at": "2026-04-
|
|
54
|
+
"created_at": "2026-04-07T12:58:13.324Z"
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
"id": "artifact-demo-api-v2-reports-security-gate-failure-md",
|
|
58
58
|
"name": "reports/security-gate-failure.md",
|
|
59
59
|
"type": "summary",
|
|
60
60
|
"task_id": "api-t3",
|
|
61
|
-
"created_at": "2026-04-
|
|
61
|
+
"created_at": "2026-04-07T12:58:13.324Z"
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
"id": "artifact-demo-api-v2-src-api-rate-limiter-ts",
|
|
65
65
|
"name": "src/api/rate-limiter.ts",
|
|
66
66
|
"type": "file",
|
|
67
67
|
"task_id": "api-t2",
|
|
68
|
-
"created_at": "2026-04-
|
|
68
|
+
"created_at": "2026-04-07T12:58:13.324Z"
|
|
69
69
|
}
|
|
70
70
|
],
|
|
71
71
|
"has_more_events": false,
|
|
@@ -42,28 +42,28 @@
|
|
|
42
42
|
"name": "libs/auth/src/jwt-middleware.ts",
|
|
43
43
|
"type": "file",
|
|
44
44
|
"task_id": "auth-t2",
|
|
45
|
-
"created_at": "2026-04-
|
|
45
|
+
"created_at": "2026-04-07T12:58:13.323Z"
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
"id": "artifact-demo-auth-revamp-libs-auth-src-rls-policies-sql",
|
|
49
49
|
"name": "libs/auth/src/rls-policies.sql",
|
|
50
50
|
"type": "file",
|
|
51
51
|
"task_id": "auth-t3",
|
|
52
|
-
"created_at": "2026-04-
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"id": "artifact-demo-auth-revamp-reports-auth-review-summary-md",
|
|
56
|
-
"name": "reports/auth-review-summary.md",
|
|
57
|
-
"type": "summary",
|
|
58
|
-
"task_id": "auth-t5",
|
|
59
|
-
"created_at": "2026-04-06T09:01:29.677Z"
|
|
52
|
+
"created_at": "2026-04-07T12:58:13.323Z"
|
|
60
53
|
},
|
|
61
54
|
{
|
|
62
55
|
"id": "artifact-demo-auth-revamp-tests-auth-integration-test-ts",
|
|
63
56
|
"name": "tests/auth/integration.test.ts",
|
|
64
57
|
"type": "file",
|
|
65
58
|
"task_id": "auth-t4",
|
|
66
|
-
"created_at": "2026-04-
|
|
59
|
+
"created_at": "2026-04-07T12:58:13.323Z"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "artifact-demo-auth-revamp-reports-auth-review-summary-md",
|
|
63
|
+
"name": "reports/auth-review-summary.md",
|
|
64
|
+
"type": "summary",
|
|
65
|
+
"task_id": "auth-t5",
|
|
66
|
+
"created_at": "2026-04-07T12:58:13.324Z"
|
|
67
67
|
}
|
|
68
68
|
],
|
|
69
69
|
"has_more_events": false,
|