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.
Files changed (36) hide show
  1. package/dist/cli/pipeline.d.ts +2 -1
  2. package/dist/cli/pipeline.d.ts.map +1 -1
  3. package/dist/cli/pipeline.js +123 -60
  4. package/dist/cli/pipeline.js.map +1 -1
  5. package/dist/cli/pipeline.test.js +82 -143
  6. package/dist/cli/pipeline.test.js.map +1 -1
  7. package/dist/cli/plan.d.ts +9 -1
  8. package/dist/cli/plan.d.ts.map +1 -1
  9. package/dist/cli/plan.js +120 -11
  10. package/dist/cli/plan.js.map +1 -1
  11. package/dist/cli/run/adapters/copilot.d.ts +1 -0
  12. package/dist/cli/run/adapters/copilot.d.ts.map +1 -1
  13. package/dist/cli/run/adapters/copilot.js +11 -1
  14. package/dist/cli/run/adapters/copilot.js.map +1 -1
  15. package/dist/cli/run/adapters/index.d.ts +5 -0
  16. package/dist/cli/run/adapters/index.d.ts.map +1 -1
  17. package/dist/cli/run/adapters/index.js +13 -0
  18. package/dist/cli/run/adapters/index.js.map +1 -1
  19. package/dist/cli/run.d.ts.map +1 -1
  20. package/dist/cli/run.js +62 -9
  21. package/dist/cli/run.js.map +1 -1
  22. package/dist/cli/types.d.ts +2 -0
  23. package/dist/cli/types.d.ts.map +1 -1
  24. package/package.json +1 -1
  25. package/src/cli/pipeline.test.ts +82 -140
  26. package/src/cli/pipeline.ts +145 -62
  27. package/src/cli/plan.ts +130 -12
  28. package/src/cli/run/adapters/copilot.ts +11 -1
  29. package/src/cli/run/adapters/index.ts +13 -0
  30. package/src/cli/run.ts +60 -9
  31. package/src/cli/types.ts +2 -0
  32. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  33. package/src/orchestrator/prompts/assess-complexity.prompt.md +67 -0
  34. package/src/orchestrator/prompts/generate-convoy.prompt.md +14 -24
  35. package/src/orchestrator/prompts/generate-prd.prompt.md +25 -36
  36. package/src/orchestrator/prompts/validate-prd.prompt.md +1 -1
@@ -1,150 +1,90 @@
1
1
  import { describe, it, expect } from 'vitest'
2
2
  import { parseComplexityAssessment } from './pipeline.js'
3
3
 
4
- const SINGLE_PRD = `# My Feature — PRD
5
-
6
- ## Overview
7
- Some overview.
8
-
9
- ## Risks & Open Questions
10
- None identified.
11
-
12
- ## Complexity Assessment
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
- "name": "full-implementation",
26
- "description": "All phases in a single convoy",
27
- "phases": [1, 2],
28
- "depends_on": []
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
- \`\`\`json
46
- {
47
- "total_tasks": 12,
48
- "total_phases": 4,
49
- "domains": ["database", "api", "frontend", "testing"],
50
- "estimated_duration_minutes": 240,
51
- "complexity": "high",
52
- "recommended_strategy": "chain",
53
- "chain_rationale": "Database schema changes have no frontend dependencies and can be validated independently.",
54
- "convoy_groups": [
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
- "name": "database-setup",
57
- "description": "Schema changes and migrations",
58
- "phases": [1],
59
- "depends_on": []
35
+ name: 'database-setup',
36
+ description: 'Schema changes and migrations',
37
+ phases: [1],
38
+ depends_on: [],
60
39
  },
61
40
  {
62
- "name": "api-integration",
63
- "description": "API routes and server logic",
64
- "phases": [2],
65
- "depends_on": ["database-setup"]
41
+ name: 'api-integration',
42
+ description: 'API routes and server logic',
43
+ phases: [2],
44
+ depends_on: ['database-setup'],
66
45
  },
67
46
  {
68
- "name": "frontend-testing",
69
- "description": "UI components and test suite",
70
- "phases": [3, 4],
71
- "depends_on": ["api-integration"]
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
- \`\`\`json
118
- {
119
- "total_tasks": 6,
120
- "total_phases": 3,
121
- "domains": ["api", "frontend"],
122
- "complexity": "medium",
123
- "recommended_strategy": "single",
124
- "convoy_groups": [
125
- {
126
- "name": "impl",
127
- "description": "All phases",
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 when PRD has no Complexity Assessment section', () => {
138
- expect(parseComplexityAssessment(NO_SECTION_PRD)).toBeNull()
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(parseComplexityAssessment(MALFORMED_JSON_PRD)).toBeNull()
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(SINGLE_PRD)
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(CHAIN_PRD)
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 prd = `# Feature — PRD\n\n## Complexity Assessment\n\n\`\`\`json\n{\n "total_tasks": 3,\n "total_phases": 1,\n "domains": ["api"],\n "complexity": "low",\n "recommended_strategy": "single",\n "convoy_groups": [{"name": "all", "description": "All", "phases": [1], "depends_on": []}]\n}\n\`\`\`\n`
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 JSON block is not fenced properly', () => {
177
- expect(parseComplexityAssessment(UNFENCED_JSON_PRD)).toBeNull()
115
+ it('returns null when required fields are missing from JSON', () => {
116
+ expect(parseComplexityAssessment('{"total_tasks": 3}')).toBeNull()
178
117
  })
179
118
 
180
- it('correctly extracts JSON even when PRD has other JSON-like content elsewhere', () => {
181
- const result = parseComplexityAssessment(OTHER_JSON_PRD)
182
- expect(result).not.toBeNull()
183
- expect(result?.total_tasks).toBe(6)
184
- expect(result?.recommended_strategy).toBe('single')
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('returns null when required fields are missing from JSON', () => {
188
- const prd = `# Feature — PRD\n\n## Complexity Assessment\n\n\`\`\`json\n{"total_tasks": 3}\n\`\`\`\n`
189
- expect(parseComplexityAssessment(prd)).toBeNull()
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
  })
@@ -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(prdContent: string): ComplexityAssessment | null {
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(jsonMatch[1].trim()) as ComplexityAssessment
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 — Generate convoy spec (generate-convoy, using PRD as BDO)
63
- Step 5 — Validate convoy spec (validate-convoy)
64
- Step 6 — Fix convoy spec (fix-convoy, up to 2 retries if invalid)
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 steps 2 and 4 (PRD and convoy validation)
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 ? 3 : 6
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 prdContentForComplexity = await readFile(prdPath, 'utf8')
322
- const complexity = parseComplexityAssessment(prdContentForComplexity)
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 chainContext = JSON.stringify({
362
- mode: 'chain_subset',
363
- group_name: group.name,
364
- group_description: group.description,
365
- group_phases: group.phases,
366
- depends_on_groups: group.depends_on,
367
- total_groups: complexity.convoy_groups.length,
368
- group_index: i + 1,
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
- filePath: prdPath,
379
- contextText: chainContext,
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
- const groupSpecContent = await readFile(resolvedGroupSpecPath, 'utf8')
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: groupSpecContent,
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 (!groupValidation.isValid) {
414
- console.log(c.yellow(` Spec has issues — attempting one auto-fix…\n`))
415
- console.log(c.dim(groupValidation.errors ?? groupValidation.rawOutput))
416
- console.log()
417
-
418
- try {
419
- await runPromptStep({
420
- ...sharedOpts,
421
- template: 'fix-convoy',
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
- process.exit(1)
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
- console.log(c.dim(` Applied fix for ${group.name}\n`))
434
- } else {
435
- console.log(c.green(` Spec valid\n`))
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 featureNameMatch = prdContentForComplexity.match(/^# (.+?)\s*(?:—|-)?\s*PRD/m)
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 = prdContentForComplexity.match(/`feat\/([^`]+)`/)
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
- // ── Step 4: Generate convoy spec ──────────────────────────────────────────
493
- const genStep = opts.skipValidation ? 2 : 4
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
- filePath: prdPath,
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
- // ── Step 5: Validate convoy spec ──────────────────────────────────────────
518
- console.log(stepLabel(5, totalSteps, 'Validating convoy spec…'))
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 5 failed: ${err instanceof Error ? err.message : String(err)}`)
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
- // ── Step 6: Fix convoy spec (up to 2 retries) ─────────────────────────────
549
- const MAX_FIX_RETRIES = 2
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(6, totalSteps, label))
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
  }