opencastle 0.14.0 → 0.16.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/dist/cli/convoy/store.d.ts +1 -0
- package/dist/cli/convoy/store.d.ts.map +1 -1
- package/dist/cli/convoy/store.js +5 -0
- package/dist/cli/convoy/store.js.map +1 -1
- package/dist/cli/run/schema.d.ts +5 -0
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +98 -143
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run/schema.test.js +53 -215
- package/dist/cli/run/schema.test.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +202 -104
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/types.d.ts +2 -58
- package/dist/cli/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/convoy/store.ts +7 -0
- package/src/cli/run/schema.test.ts +61 -241
- package/src/cli/run/schema.ts +105 -153
- package/src/cli/run.ts +216 -105
- package/src/cli/types.ts +2 -66
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/orchestrator/agents/team-lead.agent.md +2 -2
- package/src/orchestrator/prompts/generate-task-spec.prompt.md +26 -5
- package/dist/cli/run/loop-executor.d.ts +0 -3
- package/dist/cli/run/loop-executor.d.ts.map +0 -1
- package/dist/cli/run/loop-executor.js +0 -155
- package/dist/cli/run/loop-executor.js.map +0 -1
- package/dist/cli/run/loop-reporter.d.ts +0 -6
- package/dist/cli/run/loop-reporter.d.ts.map +0 -1
- package/dist/cli/run/loop-reporter.js +0 -112
- package/dist/cli/run/loop-reporter.js.map +0 -1
- package/src/cli/run/loop-executor.ts +0 -199
- package/src/cli/run/loop-reporter.ts +0 -125
package/src/cli/run.ts
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
2
3
|
import { resolve } from 'node:path'
|
|
3
|
-
import {
|
|
4
|
+
import { parseTaskSpecText, isConvoySpec } from './run/schema.js'
|
|
4
5
|
import { createExecutor, buildPhases } from './run/executor.js'
|
|
5
6
|
import { getAdapter, detectAdapter } from './run/adapters/index.js'
|
|
6
7
|
import { createReporter, printExecutionPlan } from './run/reporter.js'
|
|
7
8
|
import type { CliContext, RunOptions } from './types.js'
|
|
9
|
+
import type { ConvoyResult } from './convoy/engine.js'
|
|
8
10
|
|
|
9
11
|
const HELP = `
|
|
10
12
|
opencastle run [options]
|
|
11
13
|
|
|
12
14
|
Process a task queue from a spec file, delegating to AI agents autonomously.
|
|
13
|
-
|
|
15
|
+
Version 1 specs use the Convoy Engine; legacy specs use the standard executor.
|
|
14
16
|
|
|
15
17
|
Options:
|
|
16
18
|
--file, -f <path> Task spec file (default: opencastle.tasks.yml)
|
|
17
19
|
--dry-run Show execution plan without running
|
|
18
|
-
--concurrency, -c <n> Override max parallel tasks
|
|
20
|
+
--concurrency, -c <n> Override max parallel tasks
|
|
19
21
|
--adapter, -a <name> Override agent runtime adapter
|
|
20
22
|
--report-dir <path> Where to write run reports (default: .opencastle/runs)
|
|
21
23
|
--verbose Show full agent output
|
|
22
|
-
--
|
|
23
|
-
--
|
|
24
|
+
--resume Resume the last interrupted convoy from .opencastle/convoy.db
|
|
25
|
+
--status Print the current convoy state from .opencastle/convoy.db
|
|
24
26
|
--help, -h Show this help
|
|
25
27
|
`
|
|
26
28
|
|
|
@@ -36,8 +38,8 @@ function parseArgs(args: string[]): RunOptions {
|
|
|
36
38
|
reportDir: null,
|
|
37
39
|
verbose: false,
|
|
38
40
|
help: false,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
resume: false,
|
|
42
|
+
status: false,
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -79,26 +81,12 @@ function parseArgs(args: string[]): RunOptions {
|
|
|
79
81
|
case '--verbose':
|
|
80
82
|
opts.verbose = true
|
|
81
83
|
break
|
|
82
|
-
case '--
|
|
83
|
-
|
|
84
|
-
const val = parseInt(args[++i], 10)
|
|
85
|
-
if (!Number.isFinite(val) || val < 1) {
|
|
86
|
-
console.error(` \u2717 --max-iterations must be an integer >= 1`)
|
|
87
|
-
process.exit(1)
|
|
88
|
-
}
|
|
89
|
-
opts.maxIterations = val
|
|
84
|
+
case '--resume':
|
|
85
|
+
opts.resume = true
|
|
90
86
|
break
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (i + 1 >= args.length) { console.error(' \u2717 --mode requires a name'); process.exit(1) }
|
|
94
|
-
const modeVal = args[++i]
|
|
95
|
-
if (modeVal !== 'tasks' && modeVal !== 'loop') {
|
|
96
|
-
console.error(` \u2717 --mode must be one of: tasks, loop`)
|
|
97
|
-
process.exit(1)
|
|
98
|
-
}
|
|
99
|
-
opts.mode = modeVal
|
|
87
|
+
case '--status':
|
|
88
|
+
opts.status = true
|
|
100
89
|
break
|
|
101
|
-
}
|
|
102
90
|
default:
|
|
103
91
|
console.error(` ✗ Unknown option: ${arg}`)
|
|
104
92
|
console.log(HELP)
|
|
@@ -109,6 +97,61 @@ function parseArgs(args: string[]): RunOptions {
|
|
|
109
97
|
return opts
|
|
110
98
|
}
|
|
111
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Print a user-friendly adapter unavailable error.
|
|
102
|
+
*/
|
|
103
|
+
function printAdapterError(detectionFailed: boolean, adapterName: string): void {
|
|
104
|
+
if (detectionFailed) {
|
|
105
|
+
console.error(
|
|
106
|
+
` ✗ No agent CLI found on your PATH.\n` +
|
|
107
|
+
` Install one of the following adapters:\n` +
|
|
108
|
+
` • copilot — https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli\n` +
|
|
109
|
+
` • claude — npm install -g @anthropic-ai/claude-code\n` +
|
|
110
|
+
` • cursor — https://cursor.com (Cursor > Install CLI)\n` +
|
|
111
|
+
`\n` +
|
|
112
|
+
` Or specify an adapter explicitly: opencastle run --adapter <name>`
|
|
113
|
+
)
|
|
114
|
+
} else {
|
|
115
|
+
const hints: Record<string, string> = {
|
|
116
|
+
'claude-code':
|
|
117
|
+
' Install: npm install -g @anthropic-ai/claude-code\n' +
|
|
118
|
+
' Docs: https://docs.anthropic.com/en/docs/claude-code',
|
|
119
|
+
copilot:
|
|
120
|
+
' Requires the Copilot CLI installed and authenticated:\n' +
|
|
121
|
+
' https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli\n' +
|
|
122
|
+
' Docs: https://docs.github.com/en/copilot',
|
|
123
|
+
cursor:
|
|
124
|
+
' The Cursor agent CLI ships with the Cursor editor.\n' +
|
|
125
|
+
' Install Cursor from https://cursor.com and ensure the\n' +
|
|
126
|
+
' "agent" command is on your PATH (Cursor > Install CLI).',
|
|
127
|
+
}
|
|
128
|
+
const cliName = adapterName === 'claude-code' ? 'claude' : adapterName
|
|
129
|
+
const hint = hints[adapterName] ?? ''
|
|
130
|
+
console.error(
|
|
131
|
+
` ✗ Adapter "${adapterName}" is not available.\n` +
|
|
132
|
+
` Make sure the "${cliName}" CLI is installed and on your PATH.\n` +
|
|
133
|
+
hint
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Print a convoy result summary.
|
|
140
|
+
*/
|
|
141
|
+
function printConvoyResult(result: ConvoyResult): void {
|
|
142
|
+
console.log(`\n ──────────────────────────────────────`)
|
|
143
|
+
console.log(` Convoy ${result.status}: ${result.duration}`)
|
|
144
|
+
console.log(
|
|
145
|
+
` Done: ${result.summary.done} | Failed: ${result.summary.failed} | Skipped: ${result.summary.skipped} | Timed out: ${result.summary.timedOut}`
|
|
146
|
+
)
|
|
147
|
+
if (result.gateResults) {
|
|
148
|
+
console.log(` Gates:`)
|
|
149
|
+
for (const g of result.gateResults) {
|
|
150
|
+
console.log(` ${g.passed ? '✓' : '✗'} ${g.command}`)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
112
155
|
/**
|
|
113
156
|
* CLI entry point for the `run` command.
|
|
114
157
|
*/
|
|
@@ -120,15 +163,133 @@ export default async function run({ args }: CliContext): Promise<void> {
|
|
|
120
163
|
return
|
|
121
164
|
}
|
|
122
165
|
|
|
166
|
+
const dbPath = resolve(process.cwd(), '.opencastle', 'convoy.db')
|
|
167
|
+
|
|
168
|
+
// ── --status flag ─────────────────────────────────────────────
|
|
169
|
+
if (opts.status) {
|
|
170
|
+
if (!existsSync(dbPath)) {
|
|
171
|
+
console.log(' No convoy database found at .opencastle/convoy.db')
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
const { createConvoyStore } = await import('./convoy/store.js')
|
|
175
|
+
const store = createConvoyStore(dbPath)
|
|
176
|
+
try {
|
|
177
|
+
const convoy = store.getLatestConvoy()
|
|
178
|
+
if (!convoy) {
|
|
179
|
+
console.log(' No convoy records found.')
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
const tasks = store.getTasksByConvoy(convoy.id)
|
|
183
|
+
const byStatus = tasks.reduce((acc, t) => {
|
|
184
|
+
acc[t.status] = (acc[t.status] ?? 0) + 1
|
|
185
|
+
return acc
|
|
186
|
+
}, {} as Record<string, number>)
|
|
187
|
+
console.log(`\n Convoy: ${convoy.name}`)
|
|
188
|
+
console.log(` ID: ${convoy.id}`)
|
|
189
|
+
console.log(` Status: ${convoy.status}`)
|
|
190
|
+
console.log(` Branch: ${convoy.branch ?? '(none)'}`)
|
|
191
|
+
console.log(` Created: ${convoy.created_at}`)
|
|
192
|
+
if (convoy.started_at) console.log(` Started: ${convoy.started_at}`)
|
|
193
|
+
if (convoy.finished_at) console.log(` Finished: ${convoy.finished_at}`)
|
|
194
|
+
console.log(`\n Tasks:`)
|
|
195
|
+
for (const [status, count] of Object.entries(byStatus)) {
|
|
196
|
+
console.log(` ${status}: ${count}`)
|
|
197
|
+
}
|
|
198
|
+
console.log(` total: ${tasks.length}`)
|
|
199
|
+
} finally {
|
|
200
|
+
store.close()
|
|
201
|
+
}
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── --resume flag ─────────────────────────────────────────────
|
|
206
|
+
if (opts.resume) {
|
|
207
|
+
if (!existsSync(dbPath)) {
|
|
208
|
+
console.error(' ✗ No convoy database found at .opencastle/convoy.db')
|
|
209
|
+
console.error(' Run a convoy spec first: opencastle run convoy.yml')
|
|
210
|
+
process.exit(1)
|
|
211
|
+
}
|
|
212
|
+
const { createConvoyStore } = await import('./convoy/store.js')
|
|
213
|
+
const store = createConvoyStore(dbPath)
|
|
214
|
+
const convoy = store.getLatestConvoy()
|
|
215
|
+
store.close()
|
|
216
|
+
if (!convoy) {
|
|
217
|
+
console.error(' ✗ No convoy records found in .opencastle/convoy.db')
|
|
218
|
+
process.exit(1)
|
|
219
|
+
}
|
|
220
|
+
if (convoy.status === 'done' || convoy.status === 'failed') {
|
|
221
|
+
console.error(
|
|
222
|
+
` ✗ Last convoy "${convoy.name}" already finished with status: ${convoy.status}`
|
|
223
|
+
)
|
|
224
|
+
console.error(` Only interrupted (running/pending) convoys can be resumed.`)
|
|
225
|
+
process.exit(1)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const resumeSpec = parseTaskSpecText(convoy.spec_yaml)
|
|
229
|
+
if (opts.concurrency !== null) resumeSpec.concurrency = opts.concurrency
|
|
230
|
+
if (opts.adapter !== null) resumeSpec.adapter = opts.adapter
|
|
231
|
+
if (opts.verbose) resumeSpec._verbose = true
|
|
232
|
+
|
|
233
|
+
let resumeDetectionFailed = false
|
|
234
|
+
if (!resumeSpec.adapter) {
|
|
235
|
+
const detected = await detectAdapter()
|
|
236
|
+
if (detected) {
|
|
237
|
+
resumeSpec.adapter = detected
|
|
238
|
+
console.log(` ℹ Auto-detected adapter: ${detected}`)
|
|
239
|
+
} else {
|
|
240
|
+
resumeDetectionFailed = true
|
|
241
|
+
resumeSpec.adapter = 'claude-code'
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const resumeAdapter = await getAdapter(resumeSpec.adapter)
|
|
246
|
+
const resumeAvailable = await resumeAdapter.isAvailable()
|
|
247
|
+
if (!resumeAvailable) {
|
|
248
|
+
printAdapterError(resumeDetectionFailed, resumeSpec.adapter)
|
|
249
|
+
process.exit(1)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
console.log(`\n \uD83C\uDFF0 OpenCastle Convoy (Resume): ${convoy.name}`)
|
|
253
|
+
console.log(` Convoy ID: ${convoy.id}`)
|
|
254
|
+
const { createConvoyEngine } = await import('./convoy/engine.js')
|
|
255
|
+
const resumeEngine = createConvoyEngine({
|
|
256
|
+
spec: resumeSpec,
|
|
257
|
+
specYaml: convoy.spec_yaml,
|
|
258
|
+
adapter: resumeAdapter,
|
|
259
|
+
verbose: opts.verbose,
|
|
260
|
+
})
|
|
261
|
+
const resumeResult = await resumeEngine.resume(convoy.id)
|
|
262
|
+
printConvoyResult(resumeResult)
|
|
263
|
+
process.exit(resumeResult.status !== 'done' ? 1 : 0)
|
|
264
|
+
}
|
|
265
|
+
|
|
123
266
|
// ── Read and validate spec ────────────────────────────────────
|
|
124
267
|
const specPath = resolve(process.cwd(), opts.file)
|
|
125
|
-
|
|
268
|
+
let specText = ''
|
|
269
|
+
try {
|
|
270
|
+
specText = await readFile(specPath, 'utf8')
|
|
271
|
+
} catch (err: unknown) {
|
|
272
|
+
const e = err as Error & { code?: string }
|
|
273
|
+
if (e.code === 'ENOENT') {
|
|
274
|
+
console.error(` ✗ Task spec file not found: ${opts.file}`)
|
|
275
|
+
} else {
|
|
276
|
+
console.error(` ✗ Cannot read task spec file: ${e.message}`)
|
|
277
|
+
}
|
|
278
|
+
process.exit(1)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let spec
|
|
282
|
+
try {
|
|
283
|
+
spec = parseTaskSpecText(specText)
|
|
284
|
+
} catch (err: unknown) {
|
|
285
|
+
console.error(` ✗ ${(err as Error).message}`)
|
|
286
|
+
process.exit(1)
|
|
287
|
+
}
|
|
126
288
|
|
|
127
289
|
// Apply CLI overrides
|
|
128
290
|
if (opts.concurrency !== null) spec.concurrency = opts.concurrency
|
|
129
291
|
if (opts.adapter !== null) spec.adapter = opts.adapter
|
|
130
292
|
if (opts.verbose) spec._verbose = true
|
|
131
|
-
if (opts.mode !== null) spec.mode = opts.mode as 'tasks' | 'loop'
|
|
132
293
|
|
|
133
294
|
// ── Auto-detect adapter if not specified ─────────────────────
|
|
134
295
|
let detectionFailed = false
|
|
@@ -145,22 +306,13 @@ export default async function run({ args }: CliContext): Promise<void> {
|
|
|
145
306
|
|
|
146
307
|
// ── Dry run ──────────────────────────────────────────────────
|
|
147
308
|
if (opts.dryRun) {
|
|
148
|
-
if (spec
|
|
149
|
-
|
|
150
|
-
console.log(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
console.log(`
|
|
154
|
-
console.log(`
|
|
155
|
-
if (loop.plan_file) console.log(` Plan file: ${loop.plan_file}`)
|
|
156
|
-
if (loop.model) console.log(` Model: ${loop.model}`)
|
|
157
|
-
if (loop.backpressure?.length) {
|
|
158
|
-
console.log(` Backpressure:`)
|
|
159
|
-
for (const cmd of loop.backpressure) {
|
|
160
|
-
console.log(` - ${cmd}`)
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return
|
|
309
|
+
if (isConvoySpec(spec)) {
|
|
310
|
+
console.log(`\n \uD83C\uDFF0 Convoy Plan: ${spec.name}`)
|
|
311
|
+
console.log(
|
|
312
|
+
` Adapter: ${spec.adapter} | Concurrency: ${spec.concurrency} | Tasks: ${spec.tasks!.length}`
|
|
313
|
+
)
|
|
314
|
+
if (spec.branch) console.log(` Branch: ${spec.branch}`)
|
|
315
|
+
if (spec.gates?.length) console.log(` Gates: ${spec.gates.length} validation commands`)
|
|
164
316
|
}
|
|
165
317
|
const phases = buildPhases(spec.tasks!)
|
|
166
318
|
printExecutionPlan(spec, phases)
|
|
@@ -171,78 +323,37 @@ export default async function run({ args }: CliContext): Promise<void> {
|
|
|
171
323
|
const adapter = await getAdapter(spec.adapter)
|
|
172
324
|
const available = await adapter.isAvailable()
|
|
173
325
|
if (!available) {
|
|
174
|
-
|
|
175
|
-
console.error(
|
|
176
|
-
` ✗ No agent CLI found on your PATH.\n` +
|
|
177
|
-
` Install one of the following adapters:\n` +
|
|
178
|
-
` • copilot — https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli\n` +
|
|
179
|
-
` • claude — npm install -g @anthropic-ai/claude-code\n` +
|
|
180
|
-
` • cursor — https://cursor.com (Cursor > Install CLI)\n` +
|
|
181
|
-
`\n` +
|
|
182
|
-
` Or specify an adapter explicitly: opencastle run --adapter <name>`
|
|
183
|
-
)
|
|
184
|
-
} else {
|
|
185
|
-
const hints: Record<string, string> = {
|
|
186
|
-
'claude-code':
|
|
187
|
-
' Install: npm install -g @anthropic-ai/claude-code\n' +
|
|
188
|
-
' Docs: https://docs.anthropic.com/en/docs/claude-code',
|
|
189
|
-
copilot:
|
|
190
|
-
' Requires the Copilot CLI installed and authenticated:\n' +
|
|
191
|
-
' https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli\n' +
|
|
192
|
-
' Docs: https://docs.github.com/en/copilot',
|
|
193
|
-
cursor:
|
|
194
|
-
' The Cursor agent CLI ships with the Cursor editor.\n' +
|
|
195
|
-
' Install Cursor from https://cursor.com and ensure the\n' +
|
|
196
|
-
' "agent" command is on your PATH (Cursor > Install CLI).',
|
|
197
|
-
}
|
|
198
|
-
const cliName = spec.adapter === 'claude-code' ? 'claude' : spec.adapter
|
|
199
|
-
const hint = hints[spec.adapter] ?? ''
|
|
200
|
-
console.error(
|
|
201
|
-
` ✗ Adapter "${spec.adapter}" is not available.\n` +
|
|
202
|
-
` Make sure the "${cliName}" CLI is installed and on your PATH.\n` +
|
|
203
|
-
hint
|
|
204
|
-
)
|
|
205
|
-
}
|
|
326
|
+
printAdapterError(detectionFailed, spec.adapter)
|
|
206
327
|
process.exit(1)
|
|
207
328
|
}
|
|
208
329
|
|
|
209
|
-
// ──
|
|
210
|
-
if (spec
|
|
211
|
-
const {
|
|
212
|
-
|
|
330
|
+
// ── Convoy engine path (version: 1 specs) ────────────────────
|
|
331
|
+
if (isConvoySpec(spec)) {
|
|
332
|
+
const { createConvoyEngine } = await import('./convoy/engine.js')
|
|
333
|
+
console.log(`\n \uD83C\uDFF0 OpenCastle Convoy: ${spec.name}`)
|
|
334
|
+
console.log(
|
|
335
|
+
` Adapter: ${adapter.name} | Concurrency: ${spec.concurrency} | Tasks: ${spec.tasks!.length}`
|
|
336
|
+
)
|
|
337
|
+
if (spec.branch) console.log(` Branch: ${spec.branch}`)
|
|
338
|
+
if (spec.gates?.length) console.log(` Gates: ${spec.gates.length} validation commands`)
|
|
213
339
|
|
|
214
|
-
|
|
215
|
-
spec
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const promptPath = resolve(process.cwd(), spec.loop!.prompt)
|
|
219
|
-
try {
|
|
220
|
-
await readFile(promptPath)
|
|
221
|
-
} catch {
|
|
222
|
-
console.error(` \u2717 Prompt file not found: ${spec.loop!.prompt}`)
|
|
223
|
-
process.exit(1)
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
console.log(`\n \uD83C\uDFF0 OpenCastle Loop: ${spec.name}`)
|
|
227
|
-
console.log(` Adapter: ${adapter.name} | Max iterations: ${spec.loop!.max_iterations} | Timeout: ${spec.loop!.timeout}`)
|
|
228
|
-
if (spec.loop!.backpressure?.length) {
|
|
229
|
-
console.log(` Backpressure: ${spec.loop!.backpressure.join(', ')}`)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const loopReporter = createLoopReporter(spec.name, {
|
|
233
|
-
reportDir: opts.reportDir ? resolve(process.cwd(), opts.reportDir) : undefined,
|
|
340
|
+
const engine = createConvoyEngine({
|
|
341
|
+
spec,
|
|
342
|
+
specYaml: specText,
|
|
343
|
+
adapter,
|
|
234
344
|
verbose: opts.verbose,
|
|
235
345
|
})
|
|
236
346
|
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const failed = loopReport.stoppedReason === 'error' || loopReport.stoppedReason === 'backpressure-fail'
|
|
241
|
-
process.exit(failed ? 1 : 0)
|
|
347
|
+
const result = await engine.run()
|
|
348
|
+
printConvoyResult(result)
|
|
349
|
+
process.exit(result.status !== 'done' ? 1 : 0)
|
|
242
350
|
}
|
|
243
351
|
|
|
352
|
+
// ── Legacy executor path ──────────────────────────────────────
|
|
244
353
|
console.log(`\n \uD83C\uDFF0 OpenCastle Run: ${spec.name}`)
|
|
245
|
-
console.log(
|
|
354
|
+
console.log(
|
|
355
|
+
` Adapter: ${adapter.name} | Concurrency: ${spec.concurrency} | Tasks: ${spec.tasks!.length}`
|
|
356
|
+
)
|
|
246
357
|
|
|
247
358
|
const reporter = createReporter(spec, {
|
|
248
359
|
reportDir: opts.reportDir
|
package/src/cli/types.ts
CHANGED
|
@@ -134,22 +134,6 @@ export interface TaskDefaults {
|
|
|
134
134
|
agent?: string;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
/** Loop execution configuration. */
|
|
138
|
-
export interface LoopConfig {
|
|
139
|
-
/** Maximum number of agent iterations (default 20). */
|
|
140
|
-
max_iterations: number;
|
|
141
|
-
/** Path to the prompt file read each iteration. */
|
|
142
|
-
prompt: string;
|
|
143
|
-
/** Path to the plan file (default 'IMPLEMENTATION_PLAN.md'). */
|
|
144
|
-
plan_file?: string;
|
|
145
|
-
/** Per-iteration timeout (default '10m'). */
|
|
146
|
-
timeout: string;
|
|
147
|
-
/** Model override for loop sessions. */
|
|
148
|
-
model?: string;
|
|
149
|
-
/** Shell commands that must exit 0 after each iteration. */
|
|
150
|
-
backpressure?: string[];
|
|
151
|
-
}
|
|
152
|
-
|
|
153
137
|
/** Validated task spec from YAML. */
|
|
154
138
|
export interface TaskSpec {
|
|
155
139
|
name: string;
|
|
@@ -157,8 +141,6 @@ export interface TaskSpec {
|
|
|
157
141
|
on_failure: 'continue' | 'stop';
|
|
158
142
|
adapter: string;
|
|
159
143
|
tasks?: Task[];
|
|
160
|
-
mode?: 'tasks' | 'loop';
|
|
161
|
-
loop?: LoopConfig;
|
|
162
144
|
_verbose?: boolean;
|
|
163
145
|
/** Spec schema version (1 for Convoy Engine format). */
|
|
164
146
|
version?: number;
|
|
@@ -271,8 +253,8 @@ export interface RunOptions {
|
|
|
271
253
|
reportDir: string | null;
|
|
272
254
|
verbose: boolean;
|
|
273
255
|
help: boolean;
|
|
274
|
-
|
|
275
|
-
|
|
256
|
+
resume: boolean;
|
|
257
|
+
status: boolean;
|
|
276
258
|
}
|
|
277
259
|
|
|
278
260
|
/** Validation result. */
|
|
@@ -292,49 +274,3 @@ export interface Executor {
|
|
|
292
274
|
run(): Promise<RunReport>;
|
|
293
275
|
getPhases(): Task[][];
|
|
294
276
|
}
|
|
295
|
-
|
|
296
|
-
// ── Loop executor types ────────────────────────────────────────
|
|
297
|
-
|
|
298
|
-
/** Result of a single backpressure command run. */
|
|
299
|
-
export interface BackpressureResult {
|
|
300
|
-
command: string;
|
|
301
|
-
exitCode: number;
|
|
302
|
-
output: string;
|
|
303
|
-
passed: boolean;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/** Result of a single loop iteration. */
|
|
307
|
-
export interface LoopIterationResult {
|
|
308
|
-
iteration: number;
|
|
309
|
-
status: 'done' | 'failed' | 'backpressure-fail';
|
|
310
|
-
duration: number;
|
|
311
|
-
output: string;
|
|
312
|
-
backpressureResults?: BackpressureResult[];
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/** Final report produced by the loop executor. */
|
|
316
|
-
export interface LoopRunReport {
|
|
317
|
-
name: string;
|
|
318
|
-
mode: 'loop';
|
|
319
|
-
startedAt: string;
|
|
320
|
-
completedAt: string;
|
|
321
|
-
duration: string;
|
|
322
|
-
totalIterations: number;
|
|
323
|
-
completedIterations: number;
|
|
324
|
-
stoppedReason: 'max-iterations' | 'plan-empty' | 'backpressure-fail' | 'user-abort' | 'error';
|
|
325
|
-
iterations: LoopIterationResult[];
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/** Reporter interface for loop execution progress. */
|
|
329
|
-
export interface LoopReporter {
|
|
330
|
-
onIterationStart(iteration: number, maxIterations: number): void;
|
|
331
|
-
onIterationDone(iteration: number, result: LoopIterationResult): void;
|
|
332
|
-
onBackpressureStart(command: string): void;
|
|
333
|
-
onBackpressureResult(result: BackpressureResult): void;
|
|
334
|
-
onComplete(report: LoopRunReport): Promise<void>;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/** Executor for loop-mode run specs. */
|
|
338
|
-
export interface LoopExecutor {
|
|
339
|
-
run(): Promise<LoopRunReport>;
|
|
340
|
-
}
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
|
-
"hash": "
|
|
2
|
+
"hash": "f5f05037",
|
|
3
3
|
"configHash": "30f8ea04",
|
|
4
|
-
"lockfileHash": "
|
|
5
|
-
"browserHash": "
|
|
4
|
+
"lockfileHash": "99d70434",
|
|
5
|
+
"browserHash": "d43a2a07",
|
|
6
6
|
"optimized": {
|
|
7
7
|
"astro > cssesc": {
|
|
8
8
|
"src": "../../../../../node_modules/cssesc/cssesc.js",
|
|
9
9
|
"file": "astro___cssesc.js",
|
|
10
|
-
"fileHash": "
|
|
10
|
+
"fileHash": "29d5277e",
|
|
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": "
|
|
16
|
+
"fileHash": "4ca620a4",
|
|
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": "
|
|
22
|
+
"fileHash": "4072a265",
|
|
23
23
|
"needsInterop": true
|
|
24
24
|
}
|
|
25
25
|
},
|
|
@@ -17,9 +17,9 @@ handoffs:
|
|
|
17
17
|
- label: Quick Refinement
|
|
18
18
|
agent: 'Team Lead (OpenCastle)'
|
|
19
19
|
prompt: 'Use the quick-refinement prompt to handle these follow-up refinements (UI tweaks, polish, adjustments):'
|
|
20
|
-
- label: Generate
|
|
20
|
+
- label: Generate Convoy Spec
|
|
21
21
|
agent: 'Team Lead (OpenCastle)'
|
|
22
|
-
prompt: 'Use the generate-task-spec prompt to create
|
|
22
|
+
prompt: 'Use the generate-task-spec prompt to create a .convoy.yml spec for autonomous convoy runs based on:'
|
|
23
23
|
- label: Resolve PR Comments
|
|
24
24
|
agent: 'Team Lead (OpenCastle)'
|
|
25
25
|
prompt: 'Use the resolve-pr-comments prompt to resolve the GitHub PR review comments on this PR:'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: 'Generate a valid .
|
|
2
|
+
description: 'Generate a valid .convoy.yml spec file for autonomous convoy runs based on a high-level description of what needs to be done.'
|
|
3
3
|
agent: 'Team Lead (OpenCastle)'
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -7,7 +7,7 @@ agent: 'Team Lead (OpenCastle)'
|
|
|
7
7
|
|
|
8
8
|
# Generate Task Spec for Autonomous Run
|
|
9
9
|
|
|
10
|
-
You are the Team Lead. The user wants to run `opencastle run` to execute a batch of tasks autonomously (e.g., overnight). Your job is to produce a valid `.
|
|
10
|
+
You are the Team Lead. The user wants to run `opencastle run` to execute a batch of tasks autonomously (e.g., overnight). Your job is to produce a valid `.convoy.yml` file they can feed to the CLI. Derive a short, descriptive, kebab-case filename from the user's goal (2–4 words max) and use it as the filename — for example `auth-refactor.convoy.yml` or `add-search.convoy.yml`. Always use the `.convoy.yml` extension.
|
|
11
11
|
|
|
12
12
|
## User Goal
|
|
13
13
|
|
|
@@ -28,9 +28,13 @@ The output file must conform to the following schema. Fields marked **(required)
|
|
|
28
28
|
| Field | Type | Required | Default | Description |
|
|
29
29
|
|-------|------|----------|---------|-------------|
|
|
30
30
|
| `name` | string | **yes** | — | Human-readable name for the run |
|
|
31
|
+
| `version` | integer | **yes** | — | Spec schema version. Always `1` for convoy specs |
|
|
31
32
|
| `concurrency` | integer ≥ 1 | no | `1` | Max tasks executing in parallel |
|
|
32
33
|
| `on_failure` | `continue` \| `stop` | no | `continue` | Behaviour when a task fails |
|
|
33
34
|
| `adapter` | string | no | auto-detect | Default CLI adapter (`claude-code`, `copilot`, `cursor`). Omit to let the CLI auto-detect the first available adapter. |
|
|
35
|
+
| `branch` | string | no | — | Git feature branch name — created if missing |
|
|
36
|
+
| `defaults` | object | no | — | Worker defaults merged into each task. Keys: `timeout`, `model`, `max_retries`, `agent` |
|
|
37
|
+
| `gates` | array of strings | no | — | Shell commands run after all tasks complete; each must exit 0 |
|
|
34
38
|
| `tasks` | list | **yes** | — | Non-empty list of task objects |
|
|
35
39
|
|
|
36
40
|
### Task Fields
|
|
@@ -44,6 +48,8 @@ The output file must conform to the following schema. Fields marked **(required)
|
|
|
44
48
|
| `depends_on` | list of ids | no | `[]` | Task ids that must finish before this one starts |
|
|
45
49
|
| `files` | list of globs | no | `[]` | File scope the agent is allowed to modify |
|
|
46
50
|
| `timeout` | duration | no | `30m` | Max wall time (`<number><s|m|h>`, e.g. `10m`, `1h`) |
|
|
51
|
+
| `max_retries` | integer | no | from `defaults` or `1` | Max retry attempts for this task |
|
|
52
|
+
| `model` | string | no | — | AI model override for this task |
|
|
47
53
|
|
|
48
54
|
### Agent Roster
|
|
49
55
|
|
|
@@ -97,6 +103,9 @@ For each workstream, break it down into the smallest meaningful unit of work tha
|
|
|
97
103
|
- `concurrency` — set to 2–3 for overnight runs; keep at 1 if tasks share files or the machine is constrained.
|
|
98
104
|
- `on_failure` — use `continue` (default) when tasks are independent so one failure doesn't waste the whole run. Use `stop` when every subsequent task depends on success.
|
|
99
105
|
- `adapter` — **omit this field** to let the CLI auto-detect the first available adapter (priority: `copilot` → `claude-code` → `cursor`). Only set this explicitly if the user requests a specific adapter.
|
|
106
|
+
- `branch` — derive from the goal, e.g., `feat/auth-refactor`. Use a descriptive branch name.
|
|
107
|
+
- `defaults` — set sensible defaults for timeout and max_retries. Model can be left unset for auto-detection.
|
|
108
|
+
- `gates` — include standard validation gates (lint, type-check, test) unless the user specifies otherwise.
|
|
100
109
|
|
|
101
110
|
### 5. Write the Prompts
|
|
102
111
|
|
|
@@ -128,11 +137,16 @@ Before presenting the YAML, mentally verify:
|
|
|
128
137
|
Return the final YAML inside a fenced code block with a filename annotation:
|
|
129
138
|
|
|
130
139
|
````yaml
|
|
131
|
-
# <feature-name>.
|
|
140
|
+
# <feature-name>.convoy.yml
|
|
132
141
|
name: <run name>
|
|
142
|
+
version: 1
|
|
133
143
|
concurrency: <n>
|
|
134
144
|
on_failure: <continue|stop>
|
|
135
|
-
|
|
145
|
+
branch: <branch-name>
|
|
146
|
+
|
|
147
|
+
defaults:
|
|
148
|
+
timeout: 30m
|
|
149
|
+
max_retries: 1
|
|
136
150
|
|
|
137
151
|
tasks:
|
|
138
152
|
- id: <task-id>
|
|
@@ -148,9 +162,16 @@ tasks:
|
|
|
148
162
|
depends_on:
|
|
149
163
|
- <task-id>
|
|
150
164
|
...
|
|
165
|
+
|
|
166
|
+
gates:
|
|
167
|
+
- <lint command>
|
|
168
|
+
- <type-check command>
|
|
169
|
+
- <test command>
|
|
151
170
|
````
|
|
152
171
|
|
|
153
172
|
Also provide:
|
|
154
173
|
1. A **DAG summary** showing the phase structure so the user can verify execution order.
|
|
155
174
|
2. An **estimated total duration** (sum of timeouts on the critical path).
|
|
156
|
-
3. A `--dry-run` command they can use to validate: `npx opencastle run --file <feature-name>.
|
|
175
|
+
3. A `--dry-run` command they can use to validate: `npx opencastle run --file <feature-name>.convoy.yml --dry-run`
|
|
176
|
+
|
|
177
|
+
> **Backward compatibility:** `.tasks.yml` files without `version` still work with the legacy executor. Only spec files with `version: 1` are routed to the convoy engine.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"loop-executor.d.ts","sourceRoot":"","sources":["../../../src/cli/run/loop-executor.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,QAAQ,EAIR,YAAY,EACZ,YAAY,EACZ,YAAY,EAEb,MAAM,aAAa,CAAA;AA2DpB,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,QAAQ,EACd,OAAO,EAAE,YAAY,EACrB,QAAQ,EAAE,YAAY,GACrB,YAAY,CAwHd"}
|