opencastle 0.30.0 → 0.31.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/dist/cli/pipeline.d.ts +2 -1
- package/dist/cli/pipeline.d.ts.map +1 -1
- package/dist/cli/pipeline.js +123 -60
- package/dist/cli/pipeline.js.map +1 -1
- package/dist/cli/pipeline.test.js +82 -143
- package/dist/cli/pipeline.test.js.map +1 -1
- package/dist/cli/plan.d.ts +9 -1
- package/dist/cli/plan.d.ts.map +1 -1
- package/dist/cli/plan.js +120 -11
- package/dist/cli/plan.js.map +1 -1
- package/dist/cli/run/adapters/copilot.d.ts +1 -0
- package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
- package/dist/cli/run/adapters/copilot.js +11 -1
- package/dist/cli/run/adapters/copilot.js.map +1 -1
- package/dist/cli/run/adapters/index.d.ts +5 -0
- package/dist/cli/run/adapters/index.d.ts.map +1 -1
- package/dist/cli/run/adapters/index.js +13 -0
- package/dist/cli/run/adapters/index.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +62 -9
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/types.d.ts +2 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/pipeline.test.ts +82 -140
- package/src/cli/pipeline.ts +145 -62
- package/src/cli/plan.ts +130 -12
- package/src/cli/run/adapters/copilot.ts +11 -1
- package/src/cli/run/adapters/index.ts +13 -0
- package/src/cli/run.ts +60 -9
- package/src/cli/types.ts +2 -0
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/orchestrator/prompts/assess-complexity.prompt.md +67 -0
- package/src/orchestrator/prompts/generate-convoy.prompt.md +14 -24
- package/src/orchestrator/prompts/generate-prd.prompt.md +25 -36
- package/src/orchestrator/prompts/validate-prd.prompt.md +1 -1
package/src/cli/pipeline.test.ts
CHANGED
|
@@ -1,150 +1,90 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
2
|
import { parseComplexityAssessment } from './pipeline.js'
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
\`\`\`json
|
|
15
|
-
{
|
|
16
|
-
"total_tasks": 4,
|
|
17
|
-
"total_phases": 2,
|
|
18
|
-
"domains": ["api", "frontend"],
|
|
19
|
-
"estimated_duration_minutes": 60,
|
|
20
|
-
"complexity": "medium",
|
|
21
|
-
"recommended_strategy": "single",
|
|
22
|
-
"chain_rationale": "",
|
|
23
|
-
"convoy_groups": [
|
|
4
|
+
const SINGLE_JSON = JSON.stringify({
|
|
5
|
+
original_prompt: 'Build a REST API with user auth',
|
|
6
|
+
total_tasks: 4,
|
|
7
|
+
total_phases: 2,
|
|
8
|
+
domains: ['api', 'frontend'],
|
|
9
|
+
estimated_duration_minutes: 60,
|
|
10
|
+
complexity: 'medium',
|
|
11
|
+
recommended_strategy: 'single',
|
|
12
|
+
chain_rationale: '',
|
|
13
|
+
convoy_groups: [
|
|
24
14
|
{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
]
|
|
31
|
-
}
|
|
32
|
-
\`\`\`
|
|
33
|
-
`
|
|
34
|
-
|
|
35
|
-
const CHAIN_PRD = `# Big Feature — PRD
|
|
36
|
-
|
|
37
|
-
## Overview
|
|
38
|
-
Big feature.
|
|
39
|
-
|
|
40
|
-
## Risks & Open Questions
|
|
41
|
-
None identified.
|
|
42
|
-
|
|
43
|
-
## Complexity Assessment
|
|
15
|
+
name: 'full-implementation',
|
|
16
|
+
description: 'All phases in a single convoy',
|
|
17
|
+
phases: [1, 2],
|
|
18
|
+
depends_on: [],
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
})
|
|
44
22
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
23
|
+
const CHAIN_JSON = JSON.stringify({
|
|
24
|
+
original_prompt: 'Build a full-stack e-commerce platform',
|
|
25
|
+
total_tasks: 12,
|
|
26
|
+
total_phases: 4,
|
|
27
|
+
domains: ['database', 'api', 'frontend', 'testing'],
|
|
28
|
+
estimated_duration_minutes: 240,
|
|
29
|
+
complexity: 'high',
|
|
30
|
+
recommended_strategy: 'chain',
|
|
31
|
+
chain_rationale:
|
|
32
|
+
'Database schema changes have no frontend dependencies and can be validated independently.',
|
|
33
|
+
convoy_groups: [
|
|
55
34
|
{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
35
|
+
name: 'database-setup',
|
|
36
|
+
description: 'Schema changes and migrations',
|
|
37
|
+
phases: [1],
|
|
38
|
+
depends_on: [],
|
|
60
39
|
},
|
|
61
40
|
{
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
41
|
+
name: 'api-integration',
|
|
42
|
+
description: 'API routes and server logic',
|
|
43
|
+
phases: [2],
|
|
44
|
+
depends_on: ['database-setup'],
|
|
66
45
|
},
|
|
67
46
|
{
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
]
|
|
74
|
-
}
|
|
75
|
-
\`\`\`
|
|
76
|
-
`
|
|
77
|
-
|
|
78
|
-
const NO_SECTION_PRD = `# Feature — PRD
|
|
79
|
-
|
|
80
|
-
## Overview
|
|
81
|
-
Plain prd with no complexity section.
|
|
82
|
-
|
|
83
|
-
## Risks & Open Questions
|
|
84
|
-
None.
|
|
85
|
-
`
|
|
86
|
-
|
|
87
|
-
const MALFORMED_JSON_PRD = `# Feature — PRD
|
|
88
|
-
|
|
89
|
-
## Complexity Assessment
|
|
90
|
-
|
|
91
|
-
\`\`\`json
|
|
92
|
-
{ "total_tasks": 3, "broken":
|
|
93
|
-
\`\`\`
|
|
94
|
-
`
|
|
95
|
-
|
|
96
|
-
const UNFENCED_JSON_PRD = `# Feature — PRD
|
|
97
|
-
|
|
98
|
-
## Complexity Assessment
|
|
99
|
-
|
|
100
|
-
The complexity is medium.
|
|
101
|
-
|
|
102
|
-
total_tasks: 3, total_phases: 2
|
|
103
|
-
`
|
|
104
|
-
|
|
105
|
-
const OTHER_JSON_PRD = `# Feature — PRD
|
|
106
|
-
|
|
107
|
-
## Overview
|
|
108
|
-
Some feature with json-like content: \`{"key": "value"}\`
|
|
109
|
-
|
|
110
|
-
Another block:
|
|
111
|
-
\`\`\`json
|
|
112
|
-
{"not": "complexity"}
|
|
113
|
-
\`\`\`
|
|
114
|
-
|
|
115
|
-
## Complexity Assessment
|
|
47
|
+
name: 'frontend-testing',
|
|
48
|
+
description: 'UI components and test suite',
|
|
49
|
+
phases: [3, 4],
|
|
50
|
+
depends_on: ['api-integration'],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
})
|
|
116
54
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
{
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
"phases": [1, 2, 3],
|
|
129
|
-
"depends_on": []
|
|
130
|
-
}
|
|
131
|
-
]
|
|
132
|
-
}
|
|
133
|
-
\`\`\`
|
|
134
|
-
`
|
|
55
|
+
const MINIMAL_JSON = JSON.stringify({
|
|
56
|
+
original_prompt: 'Add a health-check endpoint',
|
|
57
|
+
total_tasks: 3,
|
|
58
|
+
total_phases: 1,
|
|
59
|
+
domains: ['api'],
|
|
60
|
+
complexity: 'low',
|
|
61
|
+
recommended_strategy: 'single',
|
|
62
|
+
convoy_groups: [
|
|
63
|
+
{ name: 'all', description: 'All', phases: [1], depends_on: [] },
|
|
64
|
+
],
|
|
65
|
+
})
|
|
135
66
|
|
|
136
67
|
describe('parseComplexityAssessment', () => {
|
|
137
|
-
it('returns null
|
|
138
|
-
expect(parseComplexityAssessment(
|
|
68
|
+
it('returns null for empty string', () => {
|
|
69
|
+
expect(parseComplexityAssessment('')).toBeNull()
|
|
139
70
|
})
|
|
140
71
|
|
|
141
72
|
it('returns null when JSON is malformed', () => {
|
|
142
|
-
expect(
|
|
73
|
+
expect(
|
|
74
|
+
parseComplexityAssessment('{ "total_tasks": 3, "broken": ')
|
|
75
|
+
).toBeNull()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('returns null for plain text that is not JSON', () => {
|
|
79
|
+
expect(
|
|
80
|
+
parseComplexityAssessment('The complexity is medium.')
|
|
81
|
+
).toBeNull()
|
|
143
82
|
})
|
|
144
83
|
|
|
145
84
|
it('parses a valid single strategy assessment', () => {
|
|
146
|
-
const result = parseComplexityAssessment(
|
|
85
|
+
const result = parseComplexityAssessment(SINGLE_JSON)
|
|
147
86
|
expect(result).not.toBeNull()
|
|
87
|
+
expect(result?.original_prompt).toBe('Build a REST API with user auth')
|
|
148
88
|
expect(result?.recommended_strategy).toBe('single')
|
|
149
89
|
expect(result?.complexity).toBe('medium')
|
|
150
90
|
expect(result?.total_tasks).toBe(4)
|
|
@@ -155,7 +95,7 @@ describe('parseComplexityAssessment', () => {
|
|
|
155
95
|
})
|
|
156
96
|
|
|
157
97
|
it('parses a valid chain strategy assessment with multiple groups', () => {
|
|
158
|
-
const result = parseComplexityAssessment(
|
|
98
|
+
const result = parseComplexityAssessment(CHAIN_JSON)
|
|
159
99
|
expect(result).not.toBeNull()
|
|
160
100
|
expect(result?.recommended_strategy).toBe('chain')
|
|
161
101
|
expect(result?.complexity).toBe('high')
|
|
@@ -166,26 +106,28 @@ describe('parseComplexityAssessment', () => {
|
|
|
166
106
|
})
|
|
167
107
|
|
|
168
108
|
it('handles missing optional fields gracefully', () => {
|
|
169
|
-
const
|
|
170
|
-
const result = parseComplexityAssessment(prd)
|
|
109
|
+
const result = parseComplexityAssessment(MINIMAL_JSON)
|
|
171
110
|
expect(result).not.toBeNull()
|
|
172
111
|
expect(result?.estimated_duration_minutes).toBeUndefined()
|
|
173
112
|
expect(result?.chain_rationale).toBeUndefined()
|
|
174
113
|
})
|
|
175
114
|
|
|
176
|
-
it('returns null when
|
|
177
|
-
expect(parseComplexityAssessment(
|
|
115
|
+
it('returns null when required fields are missing from JSON', () => {
|
|
116
|
+
expect(parseComplexityAssessment('{"total_tasks": 3}')).toBeNull()
|
|
178
117
|
})
|
|
179
118
|
|
|
180
|
-
it('
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
119
|
+
it('returns null when original_prompt is missing', () => {
|
|
120
|
+
const json = JSON.stringify({
|
|
121
|
+
total_tasks: 4, total_phases: 2, domains: ['api'],
|
|
122
|
+
complexity: 'medium', recommended_strategy: 'single',
|
|
123
|
+
convoy_groups: [{ name: 'all', description: 'All', phases: [1], depends_on: [] }],
|
|
124
|
+
})
|
|
125
|
+
expect(parseComplexityAssessment(json)).toBeNull()
|
|
185
126
|
})
|
|
186
127
|
|
|
187
|
-
it('
|
|
188
|
-
const
|
|
189
|
-
expect(
|
|
128
|
+
it('parses JSON with extra whitespace', () => {
|
|
129
|
+
const result = parseComplexityAssessment(` \n ${SINGLE_JSON} \n `)
|
|
130
|
+
expect(result).not.toBeNull()
|
|
131
|
+
expect(result?.total_tasks).toBe(4)
|
|
190
132
|
})
|
|
191
133
|
})
|
package/src/cli/pipeline.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { existsSync } from 'node:fs'
|
|
|
3
3
|
import { resolve, dirname } from 'node:path'
|
|
4
4
|
import { stringify } from 'yaml'
|
|
5
5
|
import { c, confirm, closePrompts } from './prompt.js'
|
|
6
|
-
import { runPromptStep } from './plan.js'
|
|
6
|
+
import { runPromptStep, readProjectMcpServers } from './plan.js'
|
|
7
|
+
import { cleanupAdapters } from './run/adapters/index.js'
|
|
7
8
|
import type { CliContext } from './types.js'
|
|
8
9
|
|
|
9
10
|
export interface ConvoyGroup {
|
|
@@ -14,6 +15,7 @@ export interface ConvoyGroup {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export interface ComplexityAssessment {
|
|
18
|
+
original_prompt: string
|
|
17
19
|
total_tasks: number
|
|
18
20
|
total_phases: number
|
|
19
21
|
domains: string[]
|
|
@@ -24,18 +26,12 @@ export interface ComplexityAssessment {
|
|
|
24
26
|
convoy_groups: ConvoyGroup[]
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
export function parseComplexityAssessment(
|
|
28
|
-
const sectionMatch = prdContent.match(/## Complexity Assessment\s+([\s\S]*?)(?=\n## |\n# |$)/)
|
|
29
|
-
if (!sectionMatch) return null
|
|
30
|
-
|
|
31
|
-
const sectionContent = sectionMatch[1]
|
|
32
|
-
const jsonMatch = sectionContent.match(/```json\s*([\s\S]*?)```/)
|
|
33
|
-
if (!jsonMatch) return null
|
|
34
|
-
|
|
29
|
+
export function parseComplexityAssessment(jsonText: string): ComplexityAssessment | null {
|
|
35
30
|
try {
|
|
36
|
-
const parsed = JSON.parse(
|
|
31
|
+
const parsed = JSON.parse(jsonText.trim()) as ComplexityAssessment
|
|
37
32
|
// Validate required fields
|
|
38
33
|
if (
|
|
34
|
+
typeof parsed.original_prompt !== 'string' ||
|
|
39
35
|
typeof parsed.total_tasks !== 'number' ||
|
|
40
36
|
typeof parsed.total_phases !== 'number' ||
|
|
41
37
|
!Array.isArray(parsed.domains) ||
|
|
@@ -59,9 +55,10 @@ const HELP = `
|
|
|
59
55
|
Step 1 — Generate PRD (generate-prd)
|
|
60
56
|
Step 2 — Validate PRD (validate-prd)
|
|
61
57
|
Step 3 — Fix PRD (fix-prd, up to 2 retries if invalid)
|
|
62
|
-
Step 4 —
|
|
63
|
-
Step 5 —
|
|
64
|
-
Step 6 —
|
|
58
|
+
Step 4 — Assess complexity (assess-complexity, determines single vs chain)
|
|
59
|
+
Step 5 — Generate convoy spec (generate-convoy, using PRD as input)
|
|
60
|
+
Step 6 — Validate convoy spec (validate-convoy)
|
|
61
|
+
Step 7 — Fix convoy spec (fix-convoy, up to 2 retries if invalid)
|
|
65
62
|
|
|
66
63
|
Options:
|
|
67
64
|
--text, -t <text> Feature prompt text (required, unless --prd is set)
|
|
@@ -71,7 +68,7 @@ const HELP = `
|
|
|
71
68
|
--adapter, -a <name> Override agent runtime adapter
|
|
72
69
|
--verbose Show full agent output for each step
|
|
73
70
|
--dry-run Generate and print the PRD prompt only, then stop
|
|
74
|
-
--skip-validation Skip
|
|
71
|
+
--skip-validation Skip PRD and convoy validation (steps 2, 3, 6, 7)
|
|
75
72
|
--help, -h Show this help
|
|
76
73
|
`
|
|
77
74
|
|
|
@@ -149,6 +146,8 @@ function parseArgs(args: string[]): PipelineOptions {
|
|
|
149
146
|
return opts
|
|
150
147
|
}
|
|
151
148
|
|
|
149
|
+
const MAX_FIX_RETRIES = 2
|
|
150
|
+
|
|
152
151
|
function relPath(abs: string): string {
|
|
153
152
|
return abs.startsWith(process.cwd()) ? abs.slice(process.cwd().length + 1) : abs
|
|
154
153
|
}
|
|
@@ -184,11 +183,13 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
184
183
|
}
|
|
185
184
|
}
|
|
186
185
|
|
|
187
|
-
const totalSteps = opts.skipValidation ?
|
|
186
|
+
const totalSteps = opts.skipValidation ? 4 : 7
|
|
187
|
+
const mcpServers = await readProjectMcpServers(process.cwd())
|
|
188
188
|
const sharedOpts = {
|
|
189
189
|
adapterName: opts.adapter ?? undefined,
|
|
190
190
|
verbose: opts.verbose,
|
|
191
191
|
pkgRoot,
|
|
192
|
+
...(mcpServers.length ? { mcpServers } : {}),
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
console.log(c.bold('\n opencastle pipeline\n'))
|
|
@@ -318,8 +319,26 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
318
319
|
}
|
|
319
320
|
|
|
320
321
|
// ── Complexity-aware strategy decision ────────────────────────────────────
|
|
321
|
-
const
|
|
322
|
-
|
|
322
|
+
const complexityStep = opts.skipValidation ? 2 : 4
|
|
323
|
+
console.log(stepLabel(complexityStep, totalSteps, 'Assessing complexity…'))
|
|
324
|
+
|
|
325
|
+
let complexity: ComplexityAssessment | null = null
|
|
326
|
+
try {
|
|
327
|
+
const complexityResult = await runPromptStep({
|
|
328
|
+
...sharedOpts,
|
|
329
|
+
template: 'assess-complexity',
|
|
330
|
+
filePath: prdPath,
|
|
331
|
+
contextText: opts.text ?? undefined,
|
|
332
|
+
})
|
|
333
|
+
complexity = parseComplexityAssessment(complexityResult.rawOutput)
|
|
334
|
+
} catch (err) {
|
|
335
|
+
console.warn(c.yellow(` ⚠ Complexity assessment failed: ${err instanceof Error ? err.message : String(err)}`))
|
|
336
|
+
console.warn(c.dim(` Falling back to single convoy strategy.\n`))
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (!complexity) {
|
|
340
|
+
console.log(c.dim(` Could not determine complexity — using single convoy strategy.\n`))
|
|
341
|
+
}
|
|
323
342
|
|
|
324
343
|
if (complexity) {
|
|
325
344
|
if (complexity.recommended_strategy === 'chain' && complexity.convoy_groups.length > 1) {
|
|
@@ -358,16 +377,21 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
358
377
|
)
|
|
359
378
|
)
|
|
360
379
|
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
380
|
+
const chainGoal = [
|
|
381
|
+
complexity.original_prompt,
|
|
382
|
+
'',
|
|
383
|
+
'## Convoy Group Scope',
|
|
384
|
+
'',
|
|
385
|
+
`This is group **${i + 1} of ${complexity.convoy_groups.length}** in a convoy chain.`,
|
|
386
|
+
`Generate a convoy spec covering ONLY the phases listed below.`,
|
|
387
|
+
'',
|
|
388
|
+
`- **Group name:** ${group.name}`,
|
|
389
|
+
`- **Description:** ${group.description}`,
|
|
390
|
+
`- **Phases to include:** ${group.phases.join(', ')}`,
|
|
391
|
+
group.depends_on.length ? `- **Depends on groups:** ${group.depends_on.join(', ')}` : '',
|
|
392
|
+
].filter(Boolean).join('\n')
|
|
393
|
+
|
|
394
|
+
const prdContent = await readFile(prdPath, 'utf8')
|
|
371
395
|
const groupSpecPath = resolve(convoyDir, `${group.name}.convoy.yml`)
|
|
372
396
|
|
|
373
397
|
let groupResult
|
|
@@ -375,8 +399,8 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
375
399
|
groupResult = await runPromptStep({
|
|
376
400
|
...sharedOpts,
|
|
377
401
|
template: 'generate-convoy',
|
|
378
|
-
|
|
379
|
-
contextText:
|
|
402
|
+
goalText: chainGoal,
|
|
403
|
+
contextText: prdContent,
|
|
380
404
|
outputPath: groupSpecPath,
|
|
381
405
|
})
|
|
382
406
|
} catch (err) {
|
|
@@ -395,13 +419,13 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
395
419
|
const valStep = groupStep + 1
|
|
396
420
|
console.log(stepLabel(valStep, totalGroupSteps, `Validating spec: ${group.name}…`))
|
|
397
421
|
|
|
398
|
-
|
|
422
|
+
let currentSpecContent = await readFile(resolvedGroupSpecPath, 'utf8')
|
|
399
423
|
let groupValidation
|
|
400
424
|
try {
|
|
401
425
|
groupValidation = await runPromptStep({
|
|
402
426
|
...sharedOpts,
|
|
403
427
|
template: 'validate-convoy',
|
|
404
|
-
goalText:
|
|
428
|
+
goalText: currentSpecContent,
|
|
405
429
|
})
|
|
406
430
|
} catch (err) {
|
|
407
431
|
console.error(
|
|
@@ -410,40 +434,92 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
410
434
|
process.exit(1)
|
|
411
435
|
}
|
|
412
436
|
|
|
413
|
-
if (
|
|
414
|
-
console.log(c.
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
goalText: groupSpecContent,
|
|
423
|
-
contextText: groupValidation.errors ?? groupValidation.rawOutput,
|
|
424
|
-
outputPath: resolvedGroupSpecPath,
|
|
425
|
-
})
|
|
426
|
-
} catch (err) {
|
|
427
|
-
console.error(
|
|
428
|
-
`\n ✗ Fix failed for group ${group.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
437
|
+
if (groupValidation.isValid) {
|
|
438
|
+
console.log(c.green(` ✓ Spec valid\n`))
|
|
439
|
+
} else {
|
|
440
|
+
let groupErrors = groupValidation.errors ?? groupValidation.rawOutput
|
|
441
|
+
let groupFixed = false
|
|
442
|
+
|
|
443
|
+
for (let attempt = 1; attempt <= MAX_FIX_RETRIES; attempt++) {
|
|
444
|
+
console.log(
|
|
445
|
+
c.yellow(` ⚠ Spec has issues — fix attempt ${attempt}/${MAX_FIX_RETRIES} for ${group.name}…\n`)
|
|
429
446
|
)
|
|
430
|
-
|
|
447
|
+
console.log(c.dim(groupErrors))
|
|
448
|
+
console.log()
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
await runPromptStep({
|
|
452
|
+
...sharedOpts,
|
|
453
|
+
template: 'fix-convoy',
|
|
454
|
+
goalText: currentSpecContent,
|
|
455
|
+
contextText: groupErrors,
|
|
456
|
+
outputPath: resolvedGroupSpecPath,
|
|
457
|
+
})
|
|
458
|
+
} catch (err) {
|
|
459
|
+
console.error(
|
|
460
|
+
`\n ✗ Fix failed for group ${group.name} (attempt ${attempt}): ${err instanceof Error ? err.message : String(err)}`
|
|
461
|
+
)
|
|
462
|
+
process.exit(1)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
console.log(c.dim(` Re-validating ${group.name} after fix…`))
|
|
466
|
+
|
|
467
|
+
currentSpecContent = await readFile(resolvedGroupSpecPath, 'utf8')
|
|
468
|
+
|
|
469
|
+
let revalidation
|
|
470
|
+
try {
|
|
471
|
+
revalidation = await runPromptStep({
|
|
472
|
+
...sharedOpts,
|
|
473
|
+
template: 'validate-convoy',
|
|
474
|
+
goalText: currentSpecContent,
|
|
475
|
+
})
|
|
476
|
+
} catch (err) {
|
|
477
|
+
console.error(
|
|
478
|
+
`\n ✗ Re-validation failed for group ${group.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
479
|
+
)
|
|
480
|
+
process.exit(1)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (revalidation.isValid) {
|
|
484
|
+
console.log(c.green(` ✓ ${group.name} fixed and validated\n`))
|
|
485
|
+
groupFixed = true
|
|
486
|
+
break
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
groupErrors = revalidation.errors ?? revalidation.rawOutput
|
|
490
|
+
|
|
491
|
+
if (attempt < MAX_FIX_RETRIES) {
|
|
492
|
+
console.log(
|
|
493
|
+
c.yellow(` ⚠ Still has issues after fix attempt ${attempt} — retrying…\n`)
|
|
494
|
+
)
|
|
495
|
+
}
|
|
431
496
|
}
|
|
432
497
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
498
|
+
if (!groupFixed) {
|
|
499
|
+
console.log(
|
|
500
|
+
c.red(`\n ✗ Could not auto-fix convoy spec for group ${group.name} after ${MAX_FIX_RETRIES} attempts.\n`)
|
|
501
|
+
)
|
|
502
|
+
console.log(` Remaining issues:\n`)
|
|
503
|
+
console.log(groupErrors)
|
|
504
|
+
console.log(
|
|
505
|
+
c.dim(`\n The spec has been saved to ${relPath(resolvedGroupSpecPath)} with best available fixes.\n`) +
|
|
506
|
+
c.dim(` Review the issues above and edit manually, then re-validate with:\n`) +
|
|
507
|
+
` opencastle plan --file ${relPath(resolvedGroupSpecPath)} --template validate-convoy\n`
|
|
508
|
+
)
|
|
509
|
+
process.exit(1)
|
|
510
|
+
}
|
|
436
511
|
}
|
|
437
512
|
}
|
|
438
513
|
}
|
|
439
514
|
|
|
440
515
|
// Build master pipeline spec (version 2)
|
|
441
|
-
const
|
|
516
|
+
const chainPrdContent = await readFile(prdPath, 'utf8')
|
|
517
|
+
const featureNameMatch = chainPrdContent.match(/^# (.+?)\s*(?:—|-)?\s*PRD/m)
|
|
442
518
|
const featureName = featureNameMatch
|
|
443
519
|
? featureNameMatch[1].trim().toLowerCase().replace(/[^a-z0-9]+/g, '-')
|
|
444
520
|
: 'feature'
|
|
445
521
|
|
|
446
|
-
const branchMatch =
|
|
522
|
+
const branchMatch = chainPrdContent.match(/`feat\/([^`]+)`/)
|
|
447
523
|
const branch = branchMatch ? `feat/${branchMatch[1]}` : `feat/${featureName}`
|
|
448
524
|
|
|
449
525
|
const masterSpec = {
|
|
@@ -480,6 +556,7 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
480
556
|
}
|
|
481
557
|
} finally {
|
|
482
558
|
closePrompts()
|
|
559
|
+
await cleanupAdapters()
|
|
483
560
|
}
|
|
484
561
|
return
|
|
485
562
|
} else {
|
|
@@ -489,16 +566,20 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
489
566
|
}
|
|
490
567
|
}
|
|
491
568
|
|
|
492
|
-
// ──
|
|
493
|
-
const genStep = opts.skipValidation ?
|
|
569
|
+
// ── Generate convoy spec ──────────────────────────────────────────────────
|
|
570
|
+
const genStep = opts.skipValidation ? 3 : 5
|
|
494
571
|
console.log(stepLabel(genStep, totalSteps, 'Generating convoy spec…'))
|
|
495
572
|
|
|
573
|
+
const singlePrdContent = await readFile(prdPath, 'utf8')
|
|
574
|
+
const singleGoal = complexity?.original_prompt ?? opts.text ?? ''
|
|
575
|
+
|
|
496
576
|
let specPath: string
|
|
497
577
|
try {
|
|
498
578
|
const result = await runPromptStep({
|
|
499
579
|
...sharedOpts,
|
|
500
580
|
template: 'generate-convoy',
|
|
501
|
-
|
|
581
|
+
goalText: singleGoal,
|
|
582
|
+
contextText: singlePrdContent,
|
|
502
583
|
outputPath: opts.outputSpec ? resolve(process.cwd(), opts.outputSpec) : undefined,
|
|
503
584
|
})
|
|
504
585
|
specPath = result.outputPath!
|
|
@@ -514,8 +595,9 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
514
595
|
return
|
|
515
596
|
}
|
|
516
597
|
|
|
517
|
-
// ──
|
|
518
|
-
|
|
598
|
+
// ── Validate convoy spec ──────────────────────────────────────────────
|
|
599
|
+
const valStep = opts.skipValidation ? 4 : 6
|
|
600
|
+
console.log(stepLabel(valStep, totalSteps, 'Validating convoy spec…'))
|
|
519
601
|
|
|
520
602
|
const specContent = await readFile(specPath, 'utf8')
|
|
521
603
|
let validationErrors: string
|
|
@@ -529,7 +611,7 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
529
611
|
goalText: specContent,
|
|
530
612
|
})
|
|
531
613
|
} catch (err) {
|
|
532
|
-
console.error(`\n ✗ Step
|
|
614
|
+
console.error(`\n ✗ Step ${valStep} failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
533
615
|
process.exit(1)
|
|
534
616
|
}
|
|
535
617
|
|
|
@@ -545,13 +627,13 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
545
627
|
console.log()
|
|
546
628
|
}
|
|
547
629
|
|
|
548
|
-
// ──
|
|
549
|
-
const
|
|
630
|
+
// ── Fix convoy spec (up to 2 retries) ─────────────────────────────────
|
|
631
|
+
const fixStep = opts.skipValidation ? 4 : 7
|
|
550
632
|
let fixedSpecContent = specContent
|
|
551
633
|
|
|
552
634
|
for (let attempt = 1; attempt <= MAX_FIX_RETRIES; attempt++) {
|
|
553
635
|
const label = `Fix attempt ${attempt}/${MAX_FIX_RETRIES}…`
|
|
554
|
-
console.log(stepLabel(
|
|
636
|
+
console.log(stepLabel(fixStep, totalSteps, label))
|
|
555
637
|
|
|
556
638
|
let fixResult
|
|
557
639
|
try {
|
|
@@ -639,5 +721,6 @@ async function printFinalSummary(
|
|
|
639
721
|
}
|
|
640
722
|
} finally {
|
|
641
723
|
closePrompts()
|
|
724
|
+
await cleanupAdapters()
|
|
642
725
|
}
|
|
643
726
|
}
|