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,280 @@
1
+ export interface FieldSpec {
2
+ type: 'string' | 'string[]' | 'number' | 'boolean' | 'object'
3
+ description: string
4
+ validation?: 'non-empty' | 'file-paths' | 'positive-int'
5
+ }
6
+
7
+ export interface OutputContract {
8
+ agent: string
9
+ required_fields: string[]
10
+ optional_fields: string[]
11
+ schema: Record<string, FieldSpec>
12
+ }
13
+
14
+ export interface ContractResult {
15
+ valid: boolean
16
+ missing: string[]
17
+ warnings: string[]
18
+ data?: Record<string, unknown>
19
+ }
20
+
21
+ export const AGENT_CONTRACTS: Record<string, OutputContract> = {
22
+ 'developer': {
23
+ agent: 'developer',
24
+ required_fields: ['files_changed', 'tests_added', 'summary'],
25
+ optional_fields: [],
26
+ schema: {
27
+ files_changed: { type: 'string[]', description: 'List of file paths created or modified', validation: 'file-paths' },
28
+ tests_added: { type: 'string[]', description: 'List of test file paths added or modified', validation: 'file-paths' },
29
+ summary: { type: 'string', description: 'Brief description of what was implemented', validation: 'non-empty' },
30
+ },
31
+ },
32
+ 'ui-ux-expert': {
33
+ agent: 'ui-ux-expert',
34
+ required_fields: ['files_changed', 'components_created', 'a11y_verified', 'summary'],
35
+ optional_fields: [],
36
+ schema: {
37
+ files_changed: { type: 'string[]', description: 'List of file paths created or modified', validation: 'file-paths' },
38
+ components_created: { type: 'string[]', description: 'List of new component file paths', validation: 'file-paths' },
39
+ a11y_verified: { type: 'boolean', description: 'Whether accessibility was verified' },
40
+ summary: { type: 'string', description: 'Brief description of UI/UX changes', validation: 'non-empty' },
41
+ },
42
+ },
43
+ 'testing-expert': {
44
+ agent: 'testing-expert',
45
+ required_fields: ['test_files', 'coverage_summary', 'summary'],
46
+ optional_fields: [],
47
+ schema: {
48
+ test_files: { type: 'string[]', description: 'List of test file paths created or modified', validation: 'file-paths' },
49
+ coverage_summary: { type: 'string', description: 'Summary of test coverage achieved', validation: 'non-empty' },
50
+ summary: { type: 'string', description: 'Brief description of testing work done', validation: 'non-empty' },
51
+ },
52
+ },
53
+ 'security-expert': {
54
+ agent: 'security-expert',
55
+ required_fields: ['findings', 'severity', 'files_reviewed', 'summary'],
56
+ optional_fields: [],
57
+ schema: {
58
+ findings: { type: 'string[]', description: 'List of security findings or issues found' },
59
+ severity: { type: 'string', description: 'Overall severity level (critical|high|medium|low|none)', validation: 'non-empty' },
60
+ files_reviewed: { type: 'string[]', description: 'List of file paths reviewed', validation: 'file-paths' },
61
+ summary: { type: 'string', description: 'Summary of security review', validation: 'non-empty' },
62
+ },
63
+ },
64
+ 'architect': {
65
+ agent: 'architect',
66
+ required_fields: ['decision', 'alternatives_considered', 'risks', 'summary'],
67
+ optional_fields: [],
68
+ schema: {
69
+ decision: { type: 'string', description: 'The architectural decision made', validation: 'non-empty' },
70
+ alternatives_considered: { type: 'string', description: 'Alternatives that were evaluated', validation: 'non-empty' },
71
+ risks: { type: 'string', description: 'Known risks of the decision', validation: 'non-empty' },
72
+ summary: { type: 'string', description: 'Brief summary of the architectural decision', validation: 'non-empty' },
73
+ },
74
+ },
75
+ 'researcher': {
76
+ agent: 'researcher',
77
+ required_fields: ['findings', 'sources', 'confidence', 'summary'],
78
+ optional_fields: [],
79
+ schema: {
80
+ findings: { type: 'string', description: 'Research findings and conclusions', validation: 'non-empty' },
81
+ sources: { type: 'string[]', description: 'List of sources referenced' },
82
+ confidence: { type: 'string', description: 'Confidence level (high|medium|low) with rationale', validation: 'non-empty' },
83
+ summary: { type: 'string', description: 'Brief summary of research', validation: 'non-empty' },
84
+ },
85
+ },
86
+ 'reviewer': {
87
+ agent: 'reviewer',
88
+ required_fields: ['verdict', 'issues', 'summary'],
89
+ optional_fields: [],
90
+ schema: {
91
+ verdict: { type: 'string', description: 'Review verdict (pass|block|conditional)', validation: 'non-empty' },
92
+ issues: { type: 'string[]', description: 'List of issues found during review' },
93
+ summary: { type: 'string', description: 'Brief review summary', validation: 'non-empty' },
94
+ },
95
+ },
96
+ 'documentation-writer': {
97
+ agent: 'documentation-writer',
98
+ required_fields: ['files_changed', 'summary'],
99
+ optional_fields: [],
100
+ schema: {
101
+ files_changed: { type: 'string[]', description: 'List of documentation file paths created or modified', validation: 'file-paths' },
102
+ summary: { type: 'string', description: 'Brief description of documentation changes', validation: 'non-empty' },
103
+ },
104
+ },
105
+ 'copywriter': {
106
+ agent: 'copywriter',
107
+ required_fields: ['content', 'word_count', 'summary'],
108
+ optional_fields: [],
109
+ schema: {
110
+ content: { type: 'string', description: 'The written content or key excerpts', validation: 'non-empty' },
111
+ word_count: { type: 'number', description: 'Total word count of written content', validation: 'positive-int' },
112
+ summary: { type: 'string', description: 'Brief summary of content created', validation: 'non-empty' },
113
+ },
114
+ },
115
+ 'performance-expert': {
116
+ agent: 'performance-expert',
117
+ required_fields: ['metrics_before', 'metrics_after', 'files_changed', 'summary'],
118
+ optional_fields: [],
119
+ schema: {
120
+ metrics_before: { type: 'object', description: 'Performance metrics before changes' },
121
+ metrics_after: { type: 'object', description: 'Performance metrics after changes' },
122
+ files_changed: { type: 'string[]', description: 'List of file paths created or modified', validation: 'file-paths' },
123
+ summary: { type: 'string', description: 'Summary of performance improvements', validation: 'non-empty' },
124
+ },
125
+ },
126
+ 'database-engineer': {
127
+ agent: 'database-engineer',
128
+ required_fields: ['migrations', 'rls_policies', 'rollback_plan', 'summary'],
129
+ optional_fields: [],
130
+ schema: {
131
+ migrations: { type: 'string[]', description: 'List of migration file paths applied', validation: 'file-paths' },
132
+ rls_policies: { type: 'string[]', description: 'List of RLS policy names added or modified' },
133
+ rollback_plan: { type: 'string', description: 'Steps to roll back the changes if needed', validation: 'non-empty' },
134
+ summary: { type: 'string', description: 'Summary of database changes', validation: 'non-empty' },
135
+ },
136
+ },
137
+ 'devops-expert': {
138
+ agent: 'devops-expert',
139
+ required_fields: ['files_changed', 'env_vars_added', 'summary'],
140
+ optional_fields: [],
141
+ schema: {
142
+ files_changed: { type: 'string[]', description: 'List of file paths created or modified', validation: 'file-paths' },
143
+ env_vars_added: { type: 'string[]', description: 'List of environment variable names added' },
144
+ summary: { type: 'string', description: 'Summary of DevOps changes', validation: 'non-empty' },
145
+ },
146
+ },
147
+ 'api-designer': {
148
+ agent: 'api-designer',
149
+ required_fields: ['endpoints', 'schemas', 'summary'],
150
+ optional_fields: [],
151
+ schema: {
152
+ endpoints: { type: 'string[]', description: 'List of API endpoints designed or modified' },
153
+ schemas: { type: 'string[]', description: 'List of request/response schema names' },
154
+ summary: { type: 'string', description: 'Summary of API design decisions', validation: 'non-empty' },
155
+ },
156
+ },
157
+ 'data-expert': {
158
+ agent: 'data-expert',
159
+ required_fields: ['pipeline_steps', 'files_changed', 'summary'],
160
+ optional_fields: [],
161
+ schema: {
162
+ pipeline_steps: { type: 'string[]', description: 'List of data pipeline steps implemented' },
163
+ files_changed: { type: 'string[]', description: 'List of file paths created or modified', validation: 'file-paths' },
164
+ summary: { type: 'string', description: 'Summary of data engineering work', validation: 'non-empty' },
165
+ },
166
+ },
167
+ 'seo-specialist': {
168
+ agent: 'seo-specialist',
169
+ required_fields: ['files_changed', 'tags_added', 'summary'],
170
+ optional_fields: [],
171
+ schema: {
172
+ files_changed: { type: 'string[]', description: 'List of file paths created or modified', validation: 'file-paths' },
173
+ tags_added: { type: 'string[]', description: 'List of SEO tags or structured data items added' },
174
+ summary: { type: 'string', description: 'Summary of SEO changes', validation: 'non-empty' },
175
+ },
176
+ },
177
+ 'release-manager': {
178
+ agent: 'release-manager',
179
+ required_fields: ['version', 'changelog_entries', 'checks_passed', 'summary'],
180
+ optional_fields: [],
181
+ schema: {
182
+ version: { type: 'string', description: 'The version string being released (e.g. 1.2.3)', validation: 'non-empty' },
183
+ changelog_entries: { type: 'string[]', description: 'List of changelog entries for this release' },
184
+ checks_passed: { type: 'boolean', description: 'Whether all release checks passed' },
185
+ summary: { type: 'string', description: 'Summary of the release', validation: 'non-empty' },
186
+ },
187
+ },
188
+ }
189
+
190
+ function applyValidation(
191
+ field: string,
192
+ value: unknown,
193
+ spec: FieldSpec,
194
+ missing: string[],
195
+ ): void {
196
+ if (spec.validation === 'non-empty') {
197
+ if (typeof value !== 'string' || value.trim() === '') {
198
+ missing.push(field)
199
+ }
200
+ } else if (spec.validation === 'file-paths') {
201
+ if (!Array.isArray(value) || !value.every(v => typeof v === 'string')) {
202
+ missing.push(field)
203
+ }
204
+ } else if (spec.validation === 'positive-int') {
205
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
206
+ missing.push(field)
207
+ }
208
+ }
209
+ }
210
+
211
+ export function validateOutput(agent: string, output: string): ContractResult {
212
+ const contract = AGENT_CONTRACTS[agent.toLowerCase()]
213
+ if (!contract) {
214
+ return { valid: true, missing: [], warnings: ['no_contract_defined'] }
215
+ }
216
+
217
+ const blockMatch = output.match(/<!--\s*OUTPUT_CONTRACT\s*([\s\S]*?)-->/)
218
+ if (!blockMatch) {
219
+ return { valid: false, missing: ['__contract_block'], warnings: [] }
220
+ }
221
+
222
+ let data: Record<string, unknown>
223
+ try {
224
+ data = JSON.parse(blockMatch[1].trim()) as Record<string, unknown>
225
+ } catch {
226
+ return { valid: false, missing: ['__contract_block'], warnings: ['invalid_json'] }
227
+ }
228
+
229
+ const missing: string[] = []
230
+
231
+ for (const field of contract.required_fields) {
232
+ if (!(field in data) || data[field] === null || data[field] === undefined) {
233
+ missing.push(field)
234
+ continue
235
+ }
236
+ const fieldSpec = contract.schema[field]
237
+ if (fieldSpec?.validation) {
238
+ applyValidation(field, data[field], fieldSpec, missing)
239
+ }
240
+ }
241
+
242
+ return { valid: missing.length === 0, missing, warnings: [], data }
243
+ }
244
+
245
+ export function buildContractInstruction(agent: string): string | null {
246
+ const contract = AGENT_CONTRACTS[agent.toLowerCase()]
247
+ if (!contract) return null
248
+
249
+ const exampleFields = contract.required_fields
250
+ .map(f => {
251
+ const spec = contract.schema[f]
252
+ if (!spec) return `"${f}": "..."`
253
+ if (spec.type === 'string[]') return `"${f}": [...]`
254
+ if (spec.type === 'boolean') return `"${f}": true`
255
+ if (spec.type === 'number') return `"${f}": 0`
256
+ if (spec.type === 'object') return `"${f}": {}`
257
+ return `"${f}": "..."`
258
+ })
259
+ .join(', ')
260
+
261
+ return [
262
+ '## Output Contract (REQUIRED)',
263
+ 'At the END of your response, include an OUTPUT_CONTRACT block:',
264
+ `<!-- OUTPUT_CONTRACT`,
265
+ `{ ${exampleFields} }`,
266
+ `-->`,
267
+ `The following fields are REQUIRED: ${contract.required_fields.join(', ')}`,
268
+ ].join('\n')
269
+ }
270
+
271
+ export function buildContractRetryPrompt(result: ContractResult): string {
272
+ return [
273
+ 'Your previous output was missing the required OUTPUT_CONTRACT block.',
274
+ 'At the END of your response, include:',
275
+ '<!-- OUTPUT_CONTRACT',
276
+ '{ ... }',
277
+ '-->',
278
+ `Missing fields: ${result.missing.join(', ')}`,
279
+ ].join('\n')
280
+ }
@@ -0,0 +1,349 @@
1
+ import { mkdtempSync, rmSync, realpathSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
5
+ import { createConvoyStore } from './store.js'
6
+ import type { ConvoyStore } from './store.js'
7
+ import {
8
+ extractExecutionHistory,
9
+ clusterConvoys,
10
+ analyzeAgentPerformance,
11
+ generateInsights,
12
+ analyzeDAG,
13
+ formatInsightsMarkdown,
14
+ } from './dag-analysis.js'
15
+
16
+ // ── helpers ───────────────────────────────────────────────────────────────────
17
+
18
+ let tmpDir: string
19
+ let dbPath: string
20
+ let store: ConvoyStore
21
+
22
+ beforeEach(() => {
23
+ tmpDir = realpathSync(mkdtempSync(join(tmpdir(), 'dag-test-')))
24
+ dbPath = join(tmpDir, 'test.db')
25
+ store = createConvoyStore(dbPath)
26
+ })
27
+
28
+ afterEach(() => {
29
+ store.close()
30
+ rmSync(tmpDir, { recursive: true, force: true })
31
+ })
32
+
33
+ type ConvoyInsert = Parameters<ConvoyStore['insertConvoy']>[0]
34
+ type TaskInsert = Parameters<ConvoyStore['insertTask']>[0]
35
+
36
+ function makeConvoy(overrides: Partial<ConvoyInsert> = {}): ConvoyInsert {
37
+ return {
38
+ id: 'convoy-1',
39
+ name: 'Test Convoy',
40
+ spec_hash: 'abc123',
41
+ status: 'pending',
42
+ branch: null,
43
+ created_at: new Date().toISOString(),
44
+ spec_yaml: 'name: test\nconcurrency: 2',
45
+ pipeline_id: null,
46
+ ...overrides,
47
+ }
48
+ }
49
+
50
+ function makeTask(overrides: Partial<TaskInsert> = {}): TaskInsert {
51
+ return {
52
+ id: 'task-1',
53
+ convoy_id: 'convoy-1',
54
+ phase: 0,
55
+ prompt: 'Do something',
56
+ agent: 'developer',
57
+ adapter: null,
58
+ model: null,
59
+ timeout_ms: 1_800_000,
60
+ status: 'pending',
61
+ retries: 0,
62
+ max_retries: 1,
63
+ files: null,
64
+ depends_on: null,
65
+ gates: null,
66
+ ...overrides,
67
+ } as TaskInsert
68
+ }
69
+
70
+ const NOW = new Date().toISOString()
71
+ const LONG_AGO = new Date(Date.now() - 200 * 24 * 60 * 60 * 1000).toISOString()
72
+
73
+ // ── extractExecutionHistory ───────────────────────────────────────────────────
74
+
75
+ describe('extractExecutionHistory', () => {
76
+ it('returns empty arrays for an empty database', () => {
77
+ const result = extractExecutionHistory(store)
78
+ expect(result.convoys).toHaveLength(0)
79
+ expect(result.tasks).toHaveLength(0)
80
+ })
81
+
82
+ it('filters to only done/failed convoys', () => {
83
+ store.insertConvoy(makeConvoy({ id: 'c1', status: 'pending' }))
84
+ store.insertConvoy(makeConvoy({ id: 'c2', status: 'running' }))
85
+ store.insertConvoy(makeConvoy({ id: 'c3', status: 'done' }))
86
+ store.insertConvoy(makeConvoy({ id: 'c4', status: 'failed' }))
87
+ store.updateConvoyStatus('c3', 'done', { finished_at: NOW, started_at: NOW })
88
+ store.updateConvoyStatus('c4', 'failed', { finished_at: NOW, started_at: NOW })
89
+
90
+ const result = extractExecutionHistory(store)
91
+ const ids = result.convoys.map((c) => c.id)
92
+ expect(ids).toContain('c3')
93
+ expect(ids).toContain('c4')
94
+ expect(ids).not.toContain('c1')
95
+ expect(ids).not.toContain('c2')
96
+ })
97
+
98
+ it('respects sinceDays filter and excludes old convoys', () => {
99
+ store.insertConvoy(makeConvoy({ id: 'old', status: 'done' }))
100
+ store.updateConvoyStatus('old', 'done', { finished_at: LONG_AGO, started_at: LONG_AGO })
101
+
102
+ store.insertConvoy(makeConvoy({ id: 'recent', status: 'done' }))
103
+ store.updateConvoyStatus('recent', 'done', { finished_at: NOW, started_at: NOW })
104
+
105
+ const result = extractExecutionHistory(store, 90)
106
+ const ids = result.convoys.map((c) => c.id)
107
+ expect(ids).toContain('recent')
108
+ expect(ids).not.toContain('old')
109
+ })
110
+
111
+ it('includes tasks for matching convoys', () => {
112
+ store.insertConvoy(makeConvoy({ id: 'c1', status: 'done' }))
113
+ store.updateConvoyStatus('c1', 'done', { finished_at: NOW, started_at: NOW })
114
+ store.insertTask(makeTask({ id: 't1', convoy_id: 'c1' }))
115
+ store.insertTask(makeTask({ id: 't2', convoy_id: 'c1' }))
116
+
117
+ const result = extractExecutionHistory(store)
118
+ expect(result.tasks).toHaveLength(2)
119
+ })
120
+ })
121
+
122
+ // ── clusterConvoys ────────────────────────────────────────────────────────────
123
+
124
+ describe('clusterConvoys', () => {
125
+ it('returns empty array for no convoys', () => {
126
+ expect(clusterConvoys([], [])).toHaveLength(0)
127
+ })
128
+
129
+ it('assigns correct bucket for small task count', () => {
130
+ store.insertConvoy(makeConvoy({ id: 'c1', status: 'done' }))
131
+ store.updateConvoyStatus('c1', 'done', { finished_at: NOW, started_at: NOW })
132
+
133
+ const convoys = [store.getConvoy('c1')!]
134
+ const tasks = [makeTask({ id: 't1', convoy_id: 'c1' })]
135
+
136
+ const patterns = clusterConvoys(convoys, tasks as never)
137
+ expect(patterns).toHaveLength(1)
138
+ expect(patterns[0].name).toMatch(/^small/)
139
+ })
140
+
141
+ it('convoys with same agent set cluster together', () => {
142
+ store.insertConvoy(makeConvoy({ id: 'c1', status: 'done' }))
143
+ store.updateConvoyStatus('c1', 'done', { finished_at: NOW, started_at: NOW })
144
+ store.insertConvoy(makeConvoy({ id: 'c2', status: 'done' }))
145
+ store.updateConvoyStatus('c2', 'done', { finished_at: NOW, started_at: NOW })
146
+
147
+ const convoys = [store.getConvoy('c1')!, store.getConvoy('c2')!]
148
+ const tasks = [
149
+ makeTask({ id: 't1', convoy_id: 'c1', agent: 'developer' }),
150
+ makeTask({ id: 't2', convoy_id: 'c2', agent: 'developer' }),
151
+ ]
152
+
153
+ const patterns = clusterConvoys(convoys, tasks as never)
154
+ expect(patterns).toHaveLength(1)
155
+ expect(patterns[0].sample_size).toBe(2)
156
+ })
157
+
158
+ it('calculates success rate correctly', () => {
159
+ store.insertConvoy(makeConvoy({ id: 'c1', status: 'done' }))
160
+ store.updateConvoyStatus('c1', 'done', { finished_at: NOW, started_at: NOW })
161
+ store.insertConvoy(makeConvoy({ id: 'c2', status: 'failed' }))
162
+ store.updateConvoyStatus('c2', 'failed', { finished_at: NOW, started_at: NOW })
163
+
164
+ const convoys = [store.getConvoy('c1')!, store.getConvoy('c2')!]
165
+ const tasks = [
166
+ makeTask({ id: 't1', convoy_id: 'c1', agent: 'developer' }),
167
+ makeTask({ id: 't2', convoy_id: 'c2', agent: 'developer' }),
168
+ ]
169
+
170
+ const patterns = clusterConvoys(convoys, tasks as never)
171
+ expect(patterns).toHaveLength(1)
172
+ expect(patterns[0].success_rate).toBe(0.5)
173
+ })
174
+ })
175
+
176
+ // ── analyzeAgentPerformance ───────────────────────────────────────────────────
177
+
178
+ describe('analyzeAgentPerformance', () => {
179
+ it('returns empty array for no tasks', () => {
180
+ expect(analyzeAgentPerformance([])).toHaveLength(0)
181
+ })
182
+
183
+ it('computes correct success rate for mixed tasks', () => {
184
+ store.insertConvoy(makeConvoy({ id: 'c1', status: 'done' }))
185
+ store.insertTask(makeTask({ id: 't1', convoy_id: 'c1', status: 'done', agent: 'developer' }))
186
+ store.insertTask(makeTask({ id: 't2', convoy_id: 'c1', status: 'done', agent: 'developer' }))
187
+ store.insertTask(makeTask({ id: 't3', convoy_id: 'c1', status: 'failed', agent: 'developer' }))
188
+ store.updateTaskStatus('t1', 'c1', 'done')
189
+ store.updateTaskStatus('t2', 'c1', 'done')
190
+ store.updateTaskStatus('t3', 'c1', 'failed')
191
+
192
+ const tasks = store.getTasksByConvoy('c1')
193
+ const result = analyzeAgentPerformance(tasks)
194
+ expect(result).toHaveLength(1)
195
+ expect(result[0].success_rate).toBeCloseTo(2 / 3)
196
+ expect(result[0].total_tasks).toBe(3)
197
+ })
198
+
199
+ it('identifies best file patterns', () => {
200
+ const files = JSON.stringify(['src/cli/convoy/engine.ts', 'src/cli/convoy/store.ts'])
201
+ store.insertConvoy(makeConvoy({ id: 'c1' }))
202
+ store.insertTask(makeTask({ id: 't1', convoy_id: 'c1', status: 'done', agent: 'developer', files }))
203
+ store.insertTask(makeTask({ id: 't2', convoy_id: 'c1', status: 'done', agent: 'developer', files }))
204
+ store.insertTask(makeTask({ id: 't3', convoy_id: 'c1', status: 'done', agent: 'developer', files }))
205
+ store.updateTaskStatus('t1', 'c1', 'done')
206
+ store.updateTaskStatus('t2', 'c1', 'done')
207
+ store.updateTaskStatus('t3', 'c1', 'done')
208
+
209
+ const tasks = store.getTasksByConvoy('c1')
210
+ const result = analyzeAgentPerformance(tasks)
211
+ expect(result[0].best_file_patterns).toContain('src/cli')
212
+ })
213
+
214
+ it('identifies worst file patterns for low success rate', () => {
215
+ const files = JSON.stringify(['src/dashboard/page.ts', 'src/dashboard/layout.ts'])
216
+ store.insertConvoy(makeConvoy({ id: 'c1' }))
217
+ store.insertTask(makeTask({ id: 't1', convoy_id: 'c1', status: 'failed', agent: 'developer', files }))
218
+ store.insertTask(makeTask({ id: 't2', convoy_id: 'c1', status: 'failed', agent: 'developer', files }))
219
+ store.insertTask(makeTask({ id: 't3', convoy_id: 'c1', status: 'failed', agent: 'developer', files }))
220
+ store.updateTaskStatus('t1', 'c1', 'failed')
221
+ store.updateTaskStatus('t2', 'c1', 'failed')
222
+ store.updateTaskStatus('t3', 'c1', 'failed')
223
+
224
+ const tasks = store.getTasksByConvoy('c1')
225
+ const result = analyzeAgentPerformance(tasks)
226
+ expect(result[0].worst_file_patterns).toContain('src/dashboard')
227
+ })
228
+
229
+ it('returns avg_duration_ms of 0 when no tasks have timestamps', () => {
230
+ store.insertConvoy(makeConvoy({ id: 'c1' }))
231
+ store.insertTask(makeTask({ id: 't1', convoy_id: 'c1', status: 'done', agent: 'developer' }))
232
+
233
+ const tasks = store.getTasksByConvoy('c1')
234
+ const result = analyzeAgentPerformance(tasks)
235
+ expect(result[0].avg_duration_ms).toBe(0)
236
+ })
237
+ })
238
+
239
+ // ── generateInsights ──────────────────────────────────────────────────────────
240
+
241
+ describe('generateInsights', () => {
242
+ it('returns no-data message for empty inputs', () => {
243
+ const insights = generateInsights([], [])
244
+ expect(insights).toHaveLength(1)
245
+ expect(insights[0]).toMatch(/No execution history/)
246
+ })
247
+
248
+ it('produces warning for low success rate agent', () => {
249
+ const agents = [
250
+ {
251
+ agent: 'developer',
252
+ total_tasks: 10,
253
+ success_rate: 0.5,
254
+ avg_duration_ms: 0,
255
+ avg_tokens: 0,
256
+ avg_retries: 0,
257
+ best_file_patterns: [],
258
+ worst_file_patterns: [],
259
+ },
260
+ ]
261
+ const insights = generateInsights([], agents)
262
+ expect(insights.some((i) => i.includes('⚠️') && i.includes('developer'))).toBe(true)
263
+ expect(insights.some((i) => i.includes('50%'))).toBe(true)
264
+ })
265
+
266
+ it('produces 100% success message for perfect agent with enough tasks', () => {
267
+ const agents = [
268
+ {
269
+ agent: 'testing-expert',
270
+ total_tasks: 5,
271
+ success_rate: 1,
272
+ avg_duration_ms: 0,
273
+ avg_tokens: 0,
274
+ avg_retries: 0,
275
+ best_file_patterns: [],
276
+ worst_file_patterns: [],
277
+ },
278
+ ]
279
+ const insights = generateInsights([], agents)
280
+ expect(insights.some((i) => i.includes('100%') && i.includes('testing-expert'))).toBe(true)
281
+ })
282
+
283
+ it('includes pattern insight with concurrency', () => {
284
+ const patterns = [
285
+ {
286
+ name: 'small-developer',
287
+ task_count_range: [1, 2] as [number, number],
288
+ agent_sequence: ['developer'],
289
+ avg_duration_ms: 120000,
290
+ avg_tokens: 5000,
291
+ success_rate: 0.9,
292
+ common_failure_agents: [],
293
+ recommended_concurrency: 2,
294
+ sample_size: 5,
295
+ },
296
+ ]
297
+ const insights = generateInsights(patterns, [])
298
+ expect(insights.some((i) => i.includes('concurrency 2'))).toBe(true)
299
+ })
300
+ })
301
+
302
+ // ── analyzeDAG ────────────────────────────────────────────────────────────────
303
+
304
+ describe('analyzeDAG', () => {
305
+ it('returns valid structure for empty database (no crash)', () => {
306
+ const result = analyzeDAG(store)
307
+ expect(result).toHaveProperty('patterns')
308
+ expect(result).toHaveProperty('agent_stats')
309
+ expect(result).toHaveProperty('insights')
310
+ expect(result).toHaveProperty('generated_at')
311
+ expect(Array.isArray(result.patterns)).toBe(true)
312
+ expect(Array.isArray(result.agent_stats)).toBe(true)
313
+ expect(Array.isArray(result.insights)).toBe(true)
314
+ })
315
+
316
+ it('returns populated agent data when history exists', () => {
317
+ store.insertConvoy(makeConvoy({ id: 'c1', status: 'done' }))
318
+ store.updateConvoyStatus('c1', 'done', { finished_at: NOW, started_at: NOW })
319
+ store.insertTask(makeTask({ id: 't1', convoy_id: 'c1', status: 'done', agent: 'developer' }))
320
+ store.updateTaskStatus('t1', 'c1', 'done', { finished_at: NOW, started_at: NOW, total_tokens: 5000 })
321
+
322
+ const result = analyzeDAG(store)
323
+ expect(result.agent_stats.length).toBeGreaterThan(0)
324
+ expect(result.agent_stats[0].agent).toBe('developer')
325
+ })
326
+ })
327
+
328
+ // ── formatInsightsMarkdown ────────────────────────────────────────────────────
329
+
330
+ describe('formatInsightsMarkdown', () => {
331
+ it('contains expected section headers', () => {
332
+ const rec = analyzeDAG(store)
333
+ const md = formatInsightsMarkdown(rec)
334
+ expect(md).toContain('## Convoy Patterns')
335
+ expect(md).toContain('## Agent Performance')
336
+ expect(md).toContain('## Recommendations')
337
+ })
338
+
339
+ it('includes agent table for populated data', () => {
340
+ store.insertConvoy(makeConvoy({ id: 'c1', status: 'done' }))
341
+ store.updateConvoyStatus('c1', 'done', { finished_at: NOW, started_at: NOW })
342
+ store.insertTask(makeTask({ id: 't1', convoy_id: 'c1', status: 'done', agent: 'developer' }))
343
+ store.updateTaskStatus('t1', 'c1', 'done', { finished_at: NOW, started_at: NOW, total_tokens: 5000 })
344
+
345
+ const rec = analyzeDAG(store)
346
+ const md = formatInsightsMarkdown(rec)
347
+ expect(md).toContain('developer')
348
+ })
349
+ })