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.
Files changed (34) hide show
  1. package/dist/cli/convoy/store.d.ts +1 -0
  2. package/dist/cli/convoy/store.d.ts.map +1 -1
  3. package/dist/cli/convoy/store.js +5 -0
  4. package/dist/cli/convoy/store.js.map +1 -1
  5. package/dist/cli/run/schema.d.ts +5 -0
  6. package/dist/cli/run/schema.d.ts.map +1 -1
  7. package/dist/cli/run/schema.js +98 -143
  8. package/dist/cli/run/schema.js.map +1 -1
  9. package/dist/cli/run/schema.test.js +53 -215
  10. package/dist/cli/run/schema.test.js.map +1 -1
  11. package/dist/cli/run.d.ts.map +1 -1
  12. package/dist/cli/run.js +202 -104
  13. package/dist/cli/run.js.map +1 -1
  14. package/dist/cli/types.d.ts +2 -58
  15. package/dist/cli/types.d.ts.map +1 -1
  16. package/package.json +1 -1
  17. package/src/cli/convoy/store.ts +7 -0
  18. package/src/cli/run/schema.test.ts +61 -241
  19. package/src/cli/run/schema.ts +105 -153
  20. package/src/cli/run.ts +216 -105
  21. package/src/cli/types.ts +2 -66
  22. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  23. package/src/orchestrator/agents/team-lead.agent.md +2 -2
  24. package/src/orchestrator/prompts/generate-task-spec.prompt.md +26 -5
  25. package/dist/cli/run/loop-executor.d.ts +0 -3
  26. package/dist/cli/run/loop-executor.d.ts.map +0 -1
  27. package/dist/cli/run/loop-executor.js +0 -155
  28. package/dist/cli/run/loop-executor.js.map +0 -1
  29. package/dist/cli/run/loop-reporter.d.ts +0 -6
  30. package/dist/cli/run/loop-reporter.d.ts.map +0 -1
  31. package/dist/cli/run/loop-reporter.js +0 -112
  32. package/dist/cli/run/loop-reporter.js.map +0 -1
  33. package/src/cli/run/loop-executor.ts +0 -199
  34. 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 { parseTaskSpec } from './run/schema.js'
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
- Supports two modes: tasks (default phase-based execution) and loop (iterative Ralph Loop).
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 (tasks mode)
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
- --mode <name> Execution mode: tasks | loop
23
- --max-iterations <n> Override max loop iterations (loop mode)
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
- maxIterations: null,
40
- mode: null,
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 '--max-iterations': {
83
- if (i + 1 >= args.length) { console.error(' \u2717 --max-iterations requires a number'); process.exit(1) }
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
- case '--mode': {
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
- const spec = await parseTaskSpec(specPath)
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.mode === 'loop') {
149
- const loop = spec.loop!
150
- console.log(`\n \uD83C\uDFF0 Loop Plan: ${spec.name}`)
151
- console.log(` Mode: loop`)
152
- console.log(` Prompt: ${loop.prompt}`)
153
- console.log(` Max iterations: ${loop.max_iterations}`)
154
- console.log(` Timeout: ${loop.timeout}`)
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
- if (detectionFailed) {
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
- // ── Execute ──────────────────────────────────────────────────
210
- if (spec.mode === 'loop') {
211
- const { createLoopExecutor } = await import('./run/loop-executor.js')
212
- const { createLoopReporter } = await import('./run/loop-reporter.js')
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
- if (opts.maxIterations !== null && spec.loop) {
215
- spec.loop.max_iterations = opts.maxIterations
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 loopExecutor = createLoopExecutor(spec, adapter, loopReporter)
238
- const loopReport = await loopExecutor.run()
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(` Adapter: ${adapter.name} | Concurrency: ${spec.concurrency} | Tasks: ${spec.tasks!.length}`)
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
- maxIterations: number | null;
275
- mode: string | null;
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": "a6abe049",
2
+ "hash": "f5f05037",
3
3
  "configHash": "30f8ea04",
4
- "lockfileHash": "c72e777b",
5
- "browserHash": "737e5c4c",
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": "3a9e4516",
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": "774ff115",
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": "bd1844a6",
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 Task Spec
20
+ - label: Generate Convoy Spec
21
21
  agent: 'Team Lead (OpenCastle)'
22
- prompt: 'Use the generate-task-spec prompt to create an opencastle.tasks.yml spec for autonomous overnight runs based on:'
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 .tasks.yml spec file for autonomous overnight runs based on a high-level description of what needs to be done.'
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 `.tasks.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.tasks.yml` or `add-search.tasks.yml`. Always use the `.tasks.yml` extension.
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>.tasks.yml
140
+ # <feature-name>.convoy.yml
132
141
  name: <run name>
142
+ version: 1
133
143
  concurrency: <n>
134
144
  on_failure: <continue|stop>
135
- # adapter: <adapter> # Omit for auto-detection; set only when user requests a specific adapter
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>.tasks.yml --dry-run`
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,3 +0,0 @@
1
- import type { TaskSpec, AgentAdapter, LoopReporter, LoopExecutor } from '../types.js';
2
- export declare function createLoopExecutor(spec: TaskSpec, adapter: AgentAdapter, reporter: LoopReporter): LoopExecutor;
3
- //# sourceMappingURL=loop-executor.d.ts.map
@@ -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"}