opencastle 0.31.7 → 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/README.md +4 -0
- 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 +280 -6
- 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/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/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/package.json +2 -1
- 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 +301 -7
- 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/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/skills.ts +121 -0
- package/src/cli/types.ts +4 -1
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/orchestrator/prompts/assess-complexity.prompt.md +13 -0
- package/src/orchestrator/prompts/generate-convoy.prompt.md +19 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
AGENT_CONTRACTS,
|
|
4
|
+
validateOutput,
|
|
5
|
+
buildContractInstruction,
|
|
6
|
+
buildContractRetryPrompt,
|
|
7
|
+
} from './contracts.js'
|
|
8
|
+
|
|
9
|
+
// ── Registry well-formedness ──────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
describe('AGENT_CONTRACTS registry', () => {
|
|
12
|
+
it('has entries for all expected agents', () => {
|
|
13
|
+
const expectedAgents = [
|
|
14
|
+
'developer', 'ui-ux-expert', 'testing-expert', 'security-expert',
|
|
15
|
+
'architect', 'researcher', 'reviewer', 'documentation-writer',
|
|
16
|
+
'copywriter', 'performance-expert', 'database-engineer', 'devops-expert',
|
|
17
|
+
'api-designer', 'data-expert', 'seo-specialist', 'release-manager',
|
|
18
|
+
]
|
|
19
|
+
for (const agent of expectedAgents) {
|
|
20
|
+
expect(AGENT_CONTRACTS).toHaveProperty(agent)
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('every contract has non-empty required_fields', () => {
|
|
25
|
+
for (const [key, contract] of Object.entries(AGENT_CONTRACTS)) {
|
|
26
|
+
expect(contract.required_fields.length, `${key} should have required_fields`).toBeGreaterThan(0)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('every required field has a schema entry', () => {
|
|
31
|
+
for (const [key, contract] of Object.entries(AGENT_CONTRACTS)) {
|
|
32
|
+
for (const field of contract.required_fields) {
|
|
33
|
+
expect(contract.schema, `${key}.${field} missing schema entry`).toHaveProperty(field)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('every schema entry has a valid type', () => {
|
|
39
|
+
const validTypes = new Set(['string', 'string[]', 'number', 'boolean', 'object'])
|
|
40
|
+
for (const [key, contract] of Object.entries(AGENT_CONTRACTS)) {
|
|
41
|
+
for (const [field, spec] of Object.entries(contract.schema)) {
|
|
42
|
+
expect(validTypes.has(spec.type), `${key}.${field} has invalid type "${spec.type}"`).toBe(true)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('every schema entry has a description', () => {
|
|
48
|
+
for (const [key, contract] of Object.entries(AGENT_CONTRACTS)) {
|
|
49
|
+
for (const [field, spec] of Object.entries(contract.schema)) {
|
|
50
|
+
expect(spec.description, `${key}.${field} missing description`).toBeTruthy()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('all optional_fields arrays are empty', () => {
|
|
56
|
+
for (const [key, contract] of Object.entries(AGENT_CONTRACTS)) {
|
|
57
|
+
expect(contract.optional_fields, `${key} should have empty optional_fields`).toHaveLength(0)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// ── validateOutput ────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
describe('validateOutput', () => {
|
|
65
|
+
it('returns valid for a complete developer contract block', () => {
|
|
66
|
+
const output = `
|
|
67
|
+
Some work done here.
|
|
68
|
+
|
|
69
|
+
<!-- OUTPUT_CONTRACT
|
|
70
|
+
{ "files_changed": ["src/foo.ts"], "tests_added": ["src/foo.test.ts"], "summary": "Added foo feature" }
|
|
71
|
+
-->
|
|
72
|
+
`
|
|
73
|
+
const result = validateOutput('developer', output)
|
|
74
|
+
expect(result.valid).toBe(true)
|
|
75
|
+
expect(result.missing).toHaveLength(0)
|
|
76
|
+
expect(result.warnings).toHaveLength(0)
|
|
77
|
+
expect(result.data).toBeDefined()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('returns valid=false with missing __contract_block when no block present', () => {
|
|
81
|
+
const result = validateOutput('developer', 'No contract block here.')
|
|
82
|
+
expect(result.valid).toBe(false)
|
|
83
|
+
expect(result.missing).toContain('__contract_block')
|
|
84
|
+
expect(result.warnings).toHaveLength(0)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('returns valid=false with invalid_json warning when JSON is malformed', () => {
|
|
88
|
+
const output = '<!-- OUTPUT_CONTRACT\n{ invalid json }\n-->'
|
|
89
|
+
const result = validateOutput('developer', output)
|
|
90
|
+
expect(result.valid).toBe(false)
|
|
91
|
+
expect(result.missing).toContain('__contract_block')
|
|
92
|
+
expect(result.warnings).toContain('invalid_json')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('lists missing required fields', () => {
|
|
96
|
+
const output = '<!-- OUTPUT_CONTRACT\n{ "files_changed": ["src/foo.ts"] }\n-->'
|
|
97
|
+
const result = validateOutput('developer', output)
|
|
98
|
+
expect(result.valid).toBe(false)
|
|
99
|
+
expect(result.missing).toContain('tests_added')
|
|
100
|
+
expect(result.missing).toContain('summary')
|
|
101
|
+
expect(result.missing).not.toContain('files_changed')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('returns valid=true with no_contract_defined warning for unknown agent', () => {
|
|
105
|
+
const result = validateOutput('unknown-agent-xyz', 'any output')
|
|
106
|
+
expect(result.valid).toBe(true)
|
|
107
|
+
expect(result.missing).toHaveLength(0)
|
|
108
|
+
expect(result.warnings).toContain('no_contract_defined')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('is case-insensitive for agent name lookup', () => {
|
|
112
|
+
const output = '<!-- OUTPUT_CONTRACT\n{ "files_changed": ["src/foo.ts"], "tests_added": [], "summary": "done" }\n-->'
|
|
113
|
+
const result = validateOutput('Developer', output)
|
|
114
|
+
expect(result.valid).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('works for all registered agents with minimal valid data', () => {
|
|
118
|
+
const minimalData: Record<string, Record<string, unknown>> = {
|
|
119
|
+
'developer': { files_changed: ['src/foo.ts'], tests_added: ['src/foo.test.ts'], summary: 'done' },
|
|
120
|
+
'ui-ux-expert': { files_changed: ['src/btn.tsx'], components_created: ['src/btn.tsx'], a11y_verified: true, summary: 'done' },
|
|
121
|
+
'testing-expert': { test_files: ['src/foo.test.ts'], coverage_summary: '95%', summary: 'done' },
|
|
122
|
+
'security-expert': { findings: [], severity: 'none', files_reviewed: ['src/auth.ts'], summary: 'done' },
|
|
123
|
+
'architect': { decision: 'use microservices', alternatives_considered: 'monolith', risks: 'complexity', summary: 'done' },
|
|
124
|
+
'researcher': { findings: 'found stuff', sources: ['https://example.com'], confidence: 'high', summary: 'done' },
|
|
125
|
+
'reviewer': { verdict: 'pass', issues: [], summary: 'done' },
|
|
126
|
+
'documentation-writer': { files_changed: ['docs/readme.md'], summary: 'done' },
|
|
127
|
+
'copywriter': { content: 'some content', word_count: 50, summary: 'done' },
|
|
128
|
+
'performance-expert': { metrics_before: {}, metrics_after: {}, files_changed: [], summary: 'done' },
|
|
129
|
+
'database-engineer': { migrations: [], rls_policies: [], rollback_plan: 'delete rows', summary: 'done' },
|
|
130
|
+
'devops-expert': { files_changed: [], env_vars_added: [], summary: 'done' },
|
|
131
|
+
'api-designer': { endpoints: ['/api/v1/resource'], schemas: ['ResourceSchema'], summary: 'done' },
|
|
132
|
+
'data-expert': { pipeline_steps: ['extract', 'transform'], files_changed: [], summary: 'done' },
|
|
133
|
+
'seo-specialist': { files_changed: [], tags_added: ['og:title'], summary: 'done' },
|
|
134
|
+
'release-manager': { version: '1.0.0', changelog_entries: ['feat: new thing'], checks_passed: true, summary: 'done' },
|
|
135
|
+
}
|
|
136
|
+
for (const [agent, data] of Object.entries(minimalData)) {
|
|
137
|
+
const output = `<!-- OUTPUT_CONTRACT\n${JSON.stringify(data)}\n-->`
|
|
138
|
+
const result = validateOutput(agent, output)
|
|
139
|
+
expect(result.valid, `${agent} should be valid with minimal data`).toBe(true)
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// ── Validation rules ──────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
describe('validateOutput validation rules', () => {
|
|
147
|
+
it('non-empty rejects empty string for summary', () => {
|
|
148
|
+
const output = '<!-- OUTPUT_CONTRACT\n{ "files_changed": ["src/foo.ts"], "tests_added": [], "summary": "" }\n-->'
|
|
149
|
+
const result = validateOutput('developer', output)
|
|
150
|
+
expect(result.valid).toBe(false)
|
|
151
|
+
expect(result.missing).toContain('summary')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('non-empty rejects whitespace-only string for summary', () => {
|
|
155
|
+
const output = '<!-- OUTPUT_CONTRACT\n{ "files_changed": ["src/foo.ts"], "tests_added": [], "summary": " " }\n-->'
|
|
156
|
+
const result = validateOutput('developer', output)
|
|
157
|
+
expect(result.valid).toBe(false)
|
|
158
|
+
expect(result.missing).toContain('summary')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('file-paths rejects non-array for files_changed', () => {
|
|
162
|
+
const output = '<!-- OUTPUT_CONTRACT\n{ "files_changed": "src/foo.ts", "tests_added": [], "summary": "done" }\n-->'
|
|
163
|
+
const result = validateOutput('developer', output)
|
|
164
|
+
expect(result.valid).toBe(false)
|
|
165
|
+
expect(result.missing).toContain('files_changed')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('file-paths rejects array with non-string values', () => {
|
|
169
|
+
const output = '<!-- OUTPUT_CONTRACT\n{ "files_changed": [1, 2], "tests_added": [], "summary": "done" }\n-->'
|
|
170
|
+
const result = validateOutput('developer', output)
|
|
171
|
+
expect(result.valid).toBe(false)
|
|
172
|
+
expect(result.missing).toContain('files_changed')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('file-paths accepts empty array', () => {
|
|
176
|
+
const output = '<!-- OUTPUT_CONTRACT\n{ "files_changed": [], "tests_added": [], "summary": "done" }\n-->'
|
|
177
|
+
const result = validateOutput('developer', output)
|
|
178
|
+
expect(result.valid).toBe(true)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('positive-int rejects zero for word_count', () => {
|
|
182
|
+
const output = '<!-- OUTPUT_CONTRACT\n{ "content": "text", "word_count": 0, "summary": "done" }\n-->'
|
|
183
|
+
const result = validateOutput('copywriter', output)
|
|
184
|
+
expect(result.valid).toBe(false)
|
|
185
|
+
expect(result.missing).toContain('word_count')
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('positive-int rejects negative number for word_count', () => {
|
|
189
|
+
const output = '<!-- OUTPUT_CONTRACT\n{ "content": "text", "word_count": -5, "summary": "done" }\n-->'
|
|
190
|
+
const result = validateOutput('copywriter', output)
|
|
191
|
+
expect(result.valid).toBe(false)
|
|
192
|
+
expect(result.missing).toContain('word_count')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('positive-int rejects non-number for word_count', () => {
|
|
196
|
+
const output = '<!-- OUTPUT_CONTRACT\n{ "content": "text", "word_count": "fifty", "summary": "done" }\n-->'
|
|
197
|
+
const result = validateOutput('copywriter', output)
|
|
198
|
+
expect(result.valid).toBe(false)
|
|
199
|
+
expect(result.missing).toContain('word_count')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('positive-int accepts valid positive number for word_count', () => {
|
|
203
|
+
const output = '<!-- OUTPUT_CONTRACT\n{ "content": "text", "word_count": 100, "summary": "done" }\n-->'
|
|
204
|
+
const result = validateOutput('copywriter', output)
|
|
205
|
+
expect(result.valid).toBe(true)
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// ── buildContractInstruction ──────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
describe('buildContractInstruction', () => {
|
|
212
|
+
it('returns instruction string for known agents', () => {
|
|
213
|
+
const instruction = buildContractInstruction('developer')
|
|
214
|
+
expect(instruction).not.toBeNull()
|
|
215
|
+
expect(instruction).toContain('OUTPUT_CONTRACT')
|
|
216
|
+
expect(instruction).toContain('files_changed')
|
|
217
|
+
expect(instruction).toContain('tests_added')
|
|
218
|
+
expect(instruction).toContain('summary')
|
|
219
|
+
expect(instruction).toContain('REQUIRED')
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('returns null for unknown agents', () => {
|
|
223
|
+
const instruction = buildContractInstruction('unknown-agent-xyz')
|
|
224
|
+
expect(instruction).toBeNull()
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('is case-insensitive for agent lookup', () => {
|
|
228
|
+
const instruction = buildContractInstruction('Developer')
|
|
229
|
+
expect(instruction).not.toBeNull()
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('includes all required fields in the REQUIRED list', () => {
|
|
233
|
+
for (const [agent, contract] of Object.entries(AGENT_CONTRACTS)) {
|
|
234
|
+
const instruction = buildContractInstruction(agent)
|
|
235
|
+
expect(instruction).not.toBeNull()
|
|
236
|
+
for (const field of contract.required_fields) {
|
|
237
|
+
expect(instruction).toContain(field)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('includes comment block markers', () => {
|
|
243
|
+
const instruction = buildContractInstruction('developer')
|
|
244
|
+
expect(instruction).toContain('<!-- OUTPUT_CONTRACT')
|
|
245
|
+
expect(instruction).toContain('-->')
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
// ── buildContractRetryPrompt ──────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
describe('buildContractRetryPrompt', () => {
|
|
252
|
+
it('includes missing field names in the prompt', () => {
|
|
253
|
+
const result = { valid: false, missing: ['files_changed', 'summary'], warnings: [] }
|
|
254
|
+
const prompt = buildContractRetryPrompt(result)
|
|
255
|
+
expect(prompt).toContain('files_changed')
|
|
256
|
+
expect(prompt).toContain('summary')
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('includes OUTPUT_CONTRACT block template', () => {
|
|
260
|
+
const result = { valid: false, missing: ['__contract_block'], warnings: [] }
|
|
261
|
+
const prompt = buildContractRetryPrompt(result)
|
|
262
|
+
expect(prompt).toContain('OUTPUT_CONTRACT')
|
|
263
|
+
expect(prompt).toContain('-->')
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('mentions that the previous output was missing the block', () => {
|
|
267
|
+
const result = { valid: false, missing: ['__contract_block'], warnings: [] }
|
|
268
|
+
const prompt = buildContractRetryPrompt(result)
|
|
269
|
+
expect(prompt.toLowerCase()).toContain('missing')
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('handles multiple missing fields', () => {
|
|
273
|
+
const result = { valid: false, missing: ['field_a', 'field_b', 'field_c'], warnings: [] }
|
|
274
|
+
const prompt = buildContractRetryPrompt(result)
|
|
275
|
+
expect(prompt).toContain('field_a')
|
|
276
|
+
expect(prompt).toContain('field_b')
|
|
277
|
+
expect(prompt).toContain('field_c')
|
|
278
|
+
})
|
|
279
|
+
})
|
|
@@ -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
|
+
}
|