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,164 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join, resolve } from 'node:path'
|
|
3
|
+
import type { CompactionConfig } from './types.js'
|
|
4
|
+
|
|
5
|
+
// --- Types ---
|
|
6
|
+
|
|
7
|
+
export interface CompactionSummary {
|
|
8
|
+
task_id: string
|
|
9
|
+
convoy_id: string
|
|
10
|
+
phase: string
|
|
11
|
+
completed_steps: string[]
|
|
12
|
+
pending_steps: string[]
|
|
13
|
+
key_decisions: string[]
|
|
14
|
+
files_modified: string[]
|
|
15
|
+
artifact_refs: string[]
|
|
16
|
+
timestamp: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
20
|
+
'claude-opus-4-6': 200_000,
|
|
21
|
+
'claude-sonnet-4-6': 200_000,
|
|
22
|
+
'gemini-3.1-pro': 2_000_000,
|
|
23
|
+
'gpt-5.3-codex': 200_000,
|
|
24
|
+
'gpt-5-mini': 128_000,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DEFAULT_CONTEXT_WINDOW = 128_000
|
|
28
|
+
const MAX_COMPACTIONS_PER_TASK = 3
|
|
29
|
+
|
|
30
|
+
// --- Threshold detection ---
|
|
31
|
+
|
|
32
|
+
export function shouldCompact(
|
|
33
|
+
tokensUsed: number,
|
|
34
|
+
model: string,
|
|
35
|
+
config: CompactionConfig,
|
|
36
|
+
): boolean {
|
|
37
|
+
if (!config.enabled) return false
|
|
38
|
+
const contextWindow = MODEL_CONTEXT_WINDOWS[model] ?? DEFAULT_CONTEXT_WINDOW
|
|
39
|
+
return tokensUsed / contextWindow >= config.token_threshold_pct / 100
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- Compaction prompt ---
|
|
43
|
+
|
|
44
|
+
export function generateCompactionPrompt(taskId: string): string {
|
|
45
|
+
return [
|
|
46
|
+
'## Context Compaction Required',
|
|
47
|
+
'You are approaching your context limit. Before continuing, produce a COMPACTION_SUMMARY:',
|
|
48
|
+
'',
|
|
49
|
+
'<!-- COMPACTION_SUMMARY',
|
|
50
|
+
'{',
|
|
51
|
+
' "phase": "current work phase",',
|
|
52
|
+
' "completed_steps": ["step 1 done", "step 2 done"],',
|
|
53
|
+
' "pending_steps": ["step 3 todo", "step 4 todo"],',
|
|
54
|
+
' "key_decisions": ["chose approach A because..."],',
|
|
55
|
+
' "files_modified": ["src/foo.ts", "src/bar.ts"],',
|
|
56
|
+
' "artifact_refs": [".opencastle/artifacts/.../report.md"]',
|
|
57
|
+
'}',
|
|
58
|
+
'-->',
|
|
59
|
+
'',
|
|
60
|
+
'Be concise. Focus on WHAT was decided and WHAT remains, not HOW you got here.',
|
|
61
|
+
].join('\n')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// --- Parse compaction summary from agent output ---
|
|
65
|
+
|
|
66
|
+
export function parseCompactionSummary(
|
|
67
|
+
output: string,
|
|
68
|
+
taskId: string,
|
|
69
|
+
convoyId: string,
|
|
70
|
+
): CompactionSummary | null {
|
|
71
|
+
const match = output.match(/<!--\s*COMPACTION_SUMMARY\s*\n([\s\S]*?)-->/)
|
|
72
|
+
if (!match) return null
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(match[1].trim()) as Record<string, unknown>
|
|
76
|
+
return {
|
|
77
|
+
task_id: taskId,
|
|
78
|
+
convoy_id: convoyId,
|
|
79
|
+
phase: typeof parsed.phase === 'string' ? parsed.phase : 'unknown',
|
|
80
|
+
completed_steps: Array.isArray(parsed.completed_steps) ? parsed.completed_steps.filter((s): s is string => typeof s === 'string') : [],
|
|
81
|
+
pending_steps: Array.isArray(parsed.pending_steps) ? parsed.pending_steps.filter((s): s is string => typeof s === 'string') : [],
|
|
82
|
+
key_decisions: Array.isArray(parsed.key_decisions) ? parsed.key_decisions.filter((s): s is string => typeof s === 'string') : [],
|
|
83
|
+
files_modified: Array.isArray(parsed.files_modified) ? parsed.files_modified.filter((s): s is string => typeof s === 'string') : [],
|
|
84
|
+
artifact_refs: Array.isArray(parsed.artifact_refs) ? parsed.artifact_refs.filter((s): s is string => typeof s === 'string') : [],
|
|
85
|
+
timestamp: new Date().toISOString(),
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- Save / restore ---
|
|
93
|
+
|
|
94
|
+
export function getCompactionDir(convoyId: string, taskId: string): string {
|
|
95
|
+
return join(resolve(process.cwd()), '.opencastle', 'artifacts', convoyId, taskId)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function saveCompaction(
|
|
99
|
+
convoyId: string,
|
|
100
|
+
taskId: string,
|
|
101
|
+
summary: CompactionSummary,
|
|
102
|
+
compactionCount: number,
|
|
103
|
+
): string {
|
|
104
|
+
const dir = getCompactionDir(convoyId, taskId)
|
|
105
|
+
mkdirSync(dir, { recursive: true })
|
|
106
|
+
const filename = `compaction-${compactionCount}.json`
|
|
107
|
+
const filePath = join(dir, filename)
|
|
108
|
+
writeFileSync(filePath, JSON.stringify(summary, null, 2))
|
|
109
|
+
return filePath
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function loadCompaction(summaryPath: string): CompactionSummary | null {
|
|
113
|
+
try {
|
|
114
|
+
const content = readFileSync(summaryPath, 'utf8')
|
|
115
|
+
return JSON.parse(content) as CompactionSummary
|
|
116
|
+
} catch {
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- Build continuation prompt ---
|
|
122
|
+
|
|
123
|
+
export function buildContinuationPrompt(
|
|
124
|
+
originalPrompt: string,
|
|
125
|
+
summaryPath: string,
|
|
126
|
+
isolationPreamble: string,
|
|
127
|
+
): string {
|
|
128
|
+
const summary = loadCompaction(summaryPath)
|
|
129
|
+
if (!summary) {
|
|
130
|
+
return isolationPreamble + '\n\n' + originalPrompt
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const summaryBlock = [
|
|
134
|
+
'## Continuation from Compacted Context',
|
|
135
|
+
'You are CONTINUING a task that was compacted. Previous progress:',
|
|
136
|
+
'',
|
|
137
|
+
'**Phase:** ' + summary.phase,
|
|
138
|
+
'**Completed steps:**',
|
|
139
|
+
...summary.completed_steps.map(s => '- ' + s),
|
|
140
|
+
'**Pending steps:**',
|
|
141
|
+
...summary.pending_steps.map(s => '- ' + s),
|
|
142
|
+
'**Key decisions:**',
|
|
143
|
+
...summary.key_decisions.map(s => '- ' + s),
|
|
144
|
+
'**Files already modified:**',
|
|
145
|
+
...summary.files_modified.map(f => '- ' + f),
|
|
146
|
+
...(summary.artifact_refs.length > 0
|
|
147
|
+
? ['**Artifacts:**', ...summary.artifact_refs.map(a => '- ' + a)]
|
|
148
|
+
: []),
|
|
149
|
+
'',
|
|
150
|
+
'Focus on the PENDING steps. Do NOT redo completed steps.',
|
|
151
|
+
].join('\n')
|
|
152
|
+
|
|
153
|
+
return isolationPreamble + '\n\n' + summaryBlock + '\n\n' + originalPrompt
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// --- Compaction count helpers ---
|
|
157
|
+
|
|
158
|
+
export function canCompact(compactionCount: number): boolean {
|
|
159
|
+
return compactionCount < MAX_COMPACTIONS_PER_TASK
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function getMaxCompactions(): number {
|
|
163
|
+
return MAX_COMPACTIONS_PER_TASK
|
|
164
|
+
}
|
|
@@ -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
|
+
})
|