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
@@ -1,4 +1,4 @@
1
- export type ConvoyStatus = 'pending' | 'running' | 'done' | 'failed' | 'gate-failed'
1
+ export type ConvoyStatus = 'pending' | 'running' | 'done' | 'failed' | 'gate-failed' | 'hook-failed'
2
2
 
3
3
  export type ConvoyTaskStatus =
4
4
  | 'pending'
@@ -6,8 +6,13 @@ export type ConvoyTaskStatus =
6
6
  | 'running'
7
7
  | 'done'
8
8
  | 'failed'
9
+ | 'gate-failed'
10
+ | 'review-blocked'
9
11
  | 'timed-out'
10
12
  | 'skipped'
13
+ | 'hook-failed'
14
+ | 'disputed'
15
+ | 'wait-for-input'
11
16
 
12
17
  export type WorkerStatus = 'spawned' | 'running' | 'done' | 'failed' | 'killed'
13
18
 
@@ -24,8 +29,11 @@ export interface ConvoyRecord {
24
29
  finished_at: string | null
25
30
  spec_yaml: string
26
31
  total_tokens: number | null
27
- total_cost_usd: string | null
32
+ total_cost_usd: number | null
28
33
  pipeline_id: string | null
34
+ circuit_state: string | null
35
+ review_tokens_total: number | null
36
+ review_budget: number | null
29
37
  }
30
38
 
31
39
  export interface TaskRecord {
@@ -51,7 +59,25 @@ export interface TaskRecord {
51
59
  prompt_tokens: number | null
52
60
  completion_tokens: number | null
53
61
  total_tokens: number | null
54
- cost_usd: string | null
62
+ cost_usd: number | null
63
+ gates: string | null
64
+ on_exhausted: 'dlq' | 'skip' | 'stop'
65
+ injected: number
66
+ provenance: string | null
67
+ idempotency_key: string | null
68
+ current_step: number | null
69
+ total_steps: number | null
70
+ review_level: string | null
71
+ review_verdict: string | null
72
+ review_tokens: number | null
73
+ review_model: string | null
74
+ panel_attempts: number
75
+ dispute_id: string | null
76
+ drift_score: number | null
77
+ drift_retried: number
78
+ outputs?: string | null // JSON array of TaskOutput
79
+ inputs?: string | null // JSON array of TaskInput
80
+ discovered_issues?: string | null // JSON array
55
81
  }
56
82
 
57
83
  export interface WorkerRecord {
@@ -88,5 +114,253 @@ export interface PipelineRecord {
88
114
  started_at: string | null
89
115
  finished_at: string | null
90
116
  total_tokens: number | null
91
- total_cost_usd: string | null
117
+ total_cost_usd: number | null
92
118
  }
119
+
120
+ export interface BuiltInGatesConfig {
121
+ secret_scan?: boolean
122
+ blast_radius?: boolean
123
+ dependency_audit?: 'auto' | boolean
124
+ regression_test?: 'auto' | boolean
125
+ browser_test?: 'auto' | boolean
126
+ gate_timeout?: number
127
+ }
128
+
129
+
130
+ export interface BrowserTestConfig {
131
+ urls: string[]
132
+ check_console_errors?: boolean
133
+ visual_diff_threshold?: number
134
+ a11y?: boolean
135
+ severity_threshold?: 'critical' | 'serious' | 'moderate' | 'minor'
136
+ baselines_dir?: string
137
+ }
138
+ export interface GuardConfig {
139
+ enabled?: boolean // default: true
140
+ agent?: string // optional agent name (e.g. 'session-guard')
141
+ checks?: string[] // e.g. ['observability', 'cleanup', 'cost-report']
142
+ }
143
+
144
+ export interface DlqRecord {
145
+ id: string
146
+ convoy_id: string
147
+ task_id: string
148
+ agent: string
149
+ failure_type: string
150
+ error_output: string | null
151
+ attempts: number
152
+ tokens_spent: number | null
153
+ escalation_task_id: string | null
154
+ resolved: number
155
+ resolution: string | null
156
+ created_at: string
157
+ resolved_at: string | null
158
+ }
159
+
160
+ export interface CircuitBreakerConfig {
161
+ threshold?: number // failures before Open (default: 3)
162
+ cooldown_ms?: number // ms in Open before Half-Open (default: 300000 = 5min)
163
+ fallback_agent?: string // reassign pending tasks when circuit opens
164
+ }
165
+
166
+ export interface TaskOutput {
167
+ name: string
168
+ type: 'file' | 'summary' | 'json'
169
+ description?: string
170
+ }
171
+
172
+ export interface TaskInput {
173
+ from: string
174
+ name: string
175
+ as?: string
176
+ }
177
+
178
+ export interface ArtifactRecord {
179
+ id: string
180
+ convoy_id: string
181
+ task_id: string
182
+ name: string
183
+ type: 'file' | 'summary' | 'json'
184
+ content: string
185
+ created_at: string
186
+ }
187
+
188
+ export interface AgentIdentityRecord {
189
+ id: string
190
+ agent: string
191
+ convoy_id: string
192
+ task_id: string
193
+ summary: string
194
+ created_at: string
195
+ retention_days: number
196
+ }
197
+
198
+ export interface StepCondition {
199
+ step: string // reference previous step by id
200
+ exitCode?: { eq?: number; ne?: number; gt?: number; lt?: number }
201
+ fileExists?: { path: string }
202
+ }
203
+
204
+ export interface TaskStep {
205
+ id?: string
206
+ prompt: string
207
+ gates?: string[]
208
+ max_retries?: number // inherits from task if omitted
209
+ if?: StepCondition
210
+ }
211
+
212
+ export interface Hook {
213
+ type: 'review' | 'guard' | 'agent' | 'command' | 'validate'
214
+ name?: string
215
+ prompt?: string // for agent hooks
216
+ command?: string // for command hooks
217
+ on?: 'pre_task' | 'post_task' | 'post_convoy'
218
+ }
219
+
220
+ export interface TaskStepRecord {
221
+ id: number
222
+ task_id: string
223
+ step_index: number
224
+ prompt: string
225
+ gates: string | null
226
+ status: string
227
+ exit_code: number | null
228
+ output: string | null
229
+ started_at: string | null
230
+ finished_at: string | null
231
+ }
232
+
233
+ export interface WatchTrigger {
234
+ type: 'file-change' | 'cron' | 'git-push'
235
+ glob?: string // for file-change: glob pattern to watch
236
+ schedule?: string // for cron: 5-field cron expression
237
+ branch?: string // for git-push: branch name pattern
238
+ debounce_ms?: number // file-change debounce (default: 500ms)
239
+ }
240
+
241
+ export interface WatchConfig {
242
+ triggers: WatchTrigger[]
243
+ clear_scratchpad?: boolean // clear scratchpad on watch start
244
+ scratchpad_retention_days?: number // auto-clear scratchpad entries older than N days
245
+ }
246
+
247
+ export interface ScratchpadRecord {
248
+ key: string
249
+ value: string
250
+ updated_at: string
251
+ }
252
+
253
+ export interface MCPServerConfig {
254
+ name: string
255
+ type: string
256
+ local?: boolean
257
+ command?: string
258
+ args?: string[]
259
+ url?: string
260
+ config?: Record<string, unknown>
261
+ }
262
+
263
+ // ---------------------------------------------------------------------------
264
+ // Discriminated union covering every canonical convoy event type.
265
+ // Each variant constrains the `data` shape that callers may pass to emit().
266
+ // ---------------------------------------------------------------------------
267
+ export type ConvoyEventType =
268
+ | { type: 'convoy_started'; data?: { name?: string } }
269
+ | { type: 'convoy_finished'; data?: { status: string } }
270
+ | { type: 'convoy_failed'; data?: { status: string; reason?: string } }
271
+ | { type: 'convoy_guard'; data?: { checks?: string[]; [key: string]: unknown } }
272
+ | { type: 'task_started'; data?: { worker_id?: string } }
273
+ | { type: 'task_done'; data?: { status?: string; retries?: number; worker_id?: string } }
274
+ | { type: 'task_failed'; data?: { reason: string; worker_id?: string; gate?: string; hook?: string } }
275
+ | { type: 'task_skipped'; data?: { reason: string } }
276
+ | { type: 'task_retried'; data?: { previous_status: string } }
277
+ | { type: 'task_waiting_input'; data?: { task_id?: string; reason?: string } }
278
+ | { type: 'review_started'; data?: { level: string; task_id?: string; model?: string } }
279
+ | {
280
+ type: 'review_verdict'
281
+ data?: {
282
+ level: string
283
+ verdict: string
284
+ tokens: number
285
+ model?: string
286
+ feedback_length?: number
287
+ budget_exceeded?: boolean
288
+ budget_downgrade?: boolean
289
+ budget_skip?: boolean
290
+ passes?: number
291
+ blocks?: number
292
+ }
293
+ }
294
+ | { type: 'dispute_opened'; data?: { dispute_id: string; task_id: string; agent?: string; reason?: string } }
295
+ | { type: 'dlq_entry_created'; data?: { dlq_id: string; task_id: string; agent?: string; attempts?: number } }
296
+ | { type: 'drift_check_result'; data?: { score?: number; threshold?: number; passed?: boolean } }
297
+ | { type: 'drift_detected'; data?: { score?: number; files?: string[] } }
298
+ | { type: 'circuit_breaker_tripped'; data?: { agent?: string; failure_count?: number; threshold?: number } }
299
+ | { type: 'circuit_breaker_fallback'; data?: { original_agent?: string; fallback_agent?: string; task_id?: string } }
300
+ | { type: 'circuit_breaker_blocked'; data?: { agent?: string; task_id?: string } }
301
+ | { type: 'merge_conflict_detected'; data?: { task_id?: string; files?: string[] } }
302
+ | { type: 'merge_conflict_failed'; data?: { task_id?: string; error?: string } }
303
+ | { type: 'file_injection_received'; data?: { task_id?: string; from_task?: string; name?: string } }
304
+ | { type: 'artifact_limit_reached'; data?: { task_id?: string; limit?: number; current?: number } }
305
+ | { type: 'agent_identity_captured'; data?: { agent?: string; task_id?: string } }
306
+ | { type: 'agent_identity_rejected'; data?: { agent?: string; task_id?: string; reason?: string } }
307
+ | { type: 'weak_area_skipped'; data?: { agent?: string; weak_areas?: string[]; task_files?: string[] } }
308
+ | { type: 'swarm_concurrency_update'; data?: { new_concurrency?: number; reason?: string } }
309
+ | { type: 'post_convoy_hook_failed'; data?: { hook?: string; error?: string } }
310
+ | { type: 'session'; data?: { agent?: string; model?: string; task?: string; outcome?: string; duration_min?: number } }
311
+ | { type: 'delegation'; data?: { agent?: string; model?: string; tier?: string; mechanism?: string; outcome?: string } }
312
+ | {
313
+ type: 'secret_leak_prevented'
314
+ data?: { original_type?: string; patterns?: string[]; task_id?: string; findings_count?: number; context?: string }
315
+ }
316
+ | { type: 'ndjson_write_failed'; data?: { original_type?: string } }
317
+ | { type: 'built_in_gate_result'; data?: { gate: string; passed: boolean; output?: string; level?: string } }
318
+ | { type: 'watch_started'; data?: { trigger_type?: string; pid?: number } }
319
+ | { type: 'watch_cycle_start'; data?: { cycle_number?: number; triggered_by?: string } }
320
+ | { type: 'watch_cycle_end'; data?: { cycle_number?: number; status?: string } }
321
+ | { type: 'watch_stopped'; data?: { reason?: string } }
322
+ | { type: 'worker_killed'; data?: { reason?: string; worker_id?: string; task_id?: string } }
323
+ | { type: 'discovered_issue'; data?: { task_id?: string; title?: string; file?: string; description?: string; severity?: string } }
324
+
325
+ /** All canonical convoy event type strings. Used for runtime validation. */
326
+ export const KNOWN_EVENT_TYPES: Set<string> = new Set<ConvoyEventType['type']>([
327
+ 'convoy_started',
328
+ 'convoy_finished',
329
+ 'convoy_failed',
330
+ 'convoy_guard',
331
+ 'task_started',
332
+ 'task_done',
333
+ 'task_failed',
334
+ 'task_skipped',
335
+ 'task_retried',
336
+ 'task_waiting_input',
337
+ 'review_started',
338
+ 'review_verdict',
339
+ 'dispute_opened',
340
+ 'dlq_entry_created',
341
+ 'drift_check_result',
342
+ 'drift_detected',
343
+ 'circuit_breaker_tripped',
344
+ 'circuit_breaker_fallback',
345
+ 'circuit_breaker_blocked',
346
+ 'merge_conflict_detected',
347
+ 'merge_conflict_failed',
348
+ 'file_injection_received',
349
+ 'artifact_limit_reached',
350
+ 'agent_identity_captured',
351
+ 'agent_identity_rejected',
352
+ 'weak_area_skipped',
353
+ 'swarm_concurrency_update',
354
+ 'post_convoy_hook_failed',
355
+ 'session',
356
+ 'delegation',
357
+ 'secret_leak_prevented',
358
+ 'ndjson_write_failed',
359
+ 'built_in_gate_result',
360
+ 'watch_started',
361
+ 'watch_cycle_start',
362
+ 'watch_cycle_end',
363
+ 'watch_stopped',
364
+ 'worker_killed',
365
+ 'discovered_issue',
366
+ ])
package/src/cli/log.ts CHANGED
@@ -1,18 +1,29 @@
1
1
  import { mkdir, appendFile, stat } from 'node:fs/promises'
2
+ import { readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
2
3
  import { join, dirname } from 'node:path'
3
4
  import type { CliContext } from './types.js'
4
5
 
5
6
  const HELP = `
6
7
  opencastle log [options]
8
+ opencastle log merge [--since <ISO-date>] [--until <ISO-date>] [--output <path>]
7
9
 
8
- Append a structured event to the observability log (events.ndjson).
10
+ Append a structured event to the observability log (events.ndjson),
11
+ or merge per-convoy NDJSON files into a single file.
9
12
 
10
- Options:
13
+ Subcommands:
14
+ merge Merge all .opencastle/logs/convoys/*.ndjson into convoy-events.ndjson
15
+
16
+ Options (log append):
11
17
  --type <type> Event type (required): session|delegation|review|panel|dispute
12
18
  --<field> <value> Any field from the event schema (see documentation)
13
19
  --logs-dir <path> Override the logs directory path
14
20
  --help, -h Show this help
15
21
 
22
+ Options (merge):
23
+ --since <ISO-date> Only include records at or after this date
24
+ --until <ISO-date> Only include records at or before this date
25
+ --output <path> Output path (default: .opencastle/logs/convoy-events.ndjson)
26
+
16
27
  Array fields (comma-separated): file_partition, lessons_added, discoveries, reviewing_agents
17
28
  Boolean fields: escalated, weighted
18
29
  Numeric fields: auto-detected from value
@@ -21,6 +32,8 @@ const HELP = `
21
32
  opencastle log --type session --agent Developer --model claude-sonnet-4-6 --task "Fix bug" --outcome success
22
33
  opencastle log --type delegation --session_id feat/prj-1 --agent Developer --tier fast --mechanism sub-agent --outcome success
23
34
  opencastle log --type panel --panel_key auth-review --verdict pass --pass_count 3 --block_count 0
35
+ opencastle log merge --since 2026-01-01 --output /tmp/merged.ndjson
36
+ opencastle log merge
24
37
  `
25
38
 
26
39
  const VALID_TYPES = ['session', 'delegation', 'review', 'panel', 'dispute']
@@ -59,6 +72,94 @@ export async function resolveLogsDir(override?: string | null): Promise<string>
59
72
  return join(process.cwd(), '.opencastle', 'logs')
60
73
  }
61
74
 
75
+ /** Merge per-convoy NDJSON files into a single deduplicated, sorted file. */
76
+ export async function mergeConvoyLogs(options: {
77
+ since?: string
78
+ until?: string
79
+ output?: string
80
+ basePath?: string
81
+ }): Promise<{ merged: number; deduplicated: number; written: number }> {
82
+ const base = options.basePath ?? process.cwd()
83
+ const convoysDir = join(base, '.opencastle', 'logs', 'convoys')
84
+
85
+ let files: string[] = []
86
+ try {
87
+ files = readdirSync(convoysDir)
88
+ .filter(f => f.endsWith('.ndjson'))
89
+ .map(f => join(convoysDir, f))
90
+ } catch {
91
+ return { merged: 0, deduplicated: 0, written: 0 }
92
+ }
93
+
94
+ if (files.length === 0) {
95
+ return { merged: 0, deduplicated: 0, written: 0 }
96
+ }
97
+
98
+ const allRecords: Array<Record<string, unknown>> = []
99
+ let totalRead = 0
100
+
101
+ for (const file of files) {
102
+ const content = readFileSync(file, 'utf8')
103
+ const lines = content.split('\n').filter(l => l.trim())
104
+ for (const line of lines) {
105
+ try {
106
+ allRecords.push(JSON.parse(line) as Record<string, unknown>)
107
+ totalRead++
108
+ } catch {
109
+ // skip malformed lines
110
+ }
111
+ }
112
+ }
113
+
114
+ // Deduplicate by _event_id — keep first occurrence
115
+ const seen = new Set<unknown>()
116
+ const unique: Array<Record<string, unknown>> = []
117
+ for (const record of allRecords) {
118
+ const id = record['_event_id']
119
+ if (id !== undefined) {
120
+ if (seen.has(id)) continue
121
+ seen.add(id)
122
+ }
123
+ unique.push(record)
124
+ }
125
+
126
+ const deduplicatedCount = totalRead - unique.length
127
+
128
+ // Filter by since/until
129
+ let filtered = unique
130
+ if (options.since) {
131
+ const since = options.since
132
+ filtered = filtered.filter(r => {
133
+ const ts = r['timestamp'] as string | undefined
134
+ return ts !== undefined && ts >= since
135
+ })
136
+ }
137
+ if (options.until) {
138
+ const until = options.until
139
+ filtered = filtered.filter(r => {
140
+ const ts = r['timestamp'] as string | undefined
141
+ return ts !== undefined && ts <= until
142
+ })
143
+ }
144
+
145
+ // Sort by timestamp ascending
146
+ filtered.sort((a, b) => {
147
+ const ta = (a['timestamp'] as string) ?? ''
148
+ const tb = (b['timestamp'] as string) ?? ''
149
+ return ta < tb ? -1 : ta > tb ? 1 : 0
150
+ })
151
+
152
+ if (filtered.length === 0) {
153
+ return { merged: totalRead, deduplicated: deduplicatedCount, written: 0 }
154
+ }
155
+
156
+ const outputPath = options.output ?? join(base, '.opencastle', 'logs', 'convoy-events.ndjson')
157
+ mkdirSync(dirname(outputPath), { recursive: true })
158
+ writeFileSync(outputPath, filtered.map(r => JSON.stringify(r)).join('\n') + '\n', 'utf8')
159
+
160
+ return { merged: totalRead, deduplicated: deduplicatedCount, written: filtered.length }
161
+ }
162
+
62
163
  /** Append a structured event record to events.ndjson. */
63
164
  export async function appendEvent(
64
165
  record: Record<string, unknown>,
@@ -77,6 +178,23 @@ export default async function log({ args }: CliContext): Promise<void> {
77
178
  return
78
179
  }
79
180
 
181
+ // merge subcommand
182
+ if (args[0] === 'merge') {
183
+ const mergeArgs = args.slice(1)
184
+ let since: string | undefined
185
+ let until: string | undefined
186
+ let output: string | undefined
187
+ for (let i = 0; i < mergeArgs.length; i++) {
188
+ const a = mergeArgs[i]
189
+ if (a === '--since' && i + 1 < mergeArgs.length) { since = mergeArgs[++i]; continue }
190
+ if (a === '--until' && i + 1 < mergeArgs.length) { until = mergeArgs[++i]; continue }
191
+ if (a === '--output' && i + 1 < mergeArgs.length) { output = mergeArgs[++i]; continue }
192
+ }
193
+ const result = await mergeConvoyLogs({ since, until, output })
194
+ console.log(` Merged: ${result.merged} records, Deduplicated: ${result.deduplicated}, Written: ${result.written}`)
195
+ return
196
+ }
197
+
80
198
  let type: string | null = null
81
199
  let logsDir: string | null = null
82
200
  const fields: Record<string, unknown> = {}