opencastle 0.26.1 → 0.27.1

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 (226) hide show
  1. package/README.md +7 -1
  2. package/bin/cli.mjs +10 -0
  3. package/dist/cli/agents.d.ts +3 -0
  4. package/dist/cli/agents.d.ts.map +1 -0
  5. package/dist/cli/agents.js +161 -0
  6. package/dist/cli/agents.js.map +1 -0
  7. package/dist/cli/baselines.d.ts +3 -0
  8. package/dist/cli/baselines.d.ts.map +1 -0
  9. package/dist/cli/baselines.js +128 -0
  10. package/dist/cli/baselines.js.map +1 -0
  11. package/dist/cli/convoy/engine.d.ts +68 -2
  12. package/dist/cli/convoy/engine.d.ts.map +1 -1
  13. package/dist/cli/convoy/engine.js +2102 -26
  14. package/dist/cli/convoy/engine.js.map +1 -1
  15. package/dist/cli/convoy/engine.test.js +1572 -70
  16. package/dist/cli/convoy/engine.test.js.map +1 -1
  17. package/dist/cli/convoy/events.d.ts +4 -1
  18. package/dist/cli/convoy/events.d.ts.map +1 -1
  19. package/dist/cli/convoy/events.js +74 -13
  20. package/dist/cli/convoy/events.js.map +1 -1
  21. package/dist/cli/convoy/events.test.js +154 -27
  22. package/dist/cli/convoy/events.test.js.map +1 -1
  23. package/dist/cli/convoy/expertise.d.ts +16 -0
  24. package/dist/cli/convoy/expertise.d.ts.map +1 -0
  25. package/dist/cli/convoy/expertise.js +121 -0
  26. package/dist/cli/convoy/expertise.js.map +1 -0
  27. package/dist/cli/convoy/expertise.test.d.ts +2 -0
  28. package/dist/cli/convoy/expertise.test.d.ts.map +1 -0
  29. package/dist/cli/convoy/expertise.test.js +96 -0
  30. package/dist/cli/convoy/expertise.test.js.map +1 -0
  31. package/dist/cli/convoy/export.test.js +1 -0
  32. package/dist/cli/convoy/export.test.js.map +1 -1
  33. package/dist/cli/convoy/formula.d.ts +19 -0
  34. package/dist/cli/convoy/formula.d.ts.map +1 -0
  35. package/dist/cli/convoy/formula.js +142 -0
  36. package/dist/cli/convoy/formula.js.map +1 -0
  37. package/dist/cli/convoy/formula.test.d.ts +2 -0
  38. package/dist/cli/convoy/formula.test.d.ts.map +1 -0
  39. package/dist/cli/convoy/formula.test.js +342 -0
  40. package/dist/cli/convoy/formula.test.js.map +1 -0
  41. package/dist/cli/convoy/gates.d.ts +128 -0
  42. package/dist/cli/convoy/gates.d.ts.map +1 -0
  43. package/dist/cli/convoy/gates.js +606 -0
  44. package/dist/cli/convoy/gates.js.map +1 -0
  45. package/dist/cli/convoy/gates.test.d.ts +2 -0
  46. package/dist/cli/convoy/gates.test.d.ts.map +1 -0
  47. package/dist/cli/convoy/gates.test.js +976 -0
  48. package/dist/cli/convoy/gates.test.js.map +1 -0
  49. package/dist/cli/convoy/health.d.ts +11 -0
  50. package/dist/cli/convoy/health.d.ts.map +1 -1
  51. package/dist/cli/convoy/health.js +54 -0
  52. package/dist/cli/convoy/health.js.map +1 -1
  53. package/dist/cli/convoy/health.test.js +56 -1
  54. package/dist/cli/convoy/health.test.js.map +1 -1
  55. package/dist/cli/convoy/issues.d.ts +8 -0
  56. package/dist/cli/convoy/issues.d.ts.map +1 -0
  57. package/dist/cli/convoy/issues.js +98 -0
  58. package/dist/cli/convoy/issues.js.map +1 -0
  59. package/dist/cli/convoy/issues.test.d.ts +2 -0
  60. package/dist/cli/convoy/issues.test.d.ts.map +1 -0
  61. package/dist/cli/convoy/issues.test.js +107 -0
  62. package/dist/cli/convoy/issues.test.js.map +1 -0
  63. package/dist/cli/convoy/knowledge.d.ts +5 -0
  64. package/dist/cli/convoy/knowledge.d.ts.map +1 -0
  65. package/dist/cli/convoy/knowledge.js +116 -0
  66. package/dist/cli/convoy/knowledge.js.map +1 -0
  67. package/dist/cli/convoy/knowledge.test.d.ts +2 -0
  68. package/dist/cli/convoy/knowledge.test.d.ts.map +1 -0
  69. package/dist/cli/convoy/knowledge.test.js +87 -0
  70. package/dist/cli/convoy/knowledge.test.js.map +1 -0
  71. package/dist/cli/convoy/lessons.d.ts +17 -0
  72. package/dist/cli/convoy/lessons.d.ts.map +1 -0
  73. package/dist/cli/convoy/lessons.js +149 -0
  74. package/dist/cli/convoy/lessons.js.map +1 -0
  75. package/dist/cli/convoy/lessons.test.d.ts +2 -0
  76. package/dist/cli/convoy/lessons.test.d.ts.map +1 -0
  77. package/dist/cli/convoy/lessons.test.js +135 -0
  78. package/dist/cli/convoy/lessons.test.js.map +1 -0
  79. package/dist/cli/convoy/lock.d.ts +13 -0
  80. package/dist/cli/convoy/lock.d.ts.map +1 -0
  81. package/dist/cli/convoy/lock.js +88 -0
  82. package/dist/cli/convoy/lock.js.map +1 -0
  83. package/dist/cli/convoy/lock.test.d.ts +2 -0
  84. package/dist/cli/convoy/lock.test.d.ts.map +1 -0
  85. package/dist/cli/convoy/lock.test.js +136 -0
  86. package/dist/cli/convoy/lock.test.js.map +1 -0
  87. package/dist/cli/convoy/merge.d.ts +4 -0
  88. package/dist/cli/convoy/merge.d.ts.map +1 -1
  89. package/dist/cli/convoy/merge.js +18 -1
  90. package/dist/cli/convoy/merge.js.map +1 -1
  91. package/dist/cli/convoy/merge.test.js +6 -7
  92. package/dist/cli/convoy/merge.test.js.map +1 -1
  93. package/dist/cli/convoy/partition.d.ts +51 -0
  94. package/dist/cli/convoy/partition.d.ts.map +1 -0
  95. package/dist/cli/convoy/partition.js +186 -0
  96. package/dist/cli/convoy/partition.js.map +1 -0
  97. package/dist/cli/convoy/partition.test.d.ts +2 -0
  98. package/dist/cli/convoy/partition.test.d.ts.map +1 -0
  99. package/dist/cli/convoy/partition.test.js +315 -0
  100. package/dist/cli/convoy/partition.test.js.map +1 -0
  101. package/dist/cli/convoy/pipeline.test.js +6 -0
  102. package/dist/cli/convoy/pipeline.test.js.map +1 -1
  103. package/dist/cli/convoy/store.d.ts +47 -5
  104. package/dist/cli/convoy/store.d.ts.map +1 -1
  105. package/dist/cli/convoy/store.js +525 -19
  106. package/dist/cli/convoy/store.js.map +1 -1
  107. package/dist/cli/convoy/store.test.js +1345 -12
  108. package/dist/cli/convoy/store.test.js.map +1 -1
  109. package/dist/cli/convoy/types.d.ts +156 -2
  110. package/dist/cli/convoy/types.d.ts.map +1 -1
  111. package/dist/cli/destroy.d.ts +3 -0
  112. package/dist/cli/destroy.d.ts.map +1 -0
  113. package/dist/cli/destroy.js +69 -0
  114. package/dist/cli/destroy.js.map +1 -0
  115. package/dist/cli/destroy.test.d.ts +2 -0
  116. package/dist/cli/destroy.test.d.ts.map +1 -0
  117. package/dist/cli/destroy.test.js +116 -0
  118. package/dist/cli/destroy.test.js.map +1 -0
  119. package/dist/cli/gitignore.d.ts +9 -0
  120. package/dist/cli/gitignore.d.ts.map +1 -1
  121. package/dist/cli/gitignore.js +29 -0
  122. package/dist/cli/gitignore.js.map +1 -1
  123. package/dist/cli/plan.d.ts +3 -0
  124. package/dist/cli/plan.d.ts.map +1 -0
  125. package/dist/cli/plan.js +288 -0
  126. package/dist/cli/plan.js.map +1 -0
  127. package/dist/cli/run/adapters/claude.d.ts +2 -0
  128. package/dist/cli/run/adapters/claude.d.ts.map +1 -1
  129. package/dist/cli/run/adapters/claude.js +89 -49
  130. package/dist/cli/run/adapters/claude.js.map +1 -1
  131. package/dist/cli/run/adapters/claude.test.d.ts +2 -0
  132. package/dist/cli/run/adapters/claude.test.d.ts.map +1 -0
  133. package/dist/cli/run/adapters/claude.test.js +205 -0
  134. package/dist/cli/run/adapters/claude.test.js.map +1 -0
  135. package/dist/cli/run/adapters/copilot.d.ts +1 -0
  136. package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
  137. package/dist/cli/run/adapters/copilot.js +84 -46
  138. package/dist/cli/run/adapters/copilot.js.map +1 -1
  139. package/dist/cli/run/adapters/copilot.test.d.ts +2 -0
  140. package/dist/cli/run/adapters/copilot.test.d.ts.map +1 -0
  141. package/dist/cli/run/adapters/copilot.test.js +195 -0
  142. package/dist/cli/run/adapters/copilot.test.js.map +1 -0
  143. package/dist/cli/run/adapters/cursor.d.ts +1 -0
  144. package/dist/cli/run/adapters/cursor.d.ts.map +1 -1
  145. package/dist/cli/run/adapters/cursor.js +83 -47
  146. package/dist/cli/run/adapters/cursor.js.map +1 -1
  147. package/dist/cli/run/adapters/cursor.test.d.ts +2 -0
  148. package/dist/cli/run/adapters/cursor.test.d.ts.map +1 -0
  149. package/dist/cli/run/adapters/cursor.test.js +129 -0
  150. package/dist/cli/run/adapters/cursor.test.js.map +1 -0
  151. package/dist/cli/run/adapters/opencode.d.ts +1 -0
  152. package/dist/cli/run/adapters/opencode.d.ts.map +1 -1
  153. package/dist/cli/run/adapters/opencode.js +81 -47
  154. package/dist/cli/run/adapters/opencode.js.map +1 -1
  155. package/dist/cli/run/adapters/opencode.test.d.ts +2 -0
  156. package/dist/cli/run/adapters/opencode.test.d.ts.map +1 -0
  157. package/dist/cli/run/adapters/opencode.test.js +119 -0
  158. package/dist/cli/run/adapters/opencode.test.js.map +1 -0
  159. package/dist/cli/run/executor.js +1 -1
  160. package/dist/cli/run/executor.js.map +1 -1
  161. package/dist/cli/run/schema.d.ts.map +1 -1
  162. package/dist/cli/run/schema.js +245 -4
  163. package/dist/cli/run/schema.js.map +1 -1
  164. package/dist/cli/run/schema.test.js +669 -0
  165. package/dist/cli/run/schema.test.js.map +1 -1
  166. package/dist/cli/run.d.ts.map +1 -1
  167. package/dist/cli/run.js +362 -22
  168. package/dist/cli/run.js.map +1 -1
  169. package/dist/cli/types.d.ts +85 -2
  170. package/dist/cli/types.d.ts.map +1 -1
  171. package/dist/cli/types.js.map +1 -1
  172. package/dist/cli/watch.d.ts +15 -0
  173. package/dist/cli/watch.d.ts.map +1 -0
  174. package/dist/cli/watch.js +279 -0
  175. package/dist/cli/watch.js.map +1 -0
  176. package/package.json +1 -1
  177. package/src/cli/agents.ts +177 -0
  178. package/src/cli/baselines.ts +143 -0
  179. package/src/cli/convoy/engine.test.ts +1839 -70
  180. package/src/cli/convoy/engine.ts +2417 -38
  181. package/src/cli/convoy/events.test.ts +179 -38
  182. package/src/cli/convoy/events.ts +88 -16
  183. package/src/cli/convoy/expertise.test.ts +128 -0
  184. package/src/cli/convoy/expertise.ts +163 -0
  185. package/src/cli/convoy/export.test.ts +1 -0
  186. package/src/cli/convoy/formula.test.ts +405 -0
  187. package/src/cli/convoy/formula.ts +174 -0
  188. package/src/cli/convoy/gates.test.ts +1169 -0
  189. package/src/cli/convoy/gates.ts +774 -0
  190. package/src/cli/convoy/health.test.ts +64 -2
  191. package/src/cli/convoy/health.ts +80 -2
  192. package/src/cli/convoy/issues.test.ts +143 -0
  193. package/src/cli/convoy/issues.ts +136 -0
  194. package/src/cli/convoy/knowledge.test.ts +101 -0
  195. package/src/cli/convoy/knowledge.ts +132 -0
  196. package/src/cli/convoy/lessons.test.ts +188 -0
  197. package/src/cli/convoy/lessons.ts +164 -0
  198. package/src/cli/convoy/lock.test.ts +181 -0
  199. package/src/cli/convoy/lock.ts +103 -0
  200. package/src/cli/convoy/merge.test.ts +6 -7
  201. package/src/cli/convoy/merge.ts +19 -1
  202. package/src/cli/convoy/partition.test.ts +423 -0
  203. package/src/cli/convoy/partition.ts +232 -0
  204. package/src/cli/convoy/pipeline.test.ts +6 -0
  205. package/src/cli/convoy/store.test.ts +1512 -14
  206. package/src/cli/convoy/store.ts +676 -30
  207. package/src/cli/convoy/types.ts +170 -1
  208. package/src/cli/destroy.test.ts +141 -0
  209. package/src/cli/destroy.ts +88 -0
  210. package/src/cli/gitignore.ts +36 -0
  211. package/src/cli/plan.ts +316 -0
  212. package/src/cli/run/adapters/claude.test.ts +234 -0
  213. package/src/cli/run/adapters/claude.ts +45 -5
  214. package/src/cli/run/adapters/copilot.test.ts +224 -0
  215. package/src/cli/run/adapters/copilot.ts +34 -4
  216. package/src/cli/run/adapters/cursor.test.ts +144 -0
  217. package/src/cli/run/adapters/cursor.ts +33 -2
  218. package/src/cli/run/adapters/opencode.test.ts +135 -0
  219. package/src/cli/run/adapters/opencode.ts +30 -2
  220. package/src/cli/run/executor.ts +1 -1
  221. package/src/cli/run/schema.test.ts +758 -0
  222. package/src/cli/run/schema.ts +300 -25
  223. package/src/cli/run.ts +341 -21
  224. package/src/cli/types.ts +86 -1
  225. package/src/cli/watch.ts +298 -0
  226. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
@@ -34,30 +34,89 @@ export function parseTimeout(timeout: string): number {
34
34
  }
35
35
 
36
36
  interface RawSpec {
37
- name?: unknown
38
- concurrency?: unknown
39
- on_failure?: unknown
40
- adapter?: unknown
41
- tasks?: unknown
42
- version?: unknown
43
- defaults?: unknown
44
- gates?: unknown
45
- gate_retries?: unknown
46
- branch?: unknown
47
- depends_on_convoy?: unknown
37
+ name?: string
38
+ concurrency?: number | string
39
+ on_failure?: string
40
+ adapter?: string
41
+ tasks?: RawTask[]
42
+ version?: number
43
+ defaults?: {
44
+ timeout?: string
45
+ model?: string
46
+ max_retries?: number
47
+ agent?: string
48
+ adapter?: string
49
+ mcp_servers?: unknown[]
50
+ mcp_approve_all?: unknown
51
+ mcp_server_approval_timeout?: unknown
52
+ built_in_gates?: unknown
53
+ browser_test?: unknown
54
+ review?: unknown
55
+ reviewer_model?: unknown
56
+ review_budget?: unknown
57
+ on_review_budget_exceeded?: unknown
58
+ max_concurrent_reviews?: unknown
59
+ review_heuristics?: unknown
60
+ max_swarm_concurrency?: unknown
61
+ }
62
+ gates?: string[]
63
+ gate_retries?: number
64
+ branch?: string
65
+ depends_on_convoy?: string[]
66
+ guard?: unknown
48
67
  }
49
68
 
50
69
  interface RawTask {
51
- id?: unknown
52
- prompt?: unknown
53
- agent?: unknown
54
- timeout?: unknown
55
- depends_on?: unknown
56
- files?: unknown
57
- description?: unknown
58
- model?: unknown
59
- max_retries?: unknown
60
- adapter?: unknown
70
+ id?: string
71
+ prompt?: string
72
+ agent?: string
73
+ timeout?: string
74
+ depends_on?: string[]
75
+ files?: string[]
76
+ description?: string
77
+ model?: string
78
+ max_retries?: number
79
+ adapter?: string
80
+ built_in_gates?: unknown
81
+ browser_test?: unknown
82
+ review?: string
83
+ }
84
+
85
+ /**
86
+ * Validate a browser_test config object for the given prefix.
87
+ */
88
+ function validateBrowserTestConfig(value: unknown, prefix: string, errors: string[]): void {
89
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
90
+ errors.push(`\`${prefix}\` must be an object`)
91
+ return
92
+ }
93
+ const bt = value as Record<string, unknown>
94
+ if (bt.urls === undefined) {
95
+ errors.push(`\`${prefix}.urls\` is required`)
96
+ } else if (!Array.isArray(bt.urls) || (bt.urls as unknown[]).length === 0) {
97
+ errors.push(`\`${prefix}.urls\` must be a non-empty array`)
98
+ } else if (!(bt.urls as unknown[]).every((u) => typeof u === 'string')) {
99
+ errors.push(`\`${prefix}.urls\` must be an array of strings`)
100
+ }
101
+ if (bt.check_console_errors !== undefined && typeof bt.check_console_errors !== 'boolean') {
102
+ errors.push(`\`${prefix}.check_console_errors\` must be a boolean`)
103
+ }
104
+ if (bt.visual_diff_threshold !== undefined) {
105
+ const vdt = Number(bt.visual_diff_threshold)
106
+ if (!Number.isFinite(vdt) || vdt < 0 || vdt > 1) {
107
+ errors.push(`\`${prefix}.visual_diff_threshold\` must be a number between 0 and 1`)
108
+ }
109
+ }
110
+ if (bt.a11y !== undefined && typeof bt.a11y !== 'boolean') {
111
+ errors.push(`\`${prefix}.a11y\` must be a boolean`)
112
+ }
113
+ const validSeverities = ['critical', 'serious', 'moderate', 'minor']
114
+ if (bt.severity_threshold !== undefined && !validSeverities.includes(bt.severity_threshold as string)) {
115
+ errors.push(`\`${prefix}.severity_threshold\` must be one of: ${validSeverities.join(', ')}`)
116
+ }
117
+ if (bt.baselines_dir !== undefined && typeof bt.baselines_dir !== 'string') {
118
+ errors.push(`\`${prefix}.baselines_dir\` must be a string`)
119
+ }
61
120
  }
62
121
 
63
122
  /**
@@ -79,9 +138,13 @@ export function validateSpec(spec: unknown): ValidationResult {
79
138
 
80
139
  // Concurrency
81
140
  if (s.concurrency !== undefined) {
82
- const c = Number(s.concurrency)
83
- if (!Number.isInteger(c) || c < 1) {
84
- errors.push('`concurrency` must be an integer >= 1')
141
+ if (s.concurrency === 'auto') {
142
+ // valid swarm mode
143
+ } else {
144
+ const c = Number(s.concurrency)
145
+ if (!Number.isInteger(c) || c < 1 || c > 50) {
146
+ errors.push('`concurrency` must be an integer between 1 and 50, or "auto"')
147
+ }
85
148
  }
86
149
  }
87
150
 
@@ -142,6 +205,158 @@ export function validateSpec(spec: unknown): ValidationResult {
142
205
  if (d.adapter !== undefined && typeof d.adapter !== 'string') {
143
206
  errors.push('`defaults.adapter` must be a string')
144
207
  }
208
+
209
+ // MCP servers validation (Phase 19.7)
210
+ if (d.mcp_servers !== undefined) {
211
+ if (!Array.isArray(d.mcp_servers)) {
212
+ errors.push('`defaults.mcp_servers` must be an array')
213
+ } else {
214
+ for (let j = 0; j < (d.mcp_servers as unknown[]).length; j++) {
215
+ const server = (d.mcp_servers as unknown[])[j]
216
+ const sp = `defaults.mcp_servers[${j}]`
217
+ if (!server || typeof server !== 'object' || Array.isArray(server)) {
218
+ errors.push(`\`${sp}\` must be an object`)
219
+ continue
220
+ }
221
+ const srv = server as Record<string, unknown>
222
+ if (!srv.name || typeof srv.name !== 'string') {
223
+ errors.push(`\`${sp}.name\` is required and must be a string`)
224
+ }
225
+ if (!srv.type || typeof srv.type !== 'string') {
226
+ errors.push(`\`${sp}.type\` is required and must be a string`)
227
+ }
228
+ if (srv.local !== undefined && typeof srv.local !== 'boolean') {
229
+ errors.push(`\`${sp}.local\` must be a boolean`)
230
+ }
231
+ if (srv.command !== undefined && typeof srv.command !== 'string') {
232
+ errors.push(`\`${sp}.command\` must be a string`)
233
+ }
234
+ if (srv.args !== undefined) {
235
+ if (!Array.isArray(srv.args) || !(srv.args as unknown[]).every(a => typeof a === 'string')) {
236
+ errors.push(`\`${sp}.args\` must be an array of strings`)
237
+ }
238
+ }
239
+ if (srv.url !== undefined && typeof srv.url !== 'string') {
240
+ errors.push(`\`${sp}.url\` must be a string`)
241
+ }
242
+ if (srv.config !== undefined) {
243
+ if (!srv.config || typeof srv.config !== 'object' || Array.isArray(srv.config)) {
244
+ errors.push(`\`${sp}.config\` must be an object`)
245
+ }
246
+ }
247
+ }
248
+ }
249
+ }
250
+
251
+ // mcp_approve_all validation
252
+ if (d.mcp_approve_all !== undefined && typeof d.mcp_approve_all !== 'boolean') {
253
+ errors.push('`defaults.mcp_approve_all` must be a boolean')
254
+ }
255
+
256
+ // mcp_server_approval_timeout validation
257
+ if (d.mcp_server_approval_timeout !== undefined) {
258
+ const t = Number(d.mcp_server_approval_timeout)
259
+ if (!Number.isFinite(t) || t <= 0) {
260
+ errors.push('`defaults.mcp_server_approval_timeout` must be a number greater than 0')
261
+ }
262
+ }
263
+
264
+ // built_in_gates validation
265
+ if (d.built_in_gates !== undefined) {
266
+ if (!d.built_in_gates || typeof d.built_in_gates !== 'object' || Array.isArray(d.built_in_gates)) {
267
+ errors.push('`defaults.built_in_gates` must be an object')
268
+ } else {
269
+ const bg = d.built_in_gates as Record<string, unknown>
270
+ const boolOrAutoFields = ['secret_scan', 'blast_radius', 'dependency_audit', 'regression_test', 'browser_test'] as const
271
+ for (const field of boolOrAutoFields) {
272
+ if (bg[field] !== undefined && typeof bg[field] !== 'boolean' && bg[field] !== 'auto') {
273
+ errors.push(`\`defaults.built_in_gates.${field}\` must be a boolean or "auto"`)
274
+ }
275
+ }
276
+ if (bg.gate_timeout !== undefined) {
277
+ const gt = Number(bg.gate_timeout)
278
+ if (!Number.isFinite(gt) || gt <= 0) {
279
+ errors.push('`defaults.built_in_gates.gate_timeout` must be a number greater than 0')
280
+ }
281
+ }
282
+ }
283
+ }
284
+
285
+ // browser_test config validation
286
+ if (d.browser_test !== undefined) {
287
+ validateBrowserTestConfig(d.browser_test, 'defaults.browser_test', errors)
288
+ }
289
+
290
+ // review validation
291
+ const VALID_REVIEW = ['auto', 'fast', 'panel', 'none']
292
+ if (d.review !== undefined && !VALID_REVIEW.includes(d.review as string)) {
293
+ errors.push('`defaults.review` must be one of: ' + VALID_REVIEW.join(', '))
294
+ }
295
+ if (d.reviewer_model !== undefined && typeof d.reviewer_model !== 'string') {
296
+ errors.push('`defaults.reviewer_model` must be a string')
297
+ }
298
+ if (d.review_budget !== undefined) {
299
+ const rb = Number(d.review_budget)
300
+ if (!Number.isInteger(rb) || rb < 1) {
301
+ errors.push('`defaults.review_budget` must be a positive integer')
302
+ }
303
+ }
304
+ const VALID_BUDGET_EXCEEDED = ['skip', 'downgrade', 'stop']
305
+ if (
306
+ d.on_review_budget_exceeded !== undefined &&
307
+ !VALID_BUDGET_EXCEEDED.includes(d.on_review_budget_exceeded as string)
308
+ ) {
309
+ errors.push(
310
+ '`defaults.on_review_budget_exceeded` must be one of: ' + VALID_BUDGET_EXCEEDED.join(', '),
311
+ )
312
+ }
313
+ if (d.max_concurrent_reviews !== undefined) {
314
+ const mcr = Number(d.max_concurrent_reviews)
315
+ if (!Number.isInteger(mcr) || mcr < 1) {
316
+ errors.push('`defaults.max_concurrent_reviews` must be a positive integer')
317
+ }
318
+ }
319
+ if (d.review_heuristics !== undefined) {
320
+ if (
321
+ !d.review_heuristics ||
322
+ typeof d.review_heuristics !== 'object' ||
323
+ Array.isArray(d.review_heuristics)
324
+ ) {
325
+ errors.push('`defaults.review_heuristics` must be an object')
326
+ } else {
327
+ const rh = d.review_heuristics as Record<string, unknown>
328
+ for (const field of ['panel_paths', 'panel_agents', 'auto_pass_agents'] as const) {
329
+ if (rh[field] !== undefined) {
330
+ if (
331
+ !Array.isArray(rh[field]) ||
332
+ !(rh[field] as unknown[]).every((v) => typeof v === 'string')
333
+ ) {
334
+ errors.push(
335
+ `\`defaults.review_heuristics.${field}\` must be an array of strings`,
336
+ )
337
+ }
338
+ }
339
+ }
340
+ if (rh.auto_pass_max_lines !== undefined) {
341
+ const apl = Number(rh.auto_pass_max_lines)
342
+ if (!Number.isInteger(apl) || apl < 1) {
343
+ errors.push('`defaults.review_heuristics.auto_pass_max_lines` must be a positive integer')
344
+ }
345
+ }
346
+ if (rh.auto_pass_max_files !== undefined) {
347
+ const apf = Number(rh.auto_pass_max_files)
348
+ if (!Number.isInteger(apf) || apf < 1) {
349
+ errors.push('`defaults.review_heuristics.auto_pass_max_files` must be a positive integer')
350
+ }
351
+ }
352
+ }
353
+ }
354
+ if (d.max_swarm_concurrency !== undefined) {
355
+ const msc = Number(d.max_swarm_concurrency)
356
+ if (!Number.isInteger(msc) || msc < 1 || msc > 50) {
357
+ errors.push('`defaults.max_swarm_concurrency` must be an integer between 1 and 50')
358
+ }
359
+ }
145
360
  }
146
361
  }
147
362
 
@@ -168,6 +383,28 @@ export function validateSpec(spec: unknown): ValidationResult {
168
383
  errors.push('`branch` must be a string')
169
384
  }
170
385
 
386
+ // guard config validation
387
+ if (s.guard !== undefined) {
388
+ const g = s.guard as Record<string, unknown>
389
+ if (!g || typeof g !== 'object' || Array.isArray(g)) {
390
+ errors.push('`guard` must be an object')
391
+ } else {
392
+ if (g.enabled !== undefined && typeof g.enabled !== 'boolean') {
393
+ errors.push('`guard.enabled` must be a boolean')
394
+ }
395
+ if (g.agent !== undefined && typeof g.agent !== 'string') {
396
+ errors.push('`guard.agent` must be a string')
397
+ }
398
+ if (g.checks !== undefined) {
399
+ if (!Array.isArray(g.checks)) {
400
+ errors.push('`guard.checks` must be an array of non-empty strings')
401
+ } else if (!(g.checks as unknown[]).every((c) => typeof c === 'string' && c.length > 0)) {
402
+ errors.push('`guard.checks` must be an array of non-empty strings')
403
+ }
404
+ }
405
+ }
406
+ }
407
+
171
408
  // Tasks: required unless this is a version:2 pipeline spec with depends_on_convoy
172
409
  const isPipeline =
173
410
  s.version === 2 &&
@@ -264,6 +501,40 @@ export function validateSpec(spec: unknown): ValidationResult {
264
501
  if (task.adapter !== undefined && typeof task.adapter !== 'string') {
265
502
  errors.push(`${prefix}: \`adapter\` must be a string`)
266
503
  }
504
+
505
+ // built_in_gates (task-level)
506
+ if (task.built_in_gates !== undefined) {
507
+ if (!task.built_in_gates || typeof task.built_in_gates !== 'object' || Array.isArray(task.built_in_gates)) {
508
+ errors.push(`${prefix}: \`built_in_gates\` must be an object`)
509
+ } else {
510
+ const bg = task.built_in_gates as Record<string, unknown>
511
+ const boolOrAutoFields = ['secret_scan', 'blast_radius', 'dependency_audit', 'regression_test', 'browser_test'] as const
512
+ for (const field of boolOrAutoFields) {
513
+ if (bg[field] !== undefined && typeof bg[field] !== 'boolean' && bg[field] !== 'auto') {
514
+ errors.push(`${prefix}: \`built_in_gates.${field}\` must be a boolean or "auto"`)
515
+ }
516
+ }
517
+ if (bg.gate_timeout !== undefined) {
518
+ const gt = Number(bg.gate_timeout)
519
+ if (!Number.isFinite(gt) || gt <= 0) {
520
+ errors.push(`${prefix}: \`built_in_gates.gate_timeout\` must be a number greater than 0`)
521
+ }
522
+ }
523
+ }
524
+ }
525
+
526
+ // browser_test (task-level)
527
+ if (task.browser_test !== undefined) {
528
+ validateBrowserTestConfig(task.browser_test, `${prefix}.browser_test`, errors)
529
+ }
530
+
531
+ // review (task-level)
532
+ if (task.review !== undefined) {
533
+ const VALID_TASK_REVIEW = ['auto', 'fast', 'panel', 'none']
534
+ if (!VALID_TASK_REVIEW.includes(task.review as string)) {
535
+ errors.push(`${prefix}: \`review\` must be one of: ${VALID_TASK_REVIEW.join(', ')}`)
536
+ }
537
+ }
267
538
  }
268
539
 
269
540
  // DAG cycle detection
@@ -324,7 +595,7 @@ function detectCycles(tasks: Array<{ id: string; depends_on?: string[] }>): stri
324
595
  */
325
596
  export function applyDefaults(spec: Record<string, unknown>): TaskSpec {
326
597
  const s = spec as Record<string, unknown>
327
- s.concurrency = s.concurrency !== undefined ? Number(s.concurrency) : 1
598
+ s.concurrency = s.concurrency === 'auto' ? 'auto' : (s.concurrency !== undefined ? Number(s.concurrency) : 1)
328
599
  s.on_failure = (s.on_failure as string) || 'continue'
329
600
  // Leave adapter empty so run.ts can auto-detect the best available CLI
330
601
  s.adapter = (s.adapter as string) || ''
@@ -358,6 +629,10 @@ export function applyDefaults(spec: Record<string, unknown>): TaskSpec {
358
629
  if (task.adapter === undefined && d.adapter !== undefined) {
359
630
  task.adapter = d.adapter
360
631
  }
632
+ // review: task-level overrides defaults
633
+ if (task.review === undefined && d.review !== undefined) {
634
+ task.review = d.review
635
+ }
361
636
  }
362
637
 
363
638
  return s as unknown as TaskSpec