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.
- package/LICENSE +93 -21
- package/README.md +9 -3
- package/bin/cli.mjs +15 -0
- package/dist/cli/agents.d.ts.map +1 -1
- package/dist/cli/agents.js +19 -5
- package/dist/cli/agents.js.map +1 -1
- package/dist/cli/artifacts-cli.d.ts +3 -0
- package/dist/cli/artifacts-cli.d.ts.map +1 -0
- package/dist/cli/artifacts-cli.js +36 -0
- package/dist/cli/artifacts-cli.js.map +1 -0
- package/dist/cli/baselines.d.ts.map +1 -1
- package/dist/cli/baselines.js +11 -0
- package/dist/cli/baselines.js.map +1 -1
- package/dist/cli/convoy/artifacts.d.ts +25 -0
- package/dist/cli/convoy/artifacts.d.ts.map +1 -0
- package/dist/cli/convoy/artifacts.js +129 -0
- package/dist/cli/convoy/artifacts.js.map +1 -0
- package/dist/cli/convoy/artifacts.test.d.ts +2 -0
- package/dist/cli/convoy/artifacts.test.d.ts.map +1 -0
- package/dist/cli/convoy/artifacts.test.js +169 -0
- package/dist/cli/convoy/artifacts.test.js.map +1 -0
- package/dist/cli/convoy/compaction.d.ts +23 -0
- package/dist/cli/convoy/compaction.d.ts.map +1 -0
- package/dist/cli/convoy/compaction.js +117 -0
- package/dist/cli/convoy/compaction.js.map +1 -0
- package/dist/cli/convoy/compaction.test.d.ts +2 -0
- package/dist/cli/convoy/compaction.test.d.ts.map +1 -0
- package/dist/cli/convoy/compaction.test.js +205 -0
- package/dist/cli/convoy/compaction.test.js.map +1 -0
- package/dist/cli/convoy/contracts.d.ts +22 -0
- package/dist/cli/convoy/contracts.d.ts.map +1 -0
- package/dist/cli/convoy/contracts.js +254 -0
- package/dist/cli/convoy/contracts.js.map +1 -0
- package/dist/cli/convoy/contracts.test.d.ts +2 -0
- package/dist/cli/convoy/contracts.test.d.ts.map +1 -0
- package/dist/cli/convoy/contracts.test.js +239 -0
- package/dist/cli/convoy/contracts.test.js.map +1 -0
- package/dist/cli/convoy/dag-analysis.d.ts +40 -0
- package/dist/cli/convoy/dag-analysis.d.ts.map +1 -0
- package/dist/cli/convoy/dag-analysis.js +282 -0
- package/dist/cli/convoy/dag-analysis.js.map +1 -0
- package/dist/cli/convoy/dag-analysis.test.d.ts +2 -0
- package/dist/cli/convoy/dag-analysis.test.d.ts.map +1 -0
- package/dist/cli/convoy/dag-analysis.test.js +289 -0
- package/dist/cli/convoy/dag-analysis.test.js.map +1 -0
- package/dist/cli/convoy/effort-scaling.d.ts +20 -0
- package/dist/cli/convoy/effort-scaling.d.ts.map +1 -0
- package/dist/cli/convoy/effort-scaling.js +82 -0
- package/dist/cli/convoy/effort-scaling.js.map +1 -0
- package/dist/cli/convoy/effort-scaling.test.d.ts +2 -0
- package/dist/cli/convoy/effort-scaling.test.d.ts.map +1 -0
- package/dist/cli/convoy/effort-scaling.test.js +120 -0
- package/dist/cli/convoy/effort-scaling.test.js.map +1 -0
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +298 -11
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +155 -18
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/event-schemas.d.ts.map +1 -1
- package/dist/cli/convoy/event-schemas.js +55 -0
- package/dist/cli/convoy/event-schemas.js.map +1 -1
- package/dist/cli/convoy/isolation.d.ts +27 -0
- package/dist/cli/convoy/isolation.d.ts.map +1 -0
- package/dist/cli/convoy/isolation.js +120 -0
- package/dist/cli/convoy/isolation.js.map +1 -0
- package/dist/cli/convoy/isolation.test.d.ts +2 -0
- package/dist/cli/convoy/isolation.test.d.ts.map +1 -0
- package/dist/cli/convoy/isolation.test.js +105 -0
- package/dist/cli/convoy/isolation.test.js.map +1 -0
- package/dist/cli/convoy/review-stages.d.ts +9 -0
- package/dist/cli/convoy/review-stages.d.ts.map +1 -0
- package/dist/cli/convoy/review-stages.js +134 -0
- package/dist/cli/convoy/review-stages.js.map +1 -0
- package/dist/cli/convoy/review-stages.test.d.ts +2 -0
- package/dist/cli/convoy/review-stages.test.d.ts.map +1 -0
- package/dist/cli/convoy/review-stages.test.js +197 -0
- package/dist/cli/convoy/review-stages.test.js.map +1 -0
- package/dist/cli/convoy/skill-refinement.d.ts +39 -0
- package/dist/cli/convoy/skill-refinement.d.ts.map +1 -0
- package/dist/cli/convoy/skill-refinement.js +239 -0
- package/dist/cli/convoy/skill-refinement.js.map +1 -0
- package/dist/cli/convoy/skill-refinement.test.d.ts +2 -0
- package/dist/cli/convoy/skill-refinement.test.d.ts.map +1 -0
- package/dist/cli/convoy/skill-refinement.test.js +230 -0
- package/dist/cli/convoy/skill-refinement.test.js.map +1 -0
- package/dist/cli/convoy/spec-builder.d.ts +1 -0
- package/dist/cli/convoy/spec-builder.d.ts.map +1 -1
- package/dist/cli/convoy/spec-builder.js +11 -0
- package/dist/cli/convoy/spec-builder.js.map +1 -1
- package/dist/cli/convoy/spec-builder.test.js +54 -0
- package/dist/cli/convoy/spec-builder.test.js.map +1 -1
- package/dist/cli/convoy/store.d.ts +3 -2
- package/dist/cli/convoy/store.d.ts.map +1 -1
- package/dist/cli/convoy/store.js +20 -2
- package/dist/cli/convoy/store.js.map +1 -1
- package/dist/cli/convoy/store.test.js +15 -15
- package/dist/cli/convoy/store.test.js.map +1 -1
- package/dist/cli/convoy/tdd-gate.d.ts +15 -0
- package/dist/cli/convoy/tdd-gate.d.ts.map +1 -0
- package/dist/cli/convoy/tdd-gate.js +119 -0
- package/dist/cli/convoy/tdd-gate.js.map +1 -0
- package/dist/cli/convoy/tdd-gate.test.d.ts +2 -0
- package/dist/cli/convoy/tdd-gate.test.d.ts.map +1 -0
- package/dist/cli/convoy/tdd-gate.test.js +227 -0
- package/dist/cli/convoy/tdd-gate.test.js.map +1 -0
- package/dist/cli/convoy/types.d.ts +91 -0
- package/dist/cli/convoy/types.d.ts.map +1 -1
- package/dist/cli/convoy/types.js +8 -0
- package/dist/cli/convoy/types.js.map +1 -1
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +54 -0
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/insights.d.ts +3 -0
- package/dist/cli/insights.d.ts.map +1 -0
- package/dist/cli/insights.js +94 -0
- package/dist/cli/insights.js.map +1 -0
- package/dist/cli/lesson.d.ts.map +1 -1
- package/dist/cli/lesson.js +7 -0
- package/dist/cli/lesson.js.map +1 -1
- package/dist/cli/log.d.ts.map +1 -1
- package/dist/cli/log.js +7 -0
- package/dist/cli/log.js.map +1 -1
- package/dist/cli/package-config.d.ts +12 -0
- package/dist/cli/package-config.d.ts.map +1 -0
- package/dist/cli/package-config.js +37 -0
- package/dist/cli/package-config.js.map +1 -0
- package/dist/cli/package.d.ts +23 -0
- package/dist/cli/package.d.ts.map +1 -0
- package/dist/cli/package.js +285 -0
- package/dist/cli/package.js.map +1 -0
- package/dist/cli/package.test.d.ts +2 -0
- package/dist/cli/package.test.d.ts.map +1 -0
- package/dist/cli/package.test.js +236 -0
- package/dist/cli/package.test.js.map +1 -0
- package/dist/cli/pipeline.d.ts +6 -0
- package/dist/cli/pipeline.d.ts.map +1 -1
- package/dist/cli/pipeline.js +15 -2
- package/dist/cli/pipeline.js.map +1 -1
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +32 -0
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run/schema.test.js +51 -0
- package/dist/cli/run/schema.test.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +10 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/skills.d.ts +3 -0
- package/dist/cli/skills.d.ts.map +1 -0
- package/dist/cli/skills.js +107 -0
- package/dist/cli/skills.js.map +1 -0
- package/dist/cli/types.d.ts +4 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/update.js +2 -2
- package/package.json +3 -2
- package/src/cli/agents.ts +20 -5
- package/src/cli/artifacts-cli.ts +41 -0
- package/src/cli/baselines.ts +12 -0
- package/src/cli/convoy/artifacts.test.ts +201 -0
- package/src/cli/convoy/artifacts.ts +186 -0
- package/src/cli/convoy/compaction.test.ts +245 -0
- package/src/cli/convoy/compaction.ts +164 -0
- package/src/cli/convoy/contracts.test.ts +279 -0
- package/src/cli/convoy/contracts.ts +280 -0
- package/src/cli/convoy/dag-analysis.test.ts +349 -0
- package/src/cli/convoy/dag-analysis.ts +371 -0
- package/src/cli/convoy/effort-scaling.test.ts +140 -0
- package/src/cli/convoy/effort-scaling.ts +90 -0
- package/src/cli/convoy/engine.test.ts +175 -18
- package/src/cli/convoy/engine.ts +315 -12
- package/src/cli/convoy/event-schemas.ts +55 -0
- package/src/cli/convoy/isolation.test.ts +137 -0
- package/src/cli/convoy/isolation.ts +165 -0
- package/src/cli/convoy/review-stages.test.ts +235 -0
- package/src/cli/convoy/review-stages.ts +166 -0
- package/src/cli/convoy/skill-refinement.test.ts +277 -0
- package/src/cli/convoy/skill-refinement.ts +306 -0
- package/src/cli/convoy/spec-builder.test.ts +61 -0
- package/src/cli/convoy/spec-builder.ts +9 -0
- package/src/cli/convoy/store.test.ts +15 -15
- package/src/cli/convoy/store.ts +26 -4
- package/src/cli/convoy/tdd-gate.test.ts +281 -0
- package/src/cli/convoy/tdd-gate.ts +154 -0
- package/src/cli/convoy/types.ts +51 -0
- package/src/cli/dashboard.ts +55 -0
- package/src/cli/insights.ts +99 -0
- package/src/cli/lesson.ts +8 -0
- package/src/cli/log.ts +8 -0
- package/src/cli/package-config.ts +48 -0
- package/src/cli/package.test.ts +276 -0
- package/src/cli/package.ts +329 -0
- package/src/cli/pipeline.ts +21 -2
- package/src/cli/run/schema.test.ts +58 -0
- package/src/cli/run/schema.ts +33 -0
- package/src/cli/run.ts +14 -1
- package/src/cli/skills.ts +121 -0
- package/src/cli/types.ts +4 -1
- package/src/cli/update.ts +2 -2
- package/src/dashboard/dist/_astro/{index.Je1YjU_y.css → index.BRDFmNzR.css} +1 -1
- package/src/dashboard/dist/index.html +163 -2
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/src/pages/index.astro +162 -1
- package/src/dashboard/src/styles/dashboard.css +85 -0
- package/src/orchestrator/agents/developer.agent.md +8 -0
- package/src/orchestrator/agents/ui-ux-expert.agent.md +7 -0
- package/src/orchestrator/prompts/assess-complexity.prompt.md +13 -0
- package/src/orchestrator/prompts/brainstorm.prompt.md +18 -0
- package/src/orchestrator/prompts/generate-convoy.prompt.md +61 -0
- package/src/orchestrator/skills/decomposition/SKILL.md +35 -0
- package/src/orchestrator/skills/frontend-design/SKILL.md +27 -1
- 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
|
+
}
|