opencastle 0.31.6 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/LICENSE +93 -21
  2. package/README.md +9 -3
  3. package/bin/cli.mjs +15 -0
  4. package/dist/cli/agents.d.ts.map +1 -1
  5. package/dist/cli/agents.js +19 -5
  6. package/dist/cli/agents.js.map +1 -1
  7. package/dist/cli/artifacts-cli.d.ts +3 -0
  8. package/dist/cli/artifacts-cli.d.ts.map +1 -0
  9. package/dist/cli/artifacts-cli.js +36 -0
  10. package/dist/cli/artifacts-cli.js.map +1 -0
  11. package/dist/cli/baselines.d.ts.map +1 -1
  12. package/dist/cli/baselines.js +11 -0
  13. package/dist/cli/baselines.js.map +1 -1
  14. package/dist/cli/convoy/artifacts.d.ts +25 -0
  15. package/dist/cli/convoy/artifacts.d.ts.map +1 -0
  16. package/dist/cli/convoy/artifacts.js +129 -0
  17. package/dist/cli/convoy/artifacts.js.map +1 -0
  18. package/dist/cli/convoy/artifacts.test.d.ts +2 -0
  19. package/dist/cli/convoy/artifacts.test.d.ts.map +1 -0
  20. package/dist/cli/convoy/artifacts.test.js +169 -0
  21. package/dist/cli/convoy/artifacts.test.js.map +1 -0
  22. package/dist/cli/convoy/compaction.d.ts +23 -0
  23. package/dist/cli/convoy/compaction.d.ts.map +1 -0
  24. package/dist/cli/convoy/compaction.js +117 -0
  25. package/dist/cli/convoy/compaction.js.map +1 -0
  26. package/dist/cli/convoy/compaction.test.d.ts +2 -0
  27. package/dist/cli/convoy/compaction.test.d.ts.map +1 -0
  28. package/dist/cli/convoy/compaction.test.js +205 -0
  29. package/dist/cli/convoy/compaction.test.js.map +1 -0
  30. package/dist/cli/convoy/contracts.d.ts +22 -0
  31. package/dist/cli/convoy/contracts.d.ts.map +1 -0
  32. package/dist/cli/convoy/contracts.js +254 -0
  33. package/dist/cli/convoy/contracts.js.map +1 -0
  34. package/dist/cli/convoy/contracts.test.d.ts +2 -0
  35. package/dist/cli/convoy/contracts.test.d.ts.map +1 -0
  36. package/dist/cli/convoy/contracts.test.js +239 -0
  37. package/dist/cli/convoy/contracts.test.js.map +1 -0
  38. package/dist/cli/convoy/dag-analysis.d.ts +40 -0
  39. package/dist/cli/convoy/dag-analysis.d.ts.map +1 -0
  40. package/dist/cli/convoy/dag-analysis.js +282 -0
  41. package/dist/cli/convoy/dag-analysis.js.map +1 -0
  42. package/dist/cli/convoy/dag-analysis.test.d.ts +2 -0
  43. package/dist/cli/convoy/dag-analysis.test.d.ts.map +1 -0
  44. package/dist/cli/convoy/dag-analysis.test.js +289 -0
  45. package/dist/cli/convoy/dag-analysis.test.js.map +1 -0
  46. package/dist/cli/convoy/effort-scaling.d.ts +20 -0
  47. package/dist/cli/convoy/effort-scaling.d.ts.map +1 -0
  48. package/dist/cli/convoy/effort-scaling.js +82 -0
  49. package/dist/cli/convoy/effort-scaling.js.map +1 -0
  50. package/dist/cli/convoy/effort-scaling.test.d.ts +2 -0
  51. package/dist/cli/convoy/effort-scaling.test.d.ts.map +1 -0
  52. package/dist/cli/convoy/effort-scaling.test.js +120 -0
  53. package/dist/cli/convoy/effort-scaling.test.js.map +1 -0
  54. package/dist/cli/convoy/engine.d.ts.map +1 -1
  55. package/dist/cli/convoy/engine.js +298 -11
  56. package/dist/cli/convoy/engine.js.map +1 -1
  57. package/dist/cli/convoy/engine.test.js +155 -18
  58. package/dist/cli/convoy/engine.test.js.map +1 -1
  59. package/dist/cli/convoy/event-schemas.d.ts.map +1 -1
  60. package/dist/cli/convoy/event-schemas.js +55 -0
  61. package/dist/cli/convoy/event-schemas.js.map +1 -1
  62. package/dist/cli/convoy/isolation.d.ts +27 -0
  63. package/dist/cli/convoy/isolation.d.ts.map +1 -0
  64. package/dist/cli/convoy/isolation.js +120 -0
  65. package/dist/cli/convoy/isolation.js.map +1 -0
  66. package/dist/cli/convoy/isolation.test.d.ts +2 -0
  67. package/dist/cli/convoy/isolation.test.d.ts.map +1 -0
  68. package/dist/cli/convoy/isolation.test.js +105 -0
  69. package/dist/cli/convoy/isolation.test.js.map +1 -0
  70. package/dist/cli/convoy/review-stages.d.ts +9 -0
  71. package/dist/cli/convoy/review-stages.d.ts.map +1 -0
  72. package/dist/cli/convoy/review-stages.js +134 -0
  73. package/dist/cli/convoy/review-stages.js.map +1 -0
  74. package/dist/cli/convoy/review-stages.test.d.ts +2 -0
  75. package/dist/cli/convoy/review-stages.test.d.ts.map +1 -0
  76. package/dist/cli/convoy/review-stages.test.js +197 -0
  77. package/dist/cli/convoy/review-stages.test.js.map +1 -0
  78. package/dist/cli/convoy/skill-refinement.d.ts +39 -0
  79. package/dist/cli/convoy/skill-refinement.d.ts.map +1 -0
  80. package/dist/cli/convoy/skill-refinement.js +239 -0
  81. package/dist/cli/convoy/skill-refinement.js.map +1 -0
  82. package/dist/cli/convoy/skill-refinement.test.d.ts +2 -0
  83. package/dist/cli/convoy/skill-refinement.test.d.ts.map +1 -0
  84. package/dist/cli/convoy/skill-refinement.test.js +230 -0
  85. package/dist/cli/convoy/skill-refinement.test.js.map +1 -0
  86. package/dist/cli/convoy/spec-builder.d.ts +1 -0
  87. package/dist/cli/convoy/spec-builder.d.ts.map +1 -1
  88. package/dist/cli/convoy/spec-builder.js +11 -0
  89. package/dist/cli/convoy/spec-builder.js.map +1 -1
  90. package/dist/cli/convoy/spec-builder.test.js +54 -0
  91. package/dist/cli/convoy/spec-builder.test.js.map +1 -1
  92. package/dist/cli/convoy/store.d.ts +3 -2
  93. package/dist/cli/convoy/store.d.ts.map +1 -1
  94. package/dist/cli/convoy/store.js +20 -2
  95. package/dist/cli/convoy/store.js.map +1 -1
  96. package/dist/cli/convoy/store.test.js +15 -15
  97. package/dist/cli/convoy/store.test.js.map +1 -1
  98. package/dist/cli/convoy/tdd-gate.d.ts +15 -0
  99. package/dist/cli/convoy/tdd-gate.d.ts.map +1 -0
  100. package/dist/cli/convoy/tdd-gate.js +119 -0
  101. package/dist/cli/convoy/tdd-gate.js.map +1 -0
  102. package/dist/cli/convoy/tdd-gate.test.d.ts +2 -0
  103. package/dist/cli/convoy/tdd-gate.test.d.ts.map +1 -0
  104. package/dist/cli/convoy/tdd-gate.test.js +227 -0
  105. package/dist/cli/convoy/tdd-gate.test.js.map +1 -0
  106. package/dist/cli/convoy/types.d.ts +91 -0
  107. package/dist/cli/convoy/types.d.ts.map +1 -1
  108. package/dist/cli/convoy/types.js +8 -0
  109. package/dist/cli/convoy/types.js.map +1 -1
  110. package/dist/cli/dashboard.d.ts.map +1 -1
  111. package/dist/cli/dashboard.js +54 -0
  112. package/dist/cli/dashboard.js.map +1 -1
  113. package/dist/cli/insights.d.ts +3 -0
  114. package/dist/cli/insights.d.ts.map +1 -0
  115. package/dist/cli/insights.js +94 -0
  116. package/dist/cli/insights.js.map +1 -0
  117. package/dist/cli/lesson.d.ts.map +1 -1
  118. package/dist/cli/lesson.js +7 -0
  119. package/dist/cli/lesson.js.map +1 -1
  120. package/dist/cli/log.d.ts.map +1 -1
  121. package/dist/cli/log.js +7 -0
  122. package/dist/cli/log.js.map +1 -1
  123. package/dist/cli/package-config.d.ts +12 -0
  124. package/dist/cli/package-config.d.ts.map +1 -0
  125. package/dist/cli/package-config.js +37 -0
  126. package/dist/cli/package-config.js.map +1 -0
  127. package/dist/cli/package.d.ts +23 -0
  128. package/dist/cli/package.d.ts.map +1 -0
  129. package/dist/cli/package.js +285 -0
  130. package/dist/cli/package.js.map +1 -0
  131. package/dist/cli/package.test.d.ts +2 -0
  132. package/dist/cli/package.test.d.ts.map +1 -0
  133. package/dist/cli/package.test.js +236 -0
  134. package/dist/cli/package.test.js.map +1 -0
  135. package/dist/cli/pipeline.d.ts +6 -0
  136. package/dist/cli/pipeline.d.ts.map +1 -1
  137. package/dist/cli/pipeline.js +15 -2
  138. package/dist/cli/pipeline.js.map +1 -1
  139. package/dist/cli/run/schema.d.ts.map +1 -1
  140. package/dist/cli/run/schema.js +32 -0
  141. package/dist/cli/run/schema.js.map +1 -1
  142. package/dist/cli/run/schema.test.js +51 -0
  143. package/dist/cli/run/schema.test.js.map +1 -1
  144. package/dist/cli/run.d.ts.map +1 -1
  145. package/dist/cli/run.js +10 -1
  146. package/dist/cli/run.js.map +1 -1
  147. package/dist/cli/skills.d.ts +3 -0
  148. package/dist/cli/skills.d.ts.map +1 -0
  149. package/dist/cli/skills.js +107 -0
  150. package/dist/cli/skills.js.map +1 -0
  151. package/dist/cli/types.d.ts +4 -1
  152. package/dist/cli/types.d.ts.map +1 -1
  153. package/dist/cli/update.js +2 -2
  154. package/package.json +3 -2
  155. package/src/cli/agents.ts +20 -5
  156. package/src/cli/artifacts-cli.ts +41 -0
  157. package/src/cli/baselines.ts +12 -0
  158. package/src/cli/convoy/artifacts.test.ts +201 -0
  159. package/src/cli/convoy/artifacts.ts +186 -0
  160. package/src/cli/convoy/compaction.test.ts +245 -0
  161. package/src/cli/convoy/compaction.ts +164 -0
  162. package/src/cli/convoy/contracts.test.ts +279 -0
  163. package/src/cli/convoy/contracts.ts +280 -0
  164. package/src/cli/convoy/dag-analysis.test.ts +349 -0
  165. package/src/cli/convoy/dag-analysis.ts +371 -0
  166. package/src/cli/convoy/effort-scaling.test.ts +140 -0
  167. package/src/cli/convoy/effort-scaling.ts +90 -0
  168. package/src/cli/convoy/engine.test.ts +175 -18
  169. package/src/cli/convoy/engine.ts +315 -12
  170. package/src/cli/convoy/event-schemas.ts +55 -0
  171. package/src/cli/convoy/isolation.test.ts +137 -0
  172. package/src/cli/convoy/isolation.ts +165 -0
  173. package/src/cli/convoy/review-stages.test.ts +235 -0
  174. package/src/cli/convoy/review-stages.ts +166 -0
  175. package/src/cli/convoy/skill-refinement.test.ts +277 -0
  176. package/src/cli/convoy/skill-refinement.ts +306 -0
  177. package/src/cli/convoy/spec-builder.test.ts +61 -0
  178. package/src/cli/convoy/spec-builder.ts +9 -0
  179. package/src/cli/convoy/store.test.ts +15 -15
  180. package/src/cli/convoy/store.ts +26 -4
  181. package/src/cli/convoy/tdd-gate.test.ts +281 -0
  182. package/src/cli/convoy/tdd-gate.ts +154 -0
  183. package/src/cli/convoy/types.ts +51 -0
  184. package/src/cli/dashboard.ts +55 -0
  185. package/src/cli/insights.ts +99 -0
  186. package/src/cli/lesson.ts +8 -0
  187. package/src/cli/log.ts +8 -0
  188. package/src/cli/package-config.ts +48 -0
  189. package/src/cli/package.test.ts +276 -0
  190. package/src/cli/package.ts +329 -0
  191. package/src/cli/pipeline.ts +21 -2
  192. package/src/cli/run/schema.test.ts +58 -0
  193. package/src/cli/run/schema.ts +33 -0
  194. package/src/cli/run.ts +14 -1
  195. package/src/cli/skills.ts +121 -0
  196. package/src/cli/types.ts +4 -1
  197. package/src/cli/update.ts +2 -2
  198. package/src/dashboard/dist/_astro/{index.Je1YjU_y.css → index.BRDFmNzR.css} +1 -1
  199. package/src/dashboard/dist/index.html +163 -2
  200. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  201. package/src/dashboard/src/pages/index.astro +162 -1
  202. package/src/dashboard/src/styles/dashboard.css +85 -0
  203. package/src/orchestrator/agents/developer.agent.md +8 -0
  204. package/src/orchestrator/agents/ui-ux-expert.agent.md +7 -0
  205. package/src/orchestrator/prompts/assess-complexity.prompt.md +13 -0
  206. package/src/orchestrator/prompts/brainstorm.prompt.md +18 -0
  207. package/src/orchestrator/prompts/generate-convoy.prompt.md +61 -0
  208. package/src/orchestrator/skills/decomposition/SKILL.md +35 -0
  209. package/src/orchestrator/skills/frontend-design/SKILL.md +27 -1
  210. package/src/orchestrator/skills/project-consistency/SKILL.md +350 -0
@@ -0,0 +1,371 @@
1
+ import type { ConvoyStore } from './store.js'
2
+ import type { ConvoyRecord, TaskRecord } from './types.js'
3
+
4
+ export interface ConvoyPattern {
5
+ name: string
6
+ task_count_range: [number, number]
7
+ agent_sequence: string[]
8
+ avg_duration_ms: number
9
+ avg_tokens: number
10
+ success_rate: number
11
+ common_failure_agents: string[]
12
+ recommended_concurrency: number
13
+ sample_size: number
14
+ }
15
+
16
+ export interface AgentPerformance {
17
+ agent: string
18
+ total_tasks: number
19
+ success_rate: number
20
+ avg_duration_ms: number
21
+ avg_tokens: number
22
+ avg_retries: number
23
+ best_file_patterns: string[]
24
+ worst_file_patterns: string[]
25
+ }
26
+
27
+ export interface DAGRecommendation {
28
+ patterns: ConvoyPattern[]
29
+ agent_stats: AgentPerformance[]
30
+ insights: string[]
31
+ generated_at: string
32
+ }
33
+
34
+ // ── helpers ───────────────────────────────────────────────────────────────────
35
+
36
+ function taskBucket(count: number): string {
37
+ if (count <= 2) return 'small'
38
+ if (count <= 5) return 'medium'
39
+ if (count <= 10) return 'large'
40
+ return 'xlarge'
41
+ }
42
+
43
+ function extractConcurrency(specYaml: string | null | undefined): number {
44
+ if (!specYaml) return 1
45
+ const m = specYaml.match(/concurrency:\s*(\d+)/)
46
+ return m ? parseInt(m[1], 10) : 1
47
+ }
48
+
49
+ function modeNum(values: number[]): number {
50
+ if (values.length === 0) return 1
51
+ const freq: Record<number, number> = {}
52
+ for (const v of values) freq[v] = (freq[v] ?? 0) + 1
53
+ let best = values[0]
54
+ let bestFreq = 0
55
+ for (const [k, f] of Object.entries(freq)) {
56
+ if (f > bestFreq) { bestFreq = f; best = Number(k) }
57
+ }
58
+ return best
59
+ }
60
+
61
+ function filePrefix(filePath: string): string {
62
+ const parts = filePath.split('/')
63
+ if (parts.length <= 2) return parts[0] ?? ''
64
+ return parts.slice(0, 2).join('/')
65
+ }
66
+
67
+ function avg(nums: number[]): number {
68
+ if (nums.length === 0) return 0
69
+ return nums.reduce((a, b) => a + b, 0) / nums.length
70
+ }
71
+
72
+ // ── public API ────────────────────────────────────────────────────────────────
73
+
74
+ export function extractExecutionHistory(
75
+ store: ConvoyStore,
76
+ sinceDays = 90,
77
+ ): { convoys: ConvoyRecord[]; tasks: TaskRecord[] } {
78
+ const allConvoys = store.getConvoyList(10000, 0)
79
+ const cutoff = Date.now() - sinceDays * 24 * 60 * 60 * 1000
80
+
81
+ const convoys = allConvoys.filter((c) => {
82
+ if (c.status !== 'done' && c.status !== 'failed') return false
83
+ if (!c.finished_at) return false
84
+ return new Date(c.finished_at).getTime() >= cutoff
85
+ })
86
+
87
+ const tasks: TaskRecord[] = []
88
+ for (const convoy of convoys) {
89
+ const convoyTasks = store.getTasksByConvoy(convoy.id)
90
+ tasks.push(...convoyTasks)
91
+ }
92
+
93
+ return { convoys, tasks }
94
+ }
95
+
96
+ export function clusterConvoys(
97
+ convoys: ConvoyRecord[],
98
+ tasks: TaskRecord[],
99
+ ): ConvoyPattern[] {
100
+ const tasksByConvoy = new Map<string, TaskRecord[]>()
101
+ for (const t of tasks) {
102
+ const list = tasksByConvoy.get(t.convoy_id) ?? []
103
+ list.push(t)
104
+ tasksByConvoy.set(t.convoy_id, list)
105
+ }
106
+
107
+ type ClusterEntry = { convoys: ConvoyRecord[]; convoyTasks: TaskRecord[][] }
108
+ const clusters = new Map<string, ClusterEntry>()
109
+
110
+ for (const convoy of convoys) {
111
+ const convoyTasks = tasksByConvoy.get(convoy.id) ?? []
112
+ const count = convoyTasks.length
113
+ const bucket = taskBucket(count)
114
+ const agentSet = [...new Set(convoyTasks.map((t) => t.agent))].sort()
115
+ const key = `${bucket}|${agentSet.join(',')}`
116
+
117
+ const existing = clusters.get(key) ?? { convoys: [], convoyTasks: [] }
118
+ existing.convoys.push(convoy)
119
+ existing.convoyTasks.push(convoyTasks)
120
+ clusters.set(key, existing)
121
+ }
122
+
123
+ const patterns: ConvoyPattern[] = []
124
+
125
+ for (const [key, { convoys: clusterConvoys, convoyTasks }] of clusters) {
126
+ const sepIdx = key.indexOf('|')
127
+ const bucket = key.slice(0, sepIdx)
128
+ const agentStr = key.slice(sepIdx + 1)
129
+ const agentSeq = agentStr ? agentStr.split(',').filter(Boolean) : []
130
+
131
+ const durations: number[] = []
132
+ for (const c of clusterConvoys) {
133
+ if (c.started_at && c.finished_at) {
134
+ durations.push(new Date(c.finished_at).getTime() - new Date(c.started_at).getTime())
135
+ }
136
+ }
137
+
138
+ const tokenValues: number[] = clusterConvoys
139
+ .filter((c) => c.total_tokens != null)
140
+ .map((c) => c.total_tokens as number)
141
+
142
+ const successCount = clusterConvoys.filter((c) => c.status === 'done').length
143
+ const successRate = clusterConvoys.length > 0 ? successCount / clusterConvoys.length : 0
144
+
145
+ const agentFailCounts: Record<string, { failed: number; total: number }> = {}
146
+ for (const taskList of convoyTasks) {
147
+ for (const t of taskList) {
148
+ const entry = agentFailCounts[t.agent] ?? { failed: 0, total: 0 }
149
+ entry.total++
150
+ if (t.status === 'failed') entry.failed++
151
+ agentFailCounts[t.agent] = entry
152
+ }
153
+ }
154
+ const commonFailureAgents = Object.entries(agentFailCounts)
155
+ .filter(([, v]) => v.total > 0 && v.failed / v.total > 0.2)
156
+ .map(([agent]) => agent)
157
+
158
+ const successfulConvoys = clusterConvoys.filter((c) => c.status === 'done')
159
+ const concurrencies = successfulConvoys.map((c) => extractConcurrency(c.spec_yaml))
160
+ const recommendedConcurrency = modeNum(concurrencies.length > 0 ? concurrencies : [1])
161
+
162
+ const counts = convoyTasks.map((tl) => tl.length)
163
+ const bucketMinMap: Record<string, number> = { small: 1, medium: 3, large: 6, xlarge: 11 }
164
+ const minCount = counts.length > 0 ? Math.min(...counts) : (bucketMinMap[bucket] ?? 1)
165
+ const maxCount = counts.length > 0 ? Math.max(...counts) : 0
166
+
167
+ const agentLabel =
168
+ agentSeq.length === 0
169
+ ? 'empty'
170
+ : agentSeq.length === 1
171
+ ? agentSeq[0]
172
+ : agentSeq.slice(0, 2).join('-')
173
+ const name = `${bucket}-${agentLabel}`
174
+
175
+ patterns.push({
176
+ name,
177
+ task_count_range: [minCount, maxCount],
178
+ agent_sequence: agentSeq,
179
+ avg_duration_ms: avg(durations),
180
+ avg_tokens: avg(tokenValues),
181
+ success_rate: successRate,
182
+ common_failure_agents: commonFailureAgents,
183
+ recommended_concurrency: recommendedConcurrency,
184
+ sample_size: clusterConvoys.length,
185
+ })
186
+ }
187
+
188
+ return patterns
189
+ }
190
+
191
+ export function analyzeAgentPerformance(tasks: TaskRecord[]): AgentPerformance[] {
192
+ const byAgent = new Map<string, TaskRecord[]>()
193
+ for (const t of tasks) {
194
+ const list = byAgent.get(t.agent) ?? []
195
+ list.push(t)
196
+ byAgent.set(t.agent, list)
197
+ }
198
+
199
+ const results: AgentPerformance[] = []
200
+
201
+ for (const [agent, agentTasks] of byAgent) {
202
+ const totalTasks = agentTasks.length
203
+ const doneCount = agentTasks.filter((t) => t.status === 'done').length
204
+ const successRate = totalTasks > 0 ? doneCount / totalTasks : 0
205
+
206
+ const durations = agentTasks
207
+ .filter((t) => t.started_at != null && t.finished_at != null)
208
+ .map((t) => new Date(t.finished_at!).getTime() - new Date(t.started_at!).getTime())
209
+
210
+ const tokenValues = agentTasks
211
+ .filter((t) => t.total_tokens != null)
212
+ .map((t) => t.total_tokens as number)
213
+
214
+ const avgRetries = avg(agentTasks.map((t) => t.retries))
215
+
216
+ const prefixStats = new Map<string, { done: number; total: number }>()
217
+ for (const t of agentTasks) {
218
+ if (!t.files) continue
219
+ let fileList: string[]
220
+ try {
221
+ fileList = JSON.parse(t.files) as string[]
222
+ } catch {
223
+ continue
224
+ }
225
+ if (!Array.isArray(fileList)) continue
226
+ for (const f of fileList) {
227
+ const prefix = filePrefix(f)
228
+ if (!prefix) continue
229
+ const entry = prefixStats.get(prefix) ?? { done: 0, total: 0 }
230
+ entry.total++
231
+ if (t.status === 'done') entry.done++
232
+ prefixStats.set(prefix, entry)
233
+ }
234
+ }
235
+
236
+ const eligiblePrefixes = [...prefixStats.entries()].filter(([, v]) => v.total >= 2)
237
+ const sorted = eligiblePrefixes.sort(([, a], [, b]) => b.done / b.total - a.done / a.total)
238
+
239
+ const bestFilePatterns = sorted
240
+ .filter(([, v]) => v.done / v.total >= 0.8)
241
+ .slice(0, 3)
242
+ .map(([prefix]) => prefix)
243
+
244
+ const worstFilePatterns = [...sorted]
245
+ .reverse()
246
+ .filter(([, v]) => v.done / v.total <= 0.5)
247
+ .slice(0, 3)
248
+ .map(([prefix]) => prefix)
249
+
250
+ results.push({
251
+ agent,
252
+ total_tasks: totalTasks,
253
+ success_rate: successRate,
254
+ avg_duration_ms: avg(durations),
255
+ avg_tokens: avg(tokenValues),
256
+ avg_retries: avgRetries,
257
+ best_file_patterns: bestFilePatterns,
258
+ worst_file_patterns: worstFilePatterns,
259
+ })
260
+ }
261
+
262
+ return results
263
+ }
264
+
265
+ export function generateInsights(
266
+ patterns: ConvoyPattern[],
267
+ agents: AgentPerformance[],
268
+ ): string[] {
269
+ if (patterns.length === 0 && agents.length === 0) {
270
+ return ['No execution history available yet. Run some convoys first.']
271
+ }
272
+
273
+ const insights: string[] = []
274
+
275
+ for (const agent of agents) {
276
+ const ratePct = Math.round(agent.success_rate * 100)
277
+ if (agent.success_rate < 0.7) {
278
+ insights.push(
279
+ `⚠️ ${agent.agent} has a ${ratePct}% success rate — consider splitting tasks smaller or providing more context`,
280
+ )
281
+ }
282
+ if (agent.avg_retries > 1.5) {
283
+ insights.push(
284
+ `⚠️ ${agent.agent} has a ${agent.avg_retries.toFixed(1)}x average retry rate — investigate common failure causes`,
285
+ )
286
+ }
287
+ if (agent.best_file_patterns.length > 0) {
288
+ const bestRate = Math.round(agent.success_rate * 100)
289
+ insights.push(
290
+ `✓ ${agent.agent} excels on ${agent.best_file_patterns.join(', ')} (${bestRate}% success rate)`,
291
+ )
292
+ }
293
+ if (agent.success_rate === 1 && agent.total_tasks >= 3) {
294
+ insights.push(
295
+ `✓ ${agent.agent} has 100% success — auto-PASS review may be safe`,
296
+ )
297
+ }
298
+ }
299
+
300
+ for (const pattern of patterns) {
301
+ const durationMin = Math.round(pattern.avg_duration_ms / 60000)
302
+ insights.push(
303
+ `Convoys with ${pattern.task_count_range[0]}–${pattern.task_count_range[1]} tasks perform best at concurrency ${pattern.recommended_concurrency} (avg ${durationMin}min)`,
304
+ )
305
+ }
306
+
307
+ return insights
308
+ }
309
+
310
+ export function analyzeDAG(
311
+ store: ConvoyStore,
312
+ sinceDays?: number,
313
+ ): DAGRecommendation {
314
+ const { convoys, tasks } = extractExecutionHistory(store, sinceDays)
315
+ const patterns = clusterConvoys(convoys, tasks)
316
+ const agent_stats = analyzeAgentPerformance(tasks)
317
+ const insights = generateInsights(patterns, agent_stats)
318
+
319
+ return {
320
+ patterns,
321
+ agent_stats,
322
+ insights,
323
+ generated_at: new Date().toISOString(),
324
+ }
325
+ }
326
+
327
+ export function formatInsightsMarkdown(rec: DAGRecommendation): string {
328
+ const lines: string[] = []
329
+
330
+ lines.push('## Convoy Patterns\n')
331
+ if (rec.patterns.length === 0) {
332
+ lines.push('_No pattern data available._\n')
333
+ } else {
334
+ lines.push('| Name | Task Range | Success Rate | Concurrency | Samples |')
335
+ lines.push('|------|-----------|-------------|-------------|---------|')
336
+ for (const p of rec.patterns) {
337
+ const rate = `${Math.round(p.success_rate * 100)}%`
338
+ lines.push(
339
+ `| ${p.name} | ${p.task_count_range[0]}–${p.task_count_range[1]} | ${rate} | ${p.recommended_concurrency} | ${p.sample_size} |`,
340
+ )
341
+ }
342
+ lines.push('')
343
+ }
344
+
345
+ lines.push('## Agent Performance\n')
346
+ if (rec.agent_stats.length === 0) {
347
+ lines.push('_No agent data available._\n')
348
+ } else {
349
+ lines.push('| Agent | Tasks | Success Rate | Avg Duration | Avg Retries |')
350
+ lines.push('|-------|-------|-------------|-------------|-------------|')
351
+ for (const a of rec.agent_stats) {
352
+ const rate = `${Math.round(a.success_rate * 100)}%`
353
+ const dur = a.avg_duration_ms > 0 ? `${Math.round(a.avg_duration_ms / 1000)}s` : '—'
354
+ lines.push(
355
+ `| ${a.agent} | ${a.total_tasks} | ${rate} | ${dur} | ${a.avg_retries.toFixed(1)} |`,
356
+ )
357
+ }
358
+ lines.push('')
359
+ }
360
+
361
+ lines.push('## Recommendations\n')
362
+ for (const insight of rec.insights) {
363
+ lines.push(`- ${insight}`)
364
+ }
365
+
366
+ return lines.join('\n')
367
+ }
368
+
369
+ export function formatInsightsJSON(rec: DAGRecommendation): string {
370
+ return JSON.stringify(rec, null, 2)
371
+ }
@@ -0,0 +1,140 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { getEffortProfile, EFFORT_TABLE } from './effort-scaling.js'
3
+
4
+ describe('getEffortProfile', () => {
5
+ it('returns complexity-1 profile for score 1', () => {
6
+ const p = getEffortProfile(1)
7
+ expect(p.complexity).toBe(1)
8
+ expect(p.tier).toBe('economy')
9
+ expect(p.timeout).toBe('5m')
10
+ expect(p.max_retries).toBe(1)
11
+ expect(p.review).toBe('auto')
12
+ expect(p.expected_tokens).toBe(5000)
13
+ })
14
+
15
+ it('returns complexity-2 profile for score 2', () => {
16
+ const p = getEffortProfile(2)
17
+ expect(p.complexity).toBe(2)
18
+ expect(p.tier).toBe('economy')
19
+ expect(p.timeout).toBe('10m')
20
+ expect(p.max_retries).toBe(1)
21
+ expect(p.review).toBe('auto')
22
+ expect(p.expected_tokens).toBe(15000)
23
+ })
24
+
25
+ it('returns complexity-3 profile for score 3', () => {
26
+ const p = getEffortProfile(3)
27
+ expect(p.complexity).toBe(3)
28
+ expect(p.tier).toBe('standard')
29
+ expect(p.timeout).toBe('15m')
30
+ expect(p.max_retries).toBe(2)
31
+ expect(p.review).toBe('fast')
32
+ expect(p.expected_tokens).toBe(30000)
33
+ })
34
+
35
+ it('returns complexity-5 profile for score 5', () => {
36
+ const p = getEffortProfile(5)
37
+ expect(p.complexity).toBe(5)
38
+ expect(p.tier).toBe('standard')
39
+ expect(p.timeout).toBe('20m')
40
+ expect(p.max_retries).toBe(2)
41
+ expect(p.review).toBe('fast')
42
+ expect(p.expected_tokens).toBe(60000)
43
+ })
44
+
45
+ it('returns complexity-8 profile for score 8', () => {
46
+ const p = getEffortProfile(8)
47
+ expect(p.complexity).toBe(8)
48
+ expect(p.tier).toBe('standard')
49
+ expect(p.timeout).toBe('30m')
50
+ expect(p.max_retries).toBe(2)
51
+ expect(p.review).toBe('fast')
52
+ expect(p.expected_tokens).toBe(120000)
53
+ })
54
+
55
+ it('returns complexity-13 profile for score 13', () => {
56
+ const p = getEffortProfile(13)
57
+ expect(p.complexity).toBe(13)
58
+ expect(p.tier).toBe('premium')
59
+ expect(p.timeout).toBe('45m')
60
+ expect(p.max_retries).toBe(3)
61
+ expect(p.review).toBe('panel')
62
+ expect(p.expected_tokens).toBe(250000)
63
+ })
64
+
65
+ it('rounds up score 4 to complexity-5 profile', () => {
66
+ const p = getEffortProfile(4)
67
+ expect(p.complexity).toBe(5)
68
+ })
69
+
70
+ it('rounds up score 6 to complexity-8 profile', () => {
71
+ const p = getEffortProfile(6)
72
+ expect(p.complexity).toBe(8)
73
+ })
74
+
75
+ it('rounds up score 7 to complexity-8 profile', () => {
76
+ const p = getEffortProfile(7)
77
+ expect(p.complexity).toBe(8)
78
+ })
79
+
80
+ it('rounds up score 9 to complexity-13 profile', () => {
81
+ const p = getEffortProfile(9)
82
+ expect(p.complexity).toBe(13)
83
+ })
84
+
85
+ it('rounds up score 10 to complexity-13 profile', () => {
86
+ const p = getEffortProfile(10)
87
+ expect(p.complexity).toBe(13)
88
+ })
89
+
90
+ it('rounds up score 12 to complexity-13 profile', () => {
91
+ const p = getEffortProfile(12)
92
+ expect(p.complexity).toBe(13)
93
+ })
94
+
95
+ it('clamps score 0 to complexity-1 profile', () => {
96
+ const p = getEffortProfile(0)
97
+ expect(p.complexity).toBe(1)
98
+ })
99
+
100
+ it('clamps negative score -5 to complexity-1 profile', () => {
101
+ const p = getEffortProfile(-5)
102
+ expect(p.complexity).toBe(1)
103
+ })
104
+
105
+ it('clamps large negative score -9999 to complexity-1 profile', () => {
106
+ const p = getEffortProfile(-9999)
107
+ expect(p.complexity).toBe(1)
108
+ })
109
+
110
+ it('clamps score 14 to complexity-13 profile', () => {
111
+ const p = getEffortProfile(14)
112
+ expect(p.complexity).toBe(13)
113
+ })
114
+
115
+ it('clamps score 100 to complexity-13 profile', () => {
116
+ const p = getEffortProfile(100)
117
+ expect(p.complexity).toBe(13)
118
+ })
119
+
120
+ it('clamps very large score 9999 to complexity-13 profile', () => {
121
+ const p = getEffortProfile(9999)
122
+ expect(p.complexity).toBe(13)
123
+ })
124
+
125
+ it('returns the same object reference from EFFORT_TABLE for exact matches', () => {
126
+ for (const entry of EFFORT_TABLE) {
127
+ expect(getEffortProfile(entry.complexity)).toBe(entry)
128
+ }
129
+ })
130
+
131
+ it('EFFORT_TABLE has 6 entries', () => {
132
+ expect(EFFORT_TABLE).toHaveLength(6)
133
+ })
134
+
135
+ it('EFFORT_TABLE entries are ordered by ascending complexity', () => {
136
+ for (let i = 1; i < EFFORT_TABLE.length; i++) {
137
+ expect(EFFORT_TABLE[i].complexity).toBeGreaterThan(EFFORT_TABLE[i - 1].complexity)
138
+ }
139
+ })
140
+ })
@@ -0,0 +1,90 @@
1
+ export interface EffortProfile {
2
+ complexity: number
3
+ tier: 'economy' | 'standard' | 'utility' | 'premium'
4
+ max_agents: number
5
+ timeout: string
6
+ max_retries: number
7
+ review: 'none' | 'auto' | 'fast' | 'panel'
8
+ expected_tokens: number
9
+ description: string
10
+ }
11
+
12
+ export const EFFORT_TABLE: EffortProfile[] = [
13
+ {
14
+ complexity: 1,
15
+ tier: 'economy',
16
+ max_agents: 1,
17
+ timeout: '5m',
18
+ max_retries: 1,
19
+ review: 'auto',
20
+ expected_tokens: 5_000,
21
+ description: 'Trivial — single file edit, copy change, config tweak',
22
+ },
23
+ {
24
+ complexity: 2,
25
+ tier: 'economy',
26
+ max_agents: 1,
27
+ timeout: '10m',
28
+ max_retries: 1,
29
+ review: 'auto',
30
+ expected_tokens: 15_000,
31
+ description: 'Simple — small bug fix, add a test, update docs',
32
+ },
33
+ {
34
+ complexity: 3,
35
+ tier: 'standard',
36
+ max_agents: 1,
37
+ timeout: '15m',
38
+ max_retries: 2,
39
+ review: 'fast',
40
+ expected_tokens: 30_000,
41
+ description: 'Moderate — new component, API endpoint, or utility',
42
+ },
43
+ {
44
+ complexity: 5,
45
+ tier: 'standard',
46
+ max_agents: 2,
47
+ timeout: '20m',
48
+ max_retries: 2,
49
+ review: 'fast',
50
+ expected_tokens: 60_000,
51
+ description: 'Significant — multi-file feature, integration work',
52
+ },
53
+ {
54
+ complexity: 8,
55
+ tier: 'standard',
56
+ max_agents: 3,
57
+ timeout: '30m',
58
+ max_retries: 2,
59
+ review: 'fast',
60
+ expected_tokens: 120_000,
61
+ description: 'Complex — cross-cutting feature, schema change + UI + tests',
62
+ },
63
+ {
64
+ complexity: 13,
65
+ tier: 'premium',
66
+ max_agents: 5,
67
+ timeout: '45m',
68
+ max_retries: 3,
69
+ review: 'panel',
70
+ expected_tokens: 250_000,
71
+ description: 'Epic — architecture change, security overhaul, major refactor',
72
+ },
73
+ ]
74
+
75
+ /**
76
+ * Returns the EffortProfile for a given complexity score.
77
+ * - Exact match: returns that profile
78
+ * - Between defined values: rounds up to the next profile
79
+ * - Below 1: returns the complexity-1 profile
80
+ * - Above 13: returns the complexity-13 profile
81
+ */
82
+ export function getEffortProfile(complexity: number): EffortProfile {
83
+ if (complexity <= EFFORT_TABLE[0].complexity) return EFFORT_TABLE[0]
84
+ const last = EFFORT_TABLE[EFFORT_TABLE.length - 1]
85
+ if (complexity >= last.complexity) return last
86
+ for (const profile of EFFORT_TABLE) {
87
+ if (complexity <= profile.complexity) return profile
88
+ }
89
+ return last
90
+ }