opencastle 0.27.0 → 0.27.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/bin/cli.mjs +6 -0
  2. package/dist/cli/agents.d.ts +3 -0
  3. package/dist/cli/agents.d.ts.map +1 -0
  4. package/dist/cli/agents.js +161 -0
  5. package/dist/cli/agents.js.map +1 -0
  6. package/dist/cli/baselines.d.ts +3 -0
  7. package/dist/cli/baselines.d.ts.map +1 -0
  8. package/dist/cli/baselines.js +128 -0
  9. package/dist/cli/baselines.js.map +1 -0
  10. package/dist/cli/convoy/dashboard-types.d.ts +146 -0
  11. package/dist/cli/convoy/dashboard-types.d.ts.map +1 -0
  12. package/dist/cli/convoy/dashboard-types.js +2 -0
  13. package/dist/cli/convoy/dashboard-types.js.map +1 -0
  14. package/dist/cli/convoy/engine.d.ts +67 -2
  15. package/dist/cli/convoy/engine.d.ts.map +1 -1
  16. package/dist/cli/convoy/engine.js +2036 -28
  17. package/dist/cli/convoy/engine.js.map +1 -1
  18. package/dist/cli/convoy/engine.test.js +1659 -70
  19. package/dist/cli/convoy/engine.test.js.map +1 -1
  20. package/dist/cli/convoy/event-schemas.d.ts +9 -0
  21. package/dist/cli/convoy/event-schemas.d.ts.map +1 -0
  22. package/dist/cli/convoy/event-schemas.js +185 -0
  23. package/dist/cli/convoy/event-schemas.js.map +1 -0
  24. package/dist/cli/convoy/events.d.ts +12 -1
  25. package/dist/cli/convoy/events.d.ts.map +1 -1
  26. package/dist/cli/convoy/events.js +186 -13
  27. package/dist/cli/convoy/events.js.map +1 -1
  28. package/dist/cli/convoy/events.test.js +325 -28
  29. package/dist/cli/convoy/events.test.js.map +1 -1
  30. package/dist/cli/convoy/expertise.d.ts +16 -0
  31. package/dist/cli/convoy/expertise.d.ts.map +1 -0
  32. package/dist/cli/convoy/expertise.js +121 -0
  33. package/dist/cli/convoy/expertise.js.map +1 -0
  34. package/dist/cli/convoy/expertise.test.d.ts +2 -0
  35. package/dist/cli/convoy/expertise.test.d.ts.map +1 -0
  36. package/dist/cli/convoy/expertise.test.js +96 -0
  37. package/dist/cli/convoy/expertise.test.js.map +1 -0
  38. package/dist/cli/convoy/export.test.js +1 -0
  39. package/dist/cli/convoy/export.test.js.map +1 -1
  40. package/dist/cli/convoy/formula.d.ts +19 -0
  41. package/dist/cli/convoy/formula.d.ts.map +1 -0
  42. package/dist/cli/convoy/formula.js +142 -0
  43. package/dist/cli/convoy/formula.js.map +1 -0
  44. package/dist/cli/convoy/formula.test.d.ts +2 -0
  45. package/dist/cli/convoy/formula.test.d.ts.map +1 -0
  46. package/dist/cli/convoy/formula.test.js +342 -0
  47. package/dist/cli/convoy/formula.test.js.map +1 -0
  48. package/dist/cli/convoy/gates.d.ts +128 -0
  49. package/dist/cli/convoy/gates.d.ts.map +1 -0
  50. package/dist/cli/convoy/gates.js +606 -0
  51. package/dist/cli/convoy/gates.js.map +1 -0
  52. package/dist/cli/convoy/gates.test.d.ts +2 -0
  53. package/dist/cli/convoy/gates.test.d.ts.map +1 -0
  54. package/dist/cli/convoy/gates.test.js +976 -0
  55. package/dist/cli/convoy/gates.test.js.map +1 -0
  56. package/dist/cli/convoy/health.d.ts +11 -0
  57. package/dist/cli/convoy/health.d.ts.map +1 -1
  58. package/dist/cli/convoy/health.js +54 -0
  59. package/dist/cli/convoy/health.js.map +1 -1
  60. package/dist/cli/convoy/health.test.js +56 -1
  61. package/dist/cli/convoy/health.test.js.map +1 -1
  62. package/dist/cli/convoy/issues.d.ts +8 -0
  63. package/dist/cli/convoy/issues.d.ts.map +1 -0
  64. package/dist/cli/convoy/issues.js +98 -0
  65. package/dist/cli/convoy/issues.js.map +1 -0
  66. package/dist/cli/convoy/issues.test.d.ts +2 -0
  67. package/dist/cli/convoy/issues.test.d.ts.map +1 -0
  68. package/dist/cli/convoy/issues.test.js +107 -0
  69. package/dist/cli/convoy/issues.test.js.map +1 -0
  70. package/dist/cli/convoy/knowledge.d.ts +5 -0
  71. package/dist/cli/convoy/knowledge.d.ts.map +1 -0
  72. package/dist/cli/convoy/knowledge.js +116 -0
  73. package/dist/cli/convoy/knowledge.js.map +1 -0
  74. package/dist/cli/convoy/knowledge.test.d.ts +2 -0
  75. package/dist/cli/convoy/knowledge.test.d.ts.map +1 -0
  76. package/dist/cli/convoy/knowledge.test.js +87 -0
  77. package/dist/cli/convoy/knowledge.test.js.map +1 -0
  78. package/dist/cli/convoy/lessons.d.ts +17 -0
  79. package/dist/cli/convoy/lessons.d.ts.map +1 -0
  80. package/dist/cli/convoy/lessons.js +149 -0
  81. package/dist/cli/convoy/lessons.js.map +1 -0
  82. package/dist/cli/convoy/lessons.test.d.ts +2 -0
  83. package/dist/cli/convoy/lessons.test.d.ts.map +1 -0
  84. package/dist/cli/convoy/lessons.test.js +135 -0
  85. package/dist/cli/convoy/lessons.test.js.map +1 -0
  86. package/dist/cli/convoy/lock.d.ts +13 -0
  87. package/dist/cli/convoy/lock.d.ts.map +1 -0
  88. package/dist/cli/convoy/lock.js +88 -0
  89. package/dist/cli/convoy/lock.js.map +1 -0
  90. package/dist/cli/convoy/lock.test.d.ts +2 -0
  91. package/dist/cli/convoy/lock.test.d.ts.map +1 -0
  92. package/dist/cli/convoy/lock.test.js +136 -0
  93. package/dist/cli/convoy/lock.test.js.map +1 -0
  94. package/dist/cli/convoy/log-merge.test.d.ts +2 -0
  95. package/dist/cli/convoy/log-merge.test.d.ts.map +1 -0
  96. package/dist/cli/convoy/log-merge.test.js +147 -0
  97. package/dist/cli/convoy/log-merge.test.js.map +1 -0
  98. package/dist/cli/convoy/merge.d.ts +4 -0
  99. package/dist/cli/convoy/merge.d.ts.map +1 -1
  100. package/dist/cli/convoy/merge.js +18 -1
  101. package/dist/cli/convoy/merge.js.map +1 -1
  102. package/dist/cli/convoy/merge.test.js +6 -7
  103. package/dist/cli/convoy/merge.test.js.map +1 -1
  104. package/dist/cli/convoy/partition.d.ts +51 -0
  105. package/dist/cli/convoy/partition.d.ts.map +1 -0
  106. package/dist/cli/convoy/partition.js +186 -0
  107. package/dist/cli/convoy/partition.js.map +1 -0
  108. package/dist/cli/convoy/partition.test.d.ts +2 -0
  109. package/dist/cli/convoy/partition.test.d.ts.map +1 -0
  110. package/dist/cli/convoy/partition.test.js +315 -0
  111. package/dist/cli/convoy/partition.test.js.map +1 -0
  112. package/dist/cli/convoy/pipeline.test.js +6 -0
  113. package/dist/cli/convoy/pipeline.test.js.map +1 -1
  114. package/dist/cli/convoy/store.d.ts +99 -7
  115. package/dist/cli/convoy/store.d.ts.map +1 -1
  116. package/dist/cli/convoy/store.js +764 -31
  117. package/dist/cli/convoy/store.js.map +1 -1
  118. package/dist/cli/convoy/store.test.js +1810 -18
  119. package/dist/cli/convoy/store.test.js.map +1 -1
  120. package/dist/cli/convoy/types.d.ts +427 -5
  121. package/dist/cli/convoy/types.d.ts.map +1 -1
  122. package/dist/cli/convoy/types.js +42 -1
  123. package/dist/cli/convoy/types.js.map +1 -1
  124. package/dist/cli/log.d.ts +11 -0
  125. package/dist/cli/log.d.ts.map +1 -1
  126. package/dist/cli/log.js +114 -2
  127. package/dist/cli/log.js.map +1 -1
  128. package/dist/cli/run/adapters/claude.d.ts +2 -0
  129. package/dist/cli/run/adapters/claude.d.ts.map +1 -1
  130. package/dist/cli/run/adapters/claude.js +89 -49
  131. package/dist/cli/run/adapters/claude.js.map +1 -1
  132. package/dist/cli/run/adapters/claude.test.d.ts +2 -0
  133. package/dist/cli/run/adapters/claude.test.d.ts.map +1 -0
  134. package/dist/cli/run/adapters/claude.test.js +205 -0
  135. package/dist/cli/run/adapters/claude.test.js.map +1 -0
  136. package/dist/cli/run/adapters/copilot.d.ts +1 -0
  137. package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
  138. package/dist/cli/run/adapters/copilot.js +84 -46
  139. package/dist/cli/run/adapters/copilot.js.map +1 -1
  140. package/dist/cli/run/adapters/copilot.test.d.ts +2 -0
  141. package/dist/cli/run/adapters/copilot.test.d.ts.map +1 -0
  142. package/dist/cli/run/adapters/copilot.test.js +195 -0
  143. package/dist/cli/run/adapters/copilot.test.js.map +1 -0
  144. package/dist/cli/run/adapters/cursor.d.ts +1 -0
  145. package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
  146. package/dist/cli/run/adapters/cursor.js +83 -47
  147. package/dist/cli/run/adapters/cursor.js.map +1 -1
  148. package/dist/cli/run/adapters/cursor.test.d.ts +2 -0
  149. package/dist/cli/run/adapters/cursor.test.d.ts.map +1 -0
  150. package/dist/cli/run/adapters/cursor.test.js +129 -0
  151. package/dist/cli/run/adapters/cursor.test.js.map +1 -0
  152. package/dist/cli/run/adapters/opencode.d.ts +1 -0
  153. package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
  154. package/dist/cli/run/adapters/opencode.js +81 -47
  155. package/dist/cli/run/adapters/opencode.js.map +1 -1
  156. package/dist/cli/run/adapters/opencode.test.d.ts +2 -0
  157. package/dist/cli/run/adapters/opencode.test.d.ts.map +1 -0
  158. package/dist/cli/run/adapters/opencode.test.js +119 -0
  159. package/dist/cli/run/adapters/opencode.test.js.map +1 -0
  160. package/dist/cli/run/executor.js +1 -1
  161. package/dist/cli/run/executor.js.map +1 -1
  162. package/dist/cli/run/schema.d.ts.map +1 -1
  163. package/dist/cli/run/schema.js +245 -4
  164. package/dist/cli/run/schema.js.map +1 -1
  165. package/dist/cli/run/schema.test.js +669 -0
  166. package/dist/cli/run/schema.test.js.map +1 -1
  167. package/dist/cli/run.d.ts.map +1 -1
  168. package/dist/cli/run.js +362 -22
  169. package/dist/cli/run.js.map +1 -1
  170. package/dist/cli/types.d.ts +85 -2
  171. package/dist/cli/types.d.ts.map +1 -1
  172. package/dist/cli/types.js.map +1 -1
  173. package/dist/cli/watch.d.ts +15 -0
  174. package/dist/cli/watch.d.ts.map +1 -0
  175. package/dist/cli/watch.js +279 -0
  176. package/dist/cli/watch.js.map +1 -0
  177. package/package.json +5 -1
  178. package/src/cli/agents.ts +177 -0
  179. package/src/cli/baselines.ts +143 -0
  180. package/src/cli/convoy/TELEMETRY.md +203 -0
  181. package/src/cli/convoy/dashboard-types.ts +141 -0
  182. package/src/cli/convoy/engine.test.ts +1937 -70
  183. package/src/cli/convoy/engine.ts +2350 -40
  184. package/src/cli/convoy/event-schemas.ts +195 -0
  185. package/src/cli/convoy/events.test.ts +384 -39
  186. package/src/cli/convoy/events.ts +202 -16
  187. package/src/cli/convoy/expertise.test.ts +128 -0
  188. package/src/cli/convoy/expertise.ts +163 -0
  189. package/src/cli/convoy/export.test.ts +1 -0
  190. package/src/cli/convoy/formula.test.ts +405 -0
  191. package/src/cli/convoy/formula.ts +174 -0
  192. package/src/cli/convoy/gates.test.ts +1169 -0
  193. package/src/cli/convoy/gates.ts +774 -0
  194. package/src/cli/convoy/health.test.ts +64 -2
  195. package/src/cli/convoy/health.ts +80 -2
  196. package/src/cli/convoy/issues.test.ts +143 -0
  197. package/src/cli/convoy/issues.ts +136 -0
  198. package/src/cli/convoy/knowledge.test.ts +101 -0
  199. package/src/cli/convoy/knowledge.ts +132 -0
  200. package/src/cli/convoy/lessons.test.ts +188 -0
  201. package/src/cli/convoy/lessons.ts +164 -0
  202. package/src/cli/convoy/lock.test.ts +181 -0
  203. package/src/cli/convoy/lock.ts +103 -0
  204. package/src/cli/convoy/log-merge.test.ts +179 -0
  205. package/src/cli/convoy/merge.test.ts +6 -7
  206. package/src/cli/convoy/merge.ts +19 -1
  207. package/src/cli/convoy/partition.test.ts +423 -0
  208. package/src/cli/convoy/partition.ts +232 -0
  209. package/src/cli/convoy/pipeline.test.ts +6 -0
  210. package/src/cli/convoy/store.test.ts +2041 -20
  211. package/src/cli/convoy/store.ts +945 -46
  212. package/src/cli/convoy/types.ts +278 -4
  213. package/src/cli/log.ts +120 -2
  214. package/src/cli/run/adapters/claude.test.ts +234 -0
  215. package/src/cli/run/adapters/claude.ts +45 -5
  216. package/src/cli/run/adapters/copilot.test.ts +224 -0
  217. package/src/cli/run/adapters/copilot.ts +34 -4
  218. package/src/cli/run/adapters/cursor.test.ts +144 -0
  219. package/src/cli/run/adapters/cursor.ts +33 -2
  220. package/src/cli/run/adapters/opencode.test.ts +135 -0
  221. package/src/cli/run/adapters/opencode.ts +30 -2
  222. package/src/cli/run/executor.ts +1 -1
  223. package/src/cli/run/schema.test.ts +758 -0
  224. package/src/cli/run/schema.ts +300 -25
  225. package/src/cli/run.ts +341 -21
  226. package/src/cli/types.ts +86 -1
  227. package/src/cli/watch.ts +298 -0
  228. package/src/dashboard/dist/_astro/{index.DtnyD8a5.css → index.6L3_HsPT.css} +1 -1
  229. package/src/dashboard/dist/data/.gitkeep +0 -0
  230. package/src/dashboard/dist/data/convoy-list.json +1 -0
  231. package/src/dashboard/dist/data/overall-stats.json +24 -0
  232. package/src/dashboard/dist/index.html +701 -3
  233. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  234. package/src/dashboard/public/data/.gitkeep +0 -0
  235. package/src/dashboard/public/data/convoy-list.json +1 -0
  236. package/src/dashboard/public/data/overall-stats.json +24 -0
  237. package/src/dashboard/scripts/etl.test.ts +210 -0
  238. package/src/dashboard/scripts/etl.ts +108 -0
  239. package/src/dashboard/scripts/integration-test.ts +504 -0
  240. package/src/dashboard/src/pages/index.astro +854 -15
  241. package/src/dashboard/src/styles/dashboard.css +557 -1
  242. package/src/orchestrator/prompts/generate-convoy.prompt.md +212 -13
package/src/cli/run.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises'
2
2
  import { existsSync } from 'node:fs'
3
3
  import { resolve } from 'node:path'
4
+ import { stringify as yamlStringify } from 'yaml'
4
5
  import { parseTaskSpecText, isConvoySpec, isPipelineSpec } from './run/schema.js'
5
6
  import { createExecutor, buildPhases } from './run/executor.js'
6
7
  import { getAdapter, detectAdapter } from './run/adapters/index.js'
@@ -9,6 +10,7 @@ import { c } from './prompt.js'
9
10
  import type { CliContext, RunOptions } from './types.js'
10
11
  import type { ConvoyResult } from './convoy/engine.js'
11
12
  import type { PipelineResult } from './convoy/pipeline.js'
13
+ import { EngineAlreadyRunningError } from './convoy/lock.js'
12
14
 
13
15
  function formatTokens(n: number): string {
14
16
  if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M'
@@ -24,13 +26,24 @@ const HELP = `
24
26
 
25
27
  Options:
26
28
  --file, -f <path> Task spec file
29
+ --formula <path> Use a formula template (alternative to --file)
30
+ --set key=value Set a formula variable (repeatable)
27
31
  --dry-run Show execution plan without running
28
32
  --concurrency, -c <n> Override max parallel tasks
29
33
  --adapter, -a <name> Override agent runtime adapter
30
34
  --report-dir <path> Where to write run reports (default: .opencastle/runs)
31
35
  --verbose Show full agent output
32
36
  --resume Resume the last interrupted convoy from .opencastle/convoy.db
37
+ --retry-failed [task-id] Retry failed/gate-failed/timed-out tasks from the last convoy
33
38
  --status Print the current convoy state from .opencastle/convoy.db
39
+ --dlq-list List dead letter queue entries
40
+ --dlq-resolve <id> Resolve a DLQ entry (requires --resolution)
41
+ --dlq-retry <id> Reset a DLQ task to pending for retry
42
+ --convoy <id> Filter by convoy ID (used with --dlq-list)
43
+ --resolution <text> Resolution text (used with --dlq-resolve)
44
+ --watch Keep running, re-triggering on file changes, cron, or git push
45
+ --watch-config <p> Path to watch configuration file (overrides spec watch config)
46
+ --clear-scratchpad Clear scratchpad data at watch start
34
47
  --help, -h Show this help
35
48
  `
36
49
 
@@ -48,6 +61,20 @@ function parseArgs(args: string[]): RunOptions {
48
61
  help: false,
49
62
  resume: false,
50
63
  status: false,
64
+ retryFailed: false,
65
+ retryFailedTaskIds: undefined,
66
+ dlqList: false,
67
+ dlqResolve: false,
68
+ dlqResolveId: undefined,
69
+ dlqResolveText: undefined,
70
+ dlqRetry: false,
71
+ dlqRetryId: undefined,
72
+ dlqConvoyFilter: undefined,
73
+ formula: null,
74
+ setVars: {},
75
+ watch: false,
76
+ watchConfig: null,
77
+ clearScratchpad: false,
51
78
  }
52
79
 
53
80
  for (let i = 0; i < args.length; i++) {
@@ -92,9 +119,59 @@ function parseArgs(args: string[]): RunOptions {
92
119
  case '--resume':
93
120
  opts.resume = true
94
121
  break
122
+ case '--retry-failed':
123
+ opts.retryFailed = true
124
+ if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
125
+ opts.retryFailedTaskIds = [args[++i]]
126
+ }
127
+ break
95
128
  case '--status':
96
129
  opts.status = true
97
130
  break
131
+ case '--dlq-list':
132
+ opts.dlqList = true
133
+ break
134
+ case '--dlq-resolve':
135
+ opts.dlqResolve = true
136
+ if (i + 1 < args.length && !args[i + 1].startsWith('--')) opts.dlqResolveId = args[++i]
137
+ break
138
+ case '--dlq-retry':
139
+ opts.dlqRetry = true
140
+ if (i + 1 < args.length && !args[i + 1].startsWith('--')) opts.dlqRetryId = args[++i]
141
+ break
142
+ case '--resolution':
143
+ if (i + 1 >= args.length) { console.error(' ✗ --resolution requires text'); process.exit(1) }
144
+ opts.dlqResolveText = args[++i]
145
+ break
146
+ case '--convoy':
147
+ if (i + 1 >= args.length) { console.error(' ✗ --convoy requires an ID'); process.exit(1) }
148
+ opts.dlqConvoyFilter = args[++i]
149
+ break
150
+ case '--formula':
151
+ if (i + 1 >= args.length) { console.error(' ✗ --formula requires a path'); process.exit(1) }
152
+ opts.formula = args[++i]
153
+ break
154
+ case '--set': {
155
+ if (i + 1 >= args.length) { console.error(' ✗ --set requires key=value'); process.exit(1) }
156
+ const pair = args[++i]
157
+ const eqIdx = pair.indexOf('=')
158
+ if (eqIdx < 1) {
159
+ console.error(` ✗ --set value must be in key=value format, got: ${pair}`)
160
+ process.exit(1)
161
+ }
162
+ opts.setVars[pair.slice(0, eqIdx)] = pair.slice(eqIdx + 1)
163
+ break
164
+ }
165
+ case '--watch':
166
+ opts.watch = true
167
+ break
168
+ case '--watch-config':
169
+ if (i + 1 >= args.length) { console.error(' ✗ --watch-config requires a path'); process.exit(1) }
170
+ opts.watchConfig = args[++i]
171
+ break
172
+ case '--clear-scratchpad':
173
+ opts.clearScratchpad = true
174
+ break
98
175
  default:
99
176
  console.error(` ✗ Unknown option: ${arg}`)
100
177
  console.log(HELP)
@@ -205,6 +282,99 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
205
282
 
206
283
  const dbPath = resolve(process.cwd(), '.opencastle', 'convoy.db')
207
284
 
285
+ // ── --dlq-list flag ───────────────────────────────────────────
286
+ if (opts.dlqList) {
287
+ if (!existsSync(dbPath)) {
288
+ console.log(' No convoy database found at .opencastle/convoy.db')
289
+ return
290
+ }
291
+ const { createConvoyStore } = await import('./convoy/store.js')
292
+ const store = createConvoyStore(dbPath)
293
+ try {
294
+ const entries = store.listDlqEntries(opts.dlqConvoyFilter)
295
+ if (entries.length === 0) {
296
+ console.log(' No DLQ entries found.')
297
+ return
298
+ }
299
+ console.log(`\n Dead Letter Queue (${entries.length} entries):\n`)
300
+ for (const e of entries) {
301
+ const status = e.resolved ? c.green('resolved') : c.red('unresolved')
302
+ console.log(` ${e.id} ${status}`)
303
+ console.log(` Task: ${e.task_id} | Agent: ${e.agent} | Type: ${e.failure_type}`)
304
+ console.log(` Attempts: ${e.attempts} | Created: ${e.created_at}`)
305
+ if (e.resolution) console.log(` Resolution: ${e.resolution}`)
306
+ console.log()
307
+ }
308
+ } finally {
309
+ store.close()
310
+ }
311
+ return
312
+ }
313
+
314
+ // ── --dlq-resolve flag ────────────────────────────────────────
315
+ if (opts.dlqResolve) {
316
+ if (!opts.dlqResolveId) {
317
+ console.error(' \u2717 --dlq-resolve requires a DLQ entry ID')
318
+ process.exit(1)
319
+ }
320
+ if (!opts.dlqResolveText) {
321
+ console.error(' \u2717 --dlq-resolve requires --resolution "text"')
322
+ process.exit(1)
323
+ }
324
+ if (!existsSync(dbPath)) {
325
+ console.error(' \u2717 No convoy database found at .opencastle/convoy.db')
326
+ process.exit(1)
327
+ }
328
+ const { createConvoyStore } = await import('./convoy/store.js')
329
+ const store = createConvoyStore(dbPath)
330
+ try {
331
+ store.resolveDlqEntry(opts.dlqResolveId, opts.dlqResolveText)
332
+ console.log(` \u2713 DLQ entry ${opts.dlqResolveId} resolved.`)
333
+ } finally {
334
+ store.close()
335
+ }
336
+ return
337
+ }
338
+
339
+ // ── --dlq-retry flag ──────────────────────────────────────────
340
+ if (opts.dlqRetry) {
341
+ if (!opts.dlqRetryId) {
342
+ console.error(' \u2717 --dlq-retry requires a DLQ entry ID')
343
+ process.exit(1)
344
+ }
345
+ if (!existsSync(dbPath)) {
346
+ console.error(' \u2717 No convoy database found at .opencastle/convoy.db')
347
+ process.exit(1)
348
+ }
349
+ const { createConvoyStore } = await import('./convoy/store.js')
350
+ const store = createConvoyStore(dbPath)
351
+ try {
352
+ const entries = store.listDlqEntries()
353
+ const entry = entries.find(e => e.id === opts.dlqRetryId)
354
+ if (!entry) {
355
+ console.error(` \u2717 DLQ entry "${opts.dlqRetryId}" not found`)
356
+ process.exit(1)
357
+ }
358
+ // Reset the task to pending
359
+ store.updateTaskStatus(entry.task_id, entry.convoy_id, 'pending', {
360
+ worker_id: null,
361
+ worktree: null,
362
+ started_at: null,
363
+ finished_at: null,
364
+ })
365
+ store.resolveDlqEntry(entry.id, 'Retried via CLI')
366
+ // Reset convoy status to running if needed
367
+ const convoy = store.getConvoy(entry.convoy_id)
368
+ if (convoy && (convoy.status === 'failed' || convoy.status === 'done')) {
369
+ store.updateConvoyStatus(entry.convoy_id, 'running', {})
370
+ }
371
+ console.log(` \u2713 Task ${entry.task_id} reset to pending. Run 'opencastle run --resume' to execute.`)
372
+ } finally {
373
+ store.close()
374
+ }
375
+ return
376
+ }
377
+
208
378
  // ── --status flag ─────────────────────────────────────────────
209
379
  if (opts.status) {
210
380
  if (!existsSync(dbPath)) {
@@ -288,6 +458,70 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
288
458
  return
289
459
  }
290
460
 
461
+ // ── --retry-failed flag ───────────────────────────────────────
462
+ if (opts.retryFailed) {
463
+ if (!existsSync(dbPath)) {
464
+ console.error(' ✗ No convoy database found at .opencastle/convoy.db')
465
+ console.error(' Run a convoy spec first: opencastle run convoy.yml')
466
+ process.exit(1)
467
+ }
468
+ const { createConvoyStore } = await import('./convoy/store.js')
469
+ const store = createConvoyStore(dbPath)
470
+ const convoy = store.getLatestConvoy()
471
+ store.close()
472
+ if (!convoy) {
473
+ console.error(' ✗ No convoy records found in .opencastle/convoy.db')
474
+ process.exit(1)
475
+ }
476
+
477
+ const retrySpec = parseTaskSpecText(convoy.spec_yaml)
478
+ if (opts.concurrency !== null) retrySpec.concurrency = opts.concurrency
479
+ if (opts.adapter !== null) retrySpec.adapter = opts.adapter
480
+ if (opts.verbose) retrySpec._verbose = true
481
+
482
+ let retryDetectionFailed = false
483
+ if (!retrySpec.adapter) {
484
+ const detected = await detectAdapter()
485
+ if (detected) {
486
+ retrySpec.adapter = detected
487
+ console.log(` ℹ Auto-detected adapter: ${detected}`)
488
+ } else {
489
+ retryDetectionFailed = true
490
+ retrySpec.adapter = 'claude'
491
+ }
492
+ }
493
+
494
+ const retryAdapter = await getAdapter(retrySpec.adapter)
495
+ const retryAvailable = await retryAdapter.isAvailable()
496
+ if (!retryAvailable) {
497
+ printAdapterError(retryDetectionFailed, retrySpec.adapter)
498
+ process.exit(1)
499
+ }
500
+
501
+ console.log(`\n 🏰 OpenCastle Convoy (Retry Failed): ${convoy.name}`)
502
+ console.log(` Convoy ID: ${convoy.id}`)
503
+ const { createConvoyEngine } = await import('./convoy/engine.js')
504
+ const retryEngine = createConvoyEngine({
505
+ spec: retrySpec,
506
+ specYaml: convoy.spec_yaml,
507
+ adapter: retryAdapter,
508
+ verbose: opts.verbose,
509
+ })
510
+ await retryEngine.retryFailed(convoy.id, opts.retryFailedTaskIds)
511
+ let retryResult: ConvoyResult
512
+ try {
513
+ retryResult = await retryEngine.resume(convoy.id)
514
+ } catch (err) {
515
+ if (err instanceof EngineAlreadyRunningError) {
516
+ console.error(` ✗ ${err.message}`)
517
+ process.exit(1)
518
+ }
519
+ throw err
520
+ }
521
+ printConvoyResult(retryResult)
522
+ process.exit(retryResult.status !== 'done' ? 1 : 0)
523
+ }
524
+
291
525
  // ── --resume flag ─────────────────────────────────────────────
292
526
  if (opts.resume) {
293
527
  if (!existsSync(dbPath)) {
@@ -385,32 +619,83 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
385
619
  adapter: resumeAdapter,
386
620
  verbose: opts.verbose,
387
621
  })
388
- const resumeResult = await resumeEngine.resume(convoy.id)
622
+ let resumeResult: ConvoyResult
623
+ try {
624
+ resumeResult = await resumeEngine.resume(convoy.id)
625
+ } catch (err) {
626
+ if (err instanceof EngineAlreadyRunningError) {
627
+ console.error(` ✗ ${err.message}`)
628
+ process.exit(1)
629
+ }
630
+ throw err
631
+ }
389
632
  printConvoyResult(resumeResult)
390
633
  process.exit(resumeResult.status !== 'done' ? 1 : 0)
391
634
  }
392
635
 
393
- // ── Read and validate spec ────────────────────────────────────
394
- const specPath = resolve(process.cwd(), opts.file)
636
+ // ── Formula template resolution / Read and validate spec ─────
395
637
  let specText = ''
396
- try {
397
- specText = await readFile(specPath, 'utf8')
398
- } catch (err: unknown) {
399
- const e = err as Error & { code?: string }
400
- if (e.code === 'ENOENT') {
401
- console.error(` ✗ Task spec file not found: ${opts.file}`)
402
- } else {
403
- console.error(` ✗ Cannot read task spec file: ${e.message}`)
638
+ let spec: ReturnType<typeof parseTaskSpecText>
639
+
640
+ if (opts.formula) {
641
+ const { parseFormula, substituteVariables, validateTemplate } = await import('./convoy/formula.js')
642
+ const formulaPath = resolve(process.cwd(), opts.formula)
643
+ let template
644
+ try {
645
+ template = parseFormula(formulaPath)
646
+ } catch (err: unknown) {
647
+ console.error(` ✗ ${(err as Error).message}`)
648
+ process.exit(1)
404
649
  }
405
- process.exit(1)
406
- }
407
650
 
408
- let spec
409
- try {
410
- spec = parseTaskSpecText(specText)
411
- } catch (err: unknown) {
412
- console.error(` ✗ ${(err as Error).message}`)
413
- process.exit(1)
651
+ const validation = validateTemplate(template)
652
+ if (!validation.valid) {
653
+ console.error(` ✗ Invalid formula template:\n • ${validation.errors.join('\n • ')}`)
654
+ process.exit(1)
655
+ }
656
+
657
+ if (opts.dryRun) {
658
+ console.log(`\n 📋 Formula: ${template.name}`)
659
+ if (template.description) console.log(` ${template.description}`)
660
+ console.log(` Variables:`)
661
+ for (const [key, val] of Object.entries(opts.setVars)) {
662
+ console.log(` ${key} = ${val}`)
663
+ }
664
+ for (const [key, def] of Object.entries(template.variables)) {
665
+ if (!(key in opts.setVars) && !def.required && def.default) {
666
+ console.log(` ${key} = ${def.default} (default)`)
667
+ }
668
+ }
669
+ }
670
+
671
+ try {
672
+ spec = substituteVariables(template, opts.setVars)
673
+ } catch (err: unknown) {
674
+ console.error(` ✗ ${(err as Error).message}`)
675
+ process.exit(1)
676
+ }
677
+ specText = yamlStringify(spec)
678
+ } else {
679
+ // ── Read and validate spec ──────────────────────────────────
680
+ const specPath = resolve(process.cwd(), opts.file)
681
+ try {
682
+ specText = await readFile(specPath, 'utf8')
683
+ } catch (err: unknown) {
684
+ const e = err as Error & { code?: string }
685
+ if (e.code === 'ENOENT') {
686
+ console.error(` ✗ Task spec file not found: ${opts.file}`)
687
+ } else {
688
+ console.error(` ✗ Cannot read task spec file: ${e.message}`)
689
+ }
690
+ process.exit(1)
691
+ }
692
+
693
+ try {
694
+ spec = parseTaskSpecText(specText)
695
+ } catch (err: unknown) {
696
+ console.error(` ✗ ${(err as Error).message}`)
697
+ process.exit(1)
698
+ }
414
699
  }
415
700
 
416
701
  // Apply CLI overrides
@@ -493,7 +778,16 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
493
778
  verbose: opts.verbose,
494
779
  })
495
780
 
496
- const pipelineResult = await pipelineOrchestrator.run()
781
+ let pipelineResult: PipelineResult
782
+ try {
783
+ pipelineResult = await pipelineOrchestrator.run()
784
+ } catch (err) {
785
+ if (err instanceof EngineAlreadyRunningError) {
786
+ console.error(` ✗ ${err.message}`)
787
+ process.exit(1)
788
+ }
789
+ throw err
790
+ }
497
791
  printPipelineResult(pipelineResult)
498
792
  if (pipelineDashboardResult) {
499
793
  console.log(`\n ${c.dim('Results saved to .opencastle/logs/convoys.ndjson')}`)
@@ -535,7 +829,33 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
535
829
  verbose: opts.verbose,
536
830
  })
537
831
 
538
- const result = await engine.run()
832
+ if (opts.watch) {
833
+ const pidPath = resolve(process.cwd(), '.opencastle', 'watch.pid')
834
+ const { watchLoop } = await import('./watch.js')
835
+ await watchLoop({
836
+ spec,
837
+ specText,
838
+ specPath: resolve(process.cwd(), opts.file),
839
+ adapter,
840
+ verbose: opts.verbose,
841
+ pidPath,
842
+ clearScratchpad: opts.clearScratchpad,
843
+ watchConfigPath: opts.watchConfig ? resolve(process.cwd(), opts.watchConfig) : null,
844
+ printResult: printConvoyResult,
845
+ })
846
+ return
847
+ }
848
+
849
+ let result: ConvoyResult
850
+ try {
851
+ result = await engine.run()
852
+ } catch (err) {
853
+ if (err instanceof EngineAlreadyRunningError) {
854
+ console.error(` ✗ ${err.message}`)
855
+ process.exit(1)
856
+ }
857
+ throw err
858
+ }
539
859
  printConvoyResult(result)
540
860
  if (dashboardResult) {
541
861
  console.log(`\n ${c.dim('Results saved to .opencastle/logs/convoys.ndjson')}`)
package/src/cli/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ChildProcess } from 'node:child_process';
2
+ import type { BuiltInGatesConfig, BrowserTestConfig, GuardConfig, CircuitBreakerConfig, TaskStep, Hook, TaskOutput, TaskInput, WatchConfig, MCPServerConfig } from './convoy/types.js';
2
3
 
3
4
  // ── Stack selection types ──────────────────────────────────────
4
5
 
@@ -141,6 +142,15 @@ export const IDE_LABELS: Record<IdeChoice, string> = {
141
142
 
142
143
  // ── Run command types ──────────────────────────────────────────
143
144
 
145
+ /** Heuristics for routing tasks to review levels. */
146
+ export interface ReviewHeuristics {
147
+ panel_paths?: string[];
148
+ panel_agents?: string[];
149
+ auto_pass_agents?: string[];
150
+ auto_pass_max_lines?: number;
151
+ auto_pass_max_files?: number;
152
+ }
153
+
144
154
  /** Default values merged into each task for Convoy Engine (version: 1) specs. */
145
155
  export interface TaskDefaults {
146
156
  timeout?: string;
@@ -148,12 +158,42 @@ export interface TaskDefaults {
148
158
  max_retries?: number;
149
159
  agent?: string;
150
160
  adapter?: string;
161
+ gates?: string[];
162
+ built_in_gates?: BuiltInGatesConfig;
163
+ gate_timeout?: number;
164
+ on_exhausted?: 'dlq' | 'skip' | 'stop';
165
+ escalate_to?: string;
166
+ circuit_breaker?: CircuitBreakerConfig;
167
+ review?: 'auto' | 'fast' | 'panel' | 'none';
168
+ reviewer_model?: string;
169
+ review_budget?: number;
170
+ on_review_budget_exceeded?: 'skip' | 'downgrade' | 'stop';
171
+ max_concurrent_reviews?: number;
172
+ review_heuristics?: ReviewHeuristics;
173
+ detect_drift?: boolean;
174
+ on_dispute?: 'continue' | 'stop';
175
+ /** Enable automated lesson injection into task prompts (Phase 18.1). */
176
+ inject_lessons?: boolean;
177
+ /** Enable discovered issues tracking in task prompts (Phase 18.4). */
178
+ track_discovered_issues?: boolean;
179
+ /** Skip assigning agent to tasks matching their weak areas (Phase 18.2). */
180
+ avoid_weak_agents?: boolean;
181
+ /** Maximum concurrent tasks in swarm mode (default: 8). */
182
+ max_swarm_concurrency?: number;
183
+ /** MCP servers available to tasks (Phase 19.7). */
184
+ mcp_servers?: MCPServerConfig[];
185
+ /** Auto-approve all MCP tool calls without prompting (Phase 19.7). */
186
+ mcp_approve_all?: boolean;
187
+ /** Timeout in seconds for MCP server approval prompts (Phase 19.7). */
188
+ mcp_server_approval_timeout?: number;
189
+ /** Browser test gate configuration for default built-in gates. */
190
+ browser_test?: BrowserTestConfig;
151
191
  }
152
192
 
153
193
  /** Validated task spec from YAML. */
154
194
  export interface TaskSpec {
155
195
  name: string;
156
- concurrency: number;
196
+ concurrency: number | 'auto';
157
197
  on_failure: 'continue' | 'stop';
158
198
  adapter: string;
159
199
  tasks?: Task[];
@@ -170,6 +210,12 @@ export interface TaskSpec {
170
210
  branch?: string;
171
211
  /** Other convoy spec names to run before this one (version: 2 pipeline specs). */
172
212
  depends_on_convoy?: string[];
213
+ /** Optional post-convoy guard configuration. */
214
+ guard?: GuardConfig;
215
+ /** Post-convoy lifecycle hooks. */
216
+ hooks?: Hook[];
217
+ /** Watch mode configuration (Phase 17.1). */
218
+ watch?: WatchConfig;
173
219
  }
174
220
 
175
221
  /** A single task in the spec. */
@@ -188,6 +234,24 @@ export interface Task {
188
234
  max_retries: number;
189
235
  /** Per-task adapter override. */
190
236
  adapter?: string;
237
+ /** Per-task gate shell commands run after adapter success. */
238
+ gates?: string[];
239
+ /** Multi-step task sub-prompts. */
240
+ steps?: TaskStep[];
241
+ /** Review level override for this task. */
242
+ review?: 'auto' | 'fast' | 'panel' | 'none';
243
+ /** Lifecycle hooks for this task. */
244
+ hooks?: Hook[];
245
+ /** Opt-in drift detection (streaming adapters only). */
246
+ detect_drift?: boolean;
247
+ /** Outputs this task produces as named artifacts. */
248
+ outputs?: TaskOutput[];
249
+ /** Inputs this task consumes from upstream task artifacts. */
250
+ inputs?: TaskInput[];
251
+ /** Whether this task has persistent agent identity (Phase 17.2). */
252
+ persistent?: boolean;
253
+ /** Browser test gate configuration for this task. */
254
+ browser_test?: BrowserTestConfig;
191
255
  }
192
256
 
193
257
  /** Task execution status. */
@@ -196,6 +260,7 @@ export type TaskStatus =
196
260
  | 'running'
197
261
  | 'done'
198
262
  | 'failed'
263
+ | 'gate-failed'
199
264
  | 'skipped'
200
265
  | 'timed-out';
201
266
 
@@ -233,6 +298,8 @@ export interface AgentAdapter {
233
298
  isAvailable(): Promise<boolean>;
234
299
  execute(_task: Task, _options?: ExecuteOptions): Promise<ExecuteResult>;
235
300
  kill?(_task: Task): void;
301
+ /** Whether the adapter supports reusing sessions across multi-step task steps. Defaults to false. */
302
+ supportsSessionContinuity?(): boolean;
236
303
  }
237
304
 
238
305
  /** Options for agent execution. */
@@ -240,6 +307,10 @@ export interface ExecuteOptions {
240
307
  verbose?: boolean;
241
308
  /** Working directory for the agent process (defaults to process.cwd()). */
242
309
  cwd?: string;
310
+ /** MCP servers to make available during execution (Phase 19.7). */
311
+ mcpServers?: MCPServerConfig[];
312
+ /** Automatically approve all MCP permission requests. */
313
+ mcp_approve_all?: boolean;
243
314
  }
244
315
 
245
316
  /** Token usage data from adapter execution. */
@@ -286,6 +357,20 @@ export interface RunOptions {
286
357
  help: boolean;
287
358
  resume: boolean;
288
359
  status: boolean;
360
+ retryFailed: boolean;
361
+ retryFailedTaskIds?: string[];
362
+ dlqList: boolean;
363
+ dlqResolve: boolean;
364
+ dlqResolveId?: string;
365
+ dlqResolveText?: string;
366
+ dlqRetry: boolean;
367
+ dlqRetryId?: string;
368
+ dlqConvoyFilter?: string;
369
+ formula: string | null;
370
+ setVars: Record<string, string>;
371
+ watch: boolean;
372
+ watchConfig: string | null;
373
+ clearScratchpad: boolean;
289
374
  }
290
375
 
291
376
  /** Validation result. */