opencastle 0.32.8 → 0.32.10
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/convoy/spec-builder.d.ts +16 -0
- package/dist/cli/convoy/spec-builder.d.ts.map +1 -1
- package/dist/cli/convoy/spec-builder.js +115 -62
- package/dist/cli/convoy/spec-builder.js.map +1 -1
- package/dist/cli/pipeline.d.ts.map +1 -1
- package/dist/cli/pipeline.js +279 -116
- package/dist/cli/pipeline.js.map +1 -1
- package/dist/cli/plan.d.ts.map +1 -1
- package/dist/cli/plan.js +19 -7
- package/dist/cli/plan.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/convoy/spec-builder.ts +124 -58
- package/src/cli/pipeline.ts +324 -128
- package/src/cli/plan.ts +25 -7
- package/src/dashboard/dist/data/convoys/demo-api-v2.json +3 -3
- package/src/dashboard/dist/data/convoys/demo-auth-revamp.json +4 -4
- package/src/dashboard/dist/data/convoys/demo-dashboard-ui.json +18 -18
- package/src/dashboard/dist/data/convoys/demo-data-pipeline.json +9 -9
- package/src/dashboard/dist/data/convoys/demo-deploy-ci.json +1 -1
- package/src/dashboard/dist/data/convoys/demo-docs-update.json +3 -3
- package/src/dashboard/dist/data/convoys/demo-perf-opt.json +4 -4
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/public/data/convoys/demo-api-v2.json +3 -3
- package/src/dashboard/public/data/convoys/demo-auth-revamp.json +4 -4
- package/src/dashboard/public/data/convoys/demo-dashboard-ui.json +18 -18
- package/src/dashboard/public/data/convoys/demo-data-pipeline.json +9 -9
- package/src/dashboard/public/data/convoys/demo-deploy-ci.json +1 -1
- package/src/dashboard/public/data/convoys/demo-docs-update.json +3 -3
- package/src/dashboard/public/data/convoys/demo-perf-opt.json +4 -4
- package/src/orchestrator/prompts/fix-prd.prompt.md +4 -9
- package/src/orchestrator/prompts/generate-convoy.prompt.md +1 -0
- package/src/orchestrator/prompts/generate-prd.prompt.md +29 -0
- package/src/orchestrator/prompts/validate-prd.prompt.md +14 -37
package/src/cli/pipeline.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
1
|
+
import { readFile, writeFile, mkdir, unlink } from 'node:fs/promises'
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
|
-
import { resolve
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
4
|
import { stringify } from 'yaml'
|
|
5
5
|
import { c, confirm, closePrompts } from './prompt.js'
|
|
6
6
|
import { runPromptStep, readProjectMcpServers } from './plan.js'
|
|
@@ -8,7 +8,7 @@ import type { PromptStepOptions } from './plan.js'
|
|
|
8
8
|
import { cleanupAdapters } from './run/adapters/index.js'
|
|
9
9
|
import type { CliContext } from './types.js'
|
|
10
10
|
import { parseYaml, validateSpec } from './run/schema.js'
|
|
11
|
-
import { buildConvoyYaml,
|
|
11
|
+
import { buildConvoyYaml, parseTaskPlanWithReason, parsePatches, applyPatches, deriveSpecEnrichment, detectJsonTruncation } from './convoy/spec-builder.js'
|
|
12
12
|
import type { TaskPlan, SpecEnrichment } from './convoy/spec-builder.js'
|
|
13
13
|
|
|
14
14
|
export interface ConvoyGroup {
|
|
@@ -29,6 +29,104 @@ function appendTaskComplexity(base: string, taskComplexity: ComplexityAssessment
|
|
|
29
29
|
return result
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* For chain mode, extract only the PRD sections relevant to the given phases.
|
|
34
|
+
* Keeps Overview, Technical Requirements, and the matching phase sections from
|
|
35
|
+
* Task Breakdown, while trimming the full User Stories, Implementation Scope,
|
|
36
|
+
* and non-matching phases to reduce context size and avoid output truncation.
|
|
37
|
+
*/
|
|
38
|
+
function extractRelevantPrdSections(prdContent: string, phases: number[]): string {
|
|
39
|
+
const phaseSet = new Set(phases)
|
|
40
|
+
const lines = prdContent.split('\n')
|
|
41
|
+
const result: string[] = []
|
|
42
|
+
|
|
43
|
+
// Sections to always include (key context)
|
|
44
|
+
const alwaysInclude = ['overview', 'goals', 'non-goals', 'technical requirements']
|
|
45
|
+
// Sections to include condensed
|
|
46
|
+
const condenseSection = ['user stories & acceptance criteria', 'implementation scope', 'success criteria', 'risks & open questions']
|
|
47
|
+
|
|
48
|
+
let currentSection = ''
|
|
49
|
+
let inTaskBreakdown = false
|
|
50
|
+
let inRelevantPhase = false
|
|
51
|
+
let skipSection = false
|
|
52
|
+
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
// Detect heading (## level)
|
|
55
|
+
const h2Match = line.match(/^## (.+)/)
|
|
56
|
+
if (h2Match) {
|
|
57
|
+
const heading = h2Match[1].trim().toLowerCase()
|
|
58
|
+
currentSection = heading
|
|
59
|
+
inTaskBreakdown = heading === 'task breakdown'
|
|
60
|
+
inRelevantPhase = false
|
|
61
|
+
skipSection = false
|
|
62
|
+
|
|
63
|
+
if (alwaysInclude.some(s => heading.startsWith(s))) {
|
|
64
|
+
result.push(line)
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (inTaskBreakdown) {
|
|
69
|
+
result.push(line)
|
|
70
|
+
result.push('')
|
|
71
|
+
result.push(`*(Only phases ${phases.join(', ')} shown — other phases omitted for brevity)*`)
|
|
72
|
+
result.push('')
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (condenseSection.some(s => heading.startsWith(s))) {
|
|
77
|
+
// Include the heading but mark as condensed
|
|
78
|
+
result.push(line)
|
|
79
|
+
result.push('')
|
|
80
|
+
result.push('*(Condensed — see full PRD for details)*')
|
|
81
|
+
result.push('')
|
|
82
|
+
skipSection = true
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// # title heading — always include
|
|
87
|
+
result.push(line)
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// H1 heading — always include
|
|
92
|
+
if (line.match(/^# /)) {
|
|
93
|
+
result.push(line)
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (skipSection) continue
|
|
98
|
+
|
|
99
|
+
if (inTaskBreakdown) {
|
|
100
|
+
// Detect phase headers like "Phase 1 —" or "Phase 2 —"
|
|
101
|
+
const phaseMatch = line.match(/Phase\s+(\d+)/i)
|
|
102
|
+
if (phaseMatch) {
|
|
103
|
+
const phaseNum = parseInt(phaseMatch[1], 10)
|
|
104
|
+
inRelevantPhase = phaseSet.has(phaseNum)
|
|
105
|
+
}
|
|
106
|
+
if (inRelevantPhase) {
|
|
107
|
+
result.push(line)
|
|
108
|
+
}
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
result.push(line)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return result.join('\n')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Filter task complexity entries to only those matching the given phases.
|
|
120
|
+
*/
|
|
121
|
+
function filterTaskComplexityByPhases(
|
|
122
|
+
taskComplexity: ComplexityAssessment['task_complexity'],
|
|
123
|
+
phases: number[],
|
|
124
|
+
): ComplexityAssessment['task_complexity'] {
|
|
125
|
+
if (!taskComplexity?.length) return taskComplexity
|
|
126
|
+
const phaseSet = new Set(phases)
|
|
127
|
+
return taskComplexity.filter(tc => phaseSet.has(tc.phase))
|
|
128
|
+
}
|
|
129
|
+
|
|
32
130
|
export interface ComplexityAssessment {
|
|
33
131
|
original_prompt: string
|
|
34
132
|
total_tasks: number
|
|
@@ -310,6 +408,79 @@ function stepLabel(n: number, total: number, name: string): string {
|
|
|
310
408
|
return c.bold(c.cyan(` [${n}/${total}] ${name}`))
|
|
311
409
|
}
|
|
312
410
|
|
|
411
|
+
async function fixPrd(
|
|
412
|
+
sharedOpts: Omit<PromptStepOptions, 'template' | 'goalText'>,
|
|
413
|
+
prdPath: string,
|
|
414
|
+
prdContent: string,
|
|
415
|
+
initialErrors: string,
|
|
416
|
+
totalSteps: number,
|
|
417
|
+
adapterFlag: string | null,
|
|
418
|
+
): Promise<void> {
|
|
419
|
+
const MAX_PRD_FIX_RETRIES = 2
|
|
420
|
+
let fixedPrdContent = prdContent
|
|
421
|
+
let prdValidationErrors = initialErrors
|
|
422
|
+
let prdFixed = false
|
|
423
|
+
|
|
424
|
+
for (let attempt = 1; attempt <= MAX_PRD_FIX_RETRIES; attempt++) {
|
|
425
|
+
const label = `Fix PRD attempt ${attempt}/${MAX_PRD_FIX_RETRIES}…`
|
|
426
|
+
console.log(stepLabel(3, totalSteps, label))
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
await runPromptStep({
|
|
430
|
+
...sharedOpts,
|
|
431
|
+
template: 'fix-prd',
|
|
432
|
+
goalText: fixedPrdContent,
|
|
433
|
+
contextText: prdValidationErrors,
|
|
434
|
+
outputPath: prdPath,
|
|
435
|
+
})
|
|
436
|
+
} catch (err) {
|
|
437
|
+
console.error(`\n ✗ Step 3 (attempt ${attempt}) failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
438
|
+
process.exit(1)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
console.log(c.dim(` Re-validating after fix…`))
|
|
442
|
+
|
|
443
|
+
fixedPrdContent = await readFile(prdPath, 'utf8')
|
|
444
|
+
|
|
445
|
+
let revalidation
|
|
446
|
+
try {
|
|
447
|
+
revalidation = await runPromptStep({
|
|
448
|
+
...sharedOpts,
|
|
449
|
+
template: 'validate-prd',
|
|
450
|
+
goalText: `<!-- validation-pass: ${attempt + 1} -->\n${fixedPrdContent}`,
|
|
451
|
+
})
|
|
452
|
+
} catch (err) {
|
|
453
|
+
console.error(`\n ✗ Re-validation failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
454
|
+
process.exit(1)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (revalidation.isValid) {
|
|
458
|
+
console.log(c.green(` ✓ PRD fixed and validated\n`))
|
|
459
|
+
prdFixed = true
|
|
460
|
+
break
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
prdValidationErrors = revalidation.errors ?? revalidation.rawOutput
|
|
464
|
+
|
|
465
|
+
if (attempt < MAX_PRD_FIX_RETRIES) {
|
|
466
|
+
console.log(c.yellow(` ⚠ Still has issues after fix attempt ${attempt} — retrying…\n`))
|
|
467
|
+
console.log(c.dim(prdValidationErrors))
|
|
468
|
+
console.log()
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!prdFixed) {
|
|
473
|
+
console.log(c.yellow(`\n ⚠ Could not fully auto-fix the PRD after ${MAX_PRD_FIX_RETRIES} attempts — continuing with best-effort PRD.\n`))
|
|
474
|
+
console.log(c.dim(` Remaining issues:\n`))
|
|
475
|
+
console.log(c.dim(prdValidationErrors))
|
|
476
|
+
console.log(
|
|
477
|
+
c.dim(`\n PRD saved to ${relPath(prdPath)} with best available fixes.`) +
|
|
478
|
+
c.dim(`\n You can re-validate later with:\n`) +
|
|
479
|
+
` opencastle start --prd ${relPath(prdPath)}${adapterFlag ? ` --adapter ${adapterFlag}` : ''}\n`
|
|
480
|
+
)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
313
484
|
export default async function pipeline({ args, pkgRoot }: CliContext): Promise<void> {
|
|
314
485
|
const opts = parseArgs(args)
|
|
315
486
|
|
|
@@ -386,98 +557,11 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
386
557
|
return
|
|
387
558
|
}
|
|
388
559
|
|
|
389
|
-
// ──
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const prdContent = await readFile(prdPath, 'utf8')
|
|
394
|
-
let result
|
|
395
|
-
try {
|
|
396
|
-
result = await runPromptStep({
|
|
397
|
-
...sharedOpts,
|
|
398
|
-
template: 'validate-prd',
|
|
399
|
-
goalText: `<!-- validation-pass: 1 -->\n${prdContent}`,
|
|
400
|
-
})
|
|
401
|
-
} catch (err) {
|
|
402
|
-
console.error(`\n ✗ Step 2 failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
403
|
-
process.exit(1)
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (!result.isValid) {
|
|
407
|
-
let prdValidationErrors = result.errors ?? result.rawOutput
|
|
408
|
-
console.log(c.yellow(` ⚠ PRD has validation issues — attempting auto-fix…\n`))
|
|
409
|
-
console.log(c.dim(prdValidationErrors))
|
|
410
|
-
console.log()
|
|
411
|
-
|
|
412
|
-
// ── Step 3: Fix PRD (up to 2 retries) ──────────────────────────────────
|
|
413
|
-
const MAX_PRD_FIX_RETRIES = 2
|
|
414
|
-
let fixedPrdContent = prdContent
|
|
415
|
-
let prdFixed = false
|
|
416
|
-
|
|
417
|
-
for (let attempt = 1; attempt <= MAX_PRD_FIX_RETRIES; attempt++) {
|
|
418
|
-
const label = `Fix PRD attempt ${attempt}/${MAX_PRD_FIX_RETRIES}…`
|
|
419
|
-
console.log(stepLabel(3, totalSteps, label))
|
|
420
|
-
|
|
421
|
-
try {
|
|
422
|
-
await runPromptStep({
|
|
423
|
-
...sharedOpts,
|
|
424
|
-
template: 'fix-prd',
|
|
425
|
-
goalText: fixedPrdContent,
|
|
426
|
-
contextText: prdValidationErrors,
|
|
427
|
-
outputPath: prdPath, // overwrite in place
|
|
428
|
-
})
|
|
429
|
-
} catch (err) {
|
|
430
|
-
console.error(`\n ✗ Step 3 (attempt ${attempt}) failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
431
|
-
process.exit(1)
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
console.log(c.dim(` Re-validating after fix…`))
|
|
435
|
-
|
|
436
|
-
fixedPrdContent = await readFile(prdPath, 'utf8')
|
|
437
|
-
|
|
438
|
-
let revalidation
|
|
439
|
-
try {
|
|
440
|
-
revalidation = await runPromptStep({
|
|
441
|
-
...sharedOpts,
|
|
442
|
-
template: 'validate-prd',
|
|
443
|
-
goalText: `<!-- validation-pass: ${attempt + 1} -->\n${fixedPrdContent}`,
|
|
444
|
-
})
|
|
445
|
-
} catch (err) {
|
|
446
|
-
console.error(`\n ✗ Re-validation failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
447
|
-
process.exit(1)
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
if (revalidation.isValid) {
|
|
451
|
-
console.log(c.green(` ✓ PRD fixed and validated\n`))
|
|
452
|
-
prdFixed = true
|
|
453
|
-
break
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
prdValidationErrors = revalidation.errors ?? revalidation.rawOutput
|
|
457
|
-
|
|
458
|
-
if (attempt < MAX_PRD_FIX_RETRIES) {
|
|
459
|
-
console.log(c.yellow(` ⚠ Still has issues after fix attempt ${attempt} — retrying…\n`))
|
|
460
|
-
console.log(c.dim(prdValidationErrors))
|
|
461
|
-
console.log()
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
if (!prdFixed) {
|
|
466
|
-
console.log(c.yellow(`\n ⚠ Could not fully auto-fix the PRD after ${MAX_PRD_FIX_RETRIES} attempts — continuing with best-effort PRD.\n`))
|
|
467
|
-
console.log(c.dim(` Remaining issues:\n`))
|
|
468
|
-
console.log(c.dim(prdValidationErrors))
|
|
469
|
-
console.log(
|
|
470
|
-
c.dim(`\n PRD saved to ${relPath(prdPath)} with best available fixes.`) +
|
|
471
|
-
c.dim(`\n You can re-validate later with:\n`) +
|
|
472
|
-
` opencastle start --prd ${relPath(prdPath)}${opts.adapter ? ` --adapter ${opts.adapter}` : ''}\n`
|
|
473
|
-
)
|
|
474
|
-
}
|
|
475
|
-
} else {
|
|
476
|
-
console.log(c.green(` ✓ PRD is valid\n`))
|
|
477
|
-
}
|
|
478
|
-
}
|
|
560
|
+
// ── Steps 2 + 4: Validate PRD & Assess complexity (in parallel) ────────
|
|
561
|
+
// Both only read the PRD — run them concurrently to save one LLM round-trip.
|
|
562
|
+
// If validation fails we still use the complexity result (fix-prd patches
|
|
563
|
+
// issues without changing the overall structure/phases).
|
|
479
564
|
|
|
480
|
-
// ── Complexity-aware strategy decision ────────────────────────────────────
|
|
481
565
|
const complexityStep = opts.skipValidation ? 2 : 4
|
|
482
566
|
|
|
483
567
|
let complexity: ComplexityAssessment | null = null
|
|
@@ -485,6 +569,7 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
485
569
|
? resolve(process.cwd(), opts.complexity)
|
|
486
570
|
: deriveComplexityPath(prdPath)
|
|
487
571
|
|
|
572
|
+
// Check for cached / provided complexity before launching LLM calls
|
|
488
573
|
if (opts.complexity) {
|
|
489
574
|
if (!existsSync(complexityFilePath)) {
|
|
490
575
|
console.error(` ✗ Complexity file not found: ${opts.complexity}`)
|
|
@@ -516,23 +601,107 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
516
601
|
}
|
|
517
602
|
}
|
|
518
603
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
604
|
+
const needsValidation = !opts.skipValidation
|
|
605
|
+
const needsComplexity = !complexity
|
|
606
|
+
|
|
607
|
+
if (needsValidation || needsComplexity) {
|
|
608
|
+
const prdContent = await readFile(prdPath, 'utf8')
|
|
609
|
+
|
|
610
|
+
// Launch validation and complexity in parallel when both are needed
|
|
611
|
+
if (needsValidation && needsComplexity) {
|
|
612
|
+
console.log(stepLabel(2, totalSteps, 'Validating PRD…'))
|
|
613
|
+
console.log(stepLabel(complexityStep, totalSteps, 'Assessing complexity…'))
|
|
614
|
+
console.log(c.dim(` (running in parallel)\n`))
|
|
615
|
+
|
|
616
|
+
const [validationResult, complexityResult] = await Promise.all([
|
|
617
|
+
runPromptStep({
|
|
618
|
+
...sharedOpts,
|
|
619
|
+
template: 'validate-prd',
|
|
620
|
+
goalText: `<!-- validation-pass: 1 -->\n${prdContent}`,
|
|
621
|
+
}).catch((err) => {
|
|
622
|
+
console.error(`\n ✗ Validation failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
623
|
+
return null
|
|
624
|
+
}),
|
|
625
|
+
runPromptStep({
|
|
626
|
+
...sharedOpts,
|
|
627
|
+
template: 'assess-complexity',
|
|
628
|
+
filePath: prdPath,
|
|
629
|
+
contextText: opts.text ?? undefined,
|
|
630
|
+
}).catch((err) => {
|
|
631
|
+
console.warn(c.yellow(` ⚠ Complexity assessment failed: ${err instanceof Error ? err.message : String(err)}`))
|
|
632
|
+
return null
|
|
633
|
+
}),
|
|
634
|
+
])
|
|
635
|
+
|
|
636
|
+
// Process complexity result
|
|
637
|
+
if (complexityResult) {
|
|
638
|
+
complexity = parseComplexityAssessment(complexityResult.rawOutput)
|
|
639
|
+
if (complexity) {
|
|
640
|
+
await writeFile(complexityFilePath, JSON.stringify(complexity, null, 2), 'utf8')
|
|
641
|
+
console.log(c.green(` ✓ Complexity assessment saved to ${relPath(complexityFilePath)}`))
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Process validation result
|
|
646
|
+
if (!validationResult) {
|
|
647
|
+
process.exit(1)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (!validationResult.isValid) {
|
|
651
|
+
let prdValidationErrors = validationResult.errors ?? validationResult.rawOutput
|
|
652
|
+
console.log(c.yellow(` ⚠ PRD has validation issues — attempting auto-fix…\n`))
|
|
653
|
+
console.log(c.dim(prdValidationErrors))
|
|
654
|
+
console.log()
|
|
655
|
+
|
|
656
|
+
await fixPrd(sharedOpts, prdPath, prdContent, prdValidationErrors, totalSteps, opts.adapter)
|
|
657
|
+
} else {
|
|
658
|
+
console.log(c.green(` ✓ PRD is valid\n`))
|
|
659
|
+
}
|
|
660
|
+
} else if (needsValidation) {
|
|
661
|
+
// Only validation needed (complexity was cached)
|
|
662
|
+
console.log(stepLabel(2, totalSteps, 'Validating PRD…'))
|
|
663
|
+
|
|
664
|
+
let result
|
|
665
|
+
try {
|
|
666
|
+
result = await runPromptStep({
|
|
667
|
+
...sharedOpts,
|
|
668
|
+
template: 'validate-prd',
|
|
669
|
+
goalText: `<!-- validation-pass: 1 -->\n${prdContent}`,
|
|
670
|
+
})
|
|
671
|
+
} catch (err) {
|
|
672
|
+
console.error(`\n ✗ Step 2 failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
673
|
+
process.exit(1)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (!result.isValid) {
|
|
677
|
+
let prdValidationErrors = result.errors ?? result.rawOutput
|
|
678
|
+
console.log(c.yellow(` ⚠ PRD has validation issues — attempting auto-fix…\n`))
|
|
679
|
+
console.log(c.dim(prdValidationErrors))
|
|
680
|
+
console.log()
|
|
681
|
+
|
|
682
|
+
await fixPrd(sharedOpts, prdPath, prdContent, prdValidationErrors, totalSteps, opts.adapter)
|
|
683
|
+
} else {
|
|
684
|
+
console.log(c.green(` ✓ PRD is valid\n`))
|
|
685
|
+
}
|
|
686
|
+
} else {
|
|
687
|
+
// Only complexity needed (validation skipped)
|
|
688
|
+
console.log(stepLabel(complexityStep, totalSteps, 'Assessing complexity…'))
|
|
689
|
+
try {
|
|
690
|
+
const complexityResult = await runPromptStep({
|
|
691
|
+
...sharedOpts,
|
|
692
|
+
template: 'assess-complexity',
|
|
693
|
+
filePath: prdPath,
|
|
694
|
+
contextText: opts.text ?? undefined,
|
|
695
|
+
})
|
|
696
|
+
complexity = parseComplexityAssessment(complexityResult.rawOutput)
|
|
697
|
+
if (complexity) {
|
|
698
|
+
await writeFile(complexityFilePath, JSON.stringify(complexity, null, 2), 'utf8')
|
|
699
|
+
console.log(c.green(` ✓ Complexity assessment saved to ${relPath(complexityFilePath)}\n`))
|
|
700
|
+
}
|
|
701
|
+
} catch (err) {
|
|
702
|
+
console.warn(c.yellow(` ⚠ Complexity assessment failed: ${err instanceof Error ? err.message : String(err)}`))
|
|
703
|
+
console.warn(c.dim(` Falling back to single convoy strategy.\n`))
|
|
532
704
|
}
|
|
533
|
-
} catch (err) {
|
|
534
|
-
console.warn(c.yellow(` ⚠ Complexity assessment failed: ${err instanceof Error ? err.message : String(err)}`))
|
|
535
|
-
console.warn(c.dim(` Falling back to single convoy strategy.\n`))
|
|
536
705
|
}
|
|
537
706
|
}
|
|
538
707
|
|
|
@@ -569,10 +738,18 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
569
738
|
const convoyDir = resolve(process.cwd(), '.opencastle', 'convoys')
|
|
570
739
|
await mkdir(convoyDir, { recursive: true })
|
|
571
740
|
|
|
741
|
+
// Extract feature name early for convoy naming
|
|
742
|
+
const chainPrdContent = await readFile(prdPath, 'utf8')
|
|
743
|
+
const featureNameMatch = chainPrdContent.match(/^# (.+?)\s*(?:—|-)?\s*PRD/m)
|
|
744
|
+
const featureName = featureNameMatch
|
|
745
|
+
? featureNameMatch[1].trim().toLowerCase().replace(/[^a-z0-9]+/g, '-')
|
|
746
|
+
: 'feature'
|
|
747
|
+
|
|
572
748
|
const groupSpecPaths: string[] = []
|
|
573
749
|
|
|
574
750
|
for (let i = 0; i < complexity.convoy_groups.length; i++) {
|
|
575
751
|
const group = complexity.convoy_groups[i]
|
|
752
|
+
console.log(c.cyan(` [${i + 1}/${complexity.convoy_groups.length}] Generating convoy: ${group.name}`) + c.dim(` (phases: ${group.phases.join(', ')})`))
|
|
576
753
|
|
|
577
754
|
const chainGoal = [
|
|
578
755
|
complexity.original_prompt,
|
|
@@ -589,8 +766,10 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
589
766
|
].filter(Boolean).join('\n')
|
|
590
767
|
|
|
591
768
|
const prdContent = await readFile(prdPath, 'utf8')
|
|
592
|
-
const
|
|
593
|
-
const
|
|
769
|
+
const relevantPrd = extractRelevantPrdSections(prdContent, group.phases)
|
|
770
|
+
const relevantComplexity = filterTaskComplexityByPhases(complexity?.task_complexity, group.phases)
|
|
771
|
+
const contextForSpec = appendTaskComplexity(relevantPrd, relevantComplexity)
|
|
772
|
+
const groupSpecPath = resolve(convoyDir, `${featureName}-${group.name}.convoy.yml`)
|
|
594
773
|
|
|
595
774
|
const { specPath: resolvedGroupSpecPath } = await generateAndValidateSpec({
|
|
596
775
|
sharedOpts,
|
|
@@ -599,18 +778,13 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
599
778
|
specPath: groupSpecPath,
|
|
600
779
|
skipValidation: opts.skipValidation,
|
|
601
780
|
groupName: group.name,
|
|
781
|
+
featureName,
|
|
602
782
|
enrichment: complexity ? deriveSpecEnrichment(complexity) : undefined,
|
|
603
783
|
})
|
|
604
784
|
groupSpecPaths.push(resolvedGroupSpecPath)
|
|
605
785
|
}
|
|
606
786
|
|
|
607
787
|
// Build master pipeline spec (version 2)
|
|
608
|
-
const chainPrdContent = await readFile(prdPath, 'utf8')
|
|
609
|
-
const featureNameMatch = chainPrdContent.match(/^# (.+?)\s*(?:—|-)?\s*PRD/m)
|
|
610
|
-
const featureName = featureNameMatch
|
|
611
|
-
? featureNameMatch[1].trim().toLowerCase().replace(/[^a-z0-9]+/g, '-')
|
|
612
|
-
: 'feature'
|
|
613
|
-
|
|
614
788
|
const branchMatch = chainPrdContent.match(/`feat\/([^`]+)`/)
|
|
615
789
|
const branch = branchMatch ? `feat/${branchMatch[1]}` : `feat/${featureName}`
|
|
616
790
|
|
|
@@ -777,6 +951,7 @@ async function generateAndValidateSpec(params: {
|
|
|
777
951
|
specPath?: string
|
|
778
952
|
skipValidation: boolean
|
|
779
953
|
groupName?: string
|
|
954
|
+
featureName?: string
|
|
780
955
|
enrichment?: SpecEnrichment
|
|
781
956
|
}): Promise<{ specPath: string; taskPlan: TaskPlan }> {
|
|
782
957
|
const label = params.groupName
|
|
@@ -784,6 +959,14 @@ async function generateAndValidateSpec(params: {
|
|
|
784
959
|
: 'Generating task plan…'
|
|
785
960
|
console.log(c.cyan(` ${label}`))
|
|
786
961
|
|
|
962
|
+
// Temp file for debugging — kept on failure, deleted on success
|
|
963
|
+
const convoyDir = resolve(process.cwd(), '.opencastle', 'convoys')
|
|
964
|
+
await mkdir(convoyDir, { recursive: true })
|
|
965
|
+
const tempJsonName = params.groupName
|
|
966
|
+
? `${params.featureName ? `${params.featureName}-` : ''}${params.groupName}.task-plan.json`
|
|
967
|
+
: `${params.featureName ?? 'task-plan'}.task-plan.json`
|
|
968
|
+
const tempJsonPath = resolve(convoyDir, tempJsonName)
|
|
969
|
+
|
|
787
970
|
let taskPlanResult
|
|
788
971
|
try {
|
|
789
972
|
taskPlanResult = await runPromptStep({
|
|
@@ -797,12 +980,15 @@ async function generateAndValidateSpec(params: {
|
|
|
797
980
|
process.exit(1)
|
|
798
981
|
}
|
|
799
982
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
}
|
|
983
|
+
// Write raw JSON to temp file for debugging
|
|
984
|
+
await writeFile(tempJsonPath, taskPlanResult.rawOutput + '\n', 'utf8')
|
|
985
|
+
|
|
986
|
+
let result = parseTaskPlanWithReason(taskPlanResult.rawOutput)
|
|
987
|
+
if (!result.plan) {
|
|
988
|
+
console.log(c.yellow(` ⚠ Failed to parse task plan JSON: ${result.reason}`))
|
|
989
|
+
console.log(c.dim(` Output length: ${taskPlanResult.rawOutput.length} chars`))
|
|
990
|
+
console.log(c.dim(` Temp file: ${relPath(tempJsonPath)}`))
|
|
991
|
+
console.log(c.yellow(` Retrying generation…\n`))
|
|
806
992
|
|
|
807
993
|
let retryResult
|
|
808
994
|
try {
|
|
@@ -817,14 +1003,24 @@ async function generateAndValidateSpec(params: {
|
|
|
817
1003
|
process.exit(1)
|
|
818
1004
|
}
|
|
819
1005
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1006
|
+
// Overwrite temp file with retry output
|
|
1007
|
+
await writeFile(tempJsonPath, retryResult.rawOutput + '\n', 'utf8')
|
|
1008
|
+
|
|
1009
|
+
result = parseTaskPlanWithReason(retryResult.rawOutput)
|
|
1010
|
+
if (!result.plan) {
|
|
1011
|
+
console.error(` ✗ Failed to parse task plan JSON after retry: ${result.reason}`)
|
|
1012
|
+
console.error(c.dim(` Output length: ${retryResult.rawOutput.length} chars`))
|
|
1013
|
+
console.error(c.dim(` Raw JSON saved to ${relPath(tempJsonPath)} for inspection.`))
|
|
1014
|
+
console.error(c.dim(`\n${retryResult.rawOutput}`))
|
|
824
1015
|
process.exit(1)
|
|
825
1016
|
}
|
|
826
1017
|
}
|
|
827
1018
|
|
|
1019
|
+
let taskPlan = result.plan
|
|
1020
|
+
|
|
1021
|
+
// Success — clean up temp file
|
|
1022
|
+
await unlink(tempJsonPath).catch(() => {})
|
|
1023
|
+
|
|
828
1024
|
console.log(c.green(` ✓ Task plan generated (${taskPlan.tasks.length} tasks)`))
|
|
829
1025
|
|
|
830
1026
|
// Derive spec path from plan name if not provided
|
package/src/cli/plan.ts
CHANGED
|
@@ -152,8 +152,8 @@ function parseFrontmatter(text: string): Record<string, string> {
|
|
|
152
152
|
|
|
153
153
|
/** Extract YAML content from a fenced ```yaml ... ``` block. */
|
|
154
154
|
function extractYamlBlock(text: string): string | null {
|
|
155
|
-
// 1. Prefer explicit yaml/yml fence
|
|
156
|
-
const yamlFence = text.match(/```ya?ml\s*\n([\s\S]
|
|
155
|
+
// 1. Prefer explicit yaml/yml fence — greedy to skip backticks inside content
|
|
156
|
+
const yamlFence = text.match(/```ya?ml\s*\n([\s\S]*)```/)
|
|
157
157
|
if (yamlFence) return yamlFence[1].trim()
|
|
158
158
|
|
|
159
159
|
// 2. Fallback: any code fence whose content looks like a convoy spec
|
|
@@ -363,6 +363,10 @@ export async function runPromptStep(opts: PromptStepOptions): Promise<PromptStep
|
|
|
363
363
|
max_retries: 1,
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
+
if (opts.verbose) {
|
|
367
|
+
console.log(c.dim(` Adapter: ${adapterName} | Template: ${templateName} | Agent: ${agentField}`))
|
|
368
|
+
}
|
|
369
|
+
|
|
366
370
|
const stop = opts.verbose ? null : startProgress(templateName)
|
|
367
371
|
let execResult
|
|
368
372
|
try {
|
|
@@ -373,8 +377,20 @@ export async function runPromptStep(opts: PromptStepOptions): Promise<PromptStep
|
|
|
373
377
|
} finally {
|
|
374
378
|
stop?.()
|
|
375
379
|
}
|
|
380
|
+
|
|
381
|
+
if (!execResult.success) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
`Adapter "${adapterName}" returned failure (exit code ${execResult.exitCode}) for template "${templateName}".\n` +
|
|
384
|
+
`Output (${execResult.output.length} chars):\n${execResult.output.slice(0, 2000)}`
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
|
|
376
388
|
const rawOutput = execResult.output
|
|
377
389
|
|
|
390
|
+
if (opts.verbose) {
|
|
391
|
+
console.log(c.dim(` Output: ${rawOutput.length} chars | Exit code: ${execResult.exitCode}`))
|
|
392
|
+
}
|
|
393
|
+
|
|
378
394
|
if (outputType === 'validation') {
|
|
379
395
|
const { isValid, errors } = parseValidationResult(rawOutput)
|
|
380
396
|
return { outputPath: null, rawOutput, outputType, isValid, errors }
|
|
@@ -382,13 +398,15 @@ export async function runPromptStep(opts: PromptStepOptions): Promise<PromptStep
|
|
|
382
398
|
|
|
383
399
|
if (outputType === 'json') {
|
|
384
400
|
// Extract JSON from a ```json fenced block — fail fast if missing
|
|
385
|
-
|
|
401
|
+
// Use greedy match (.*) to capture up to the LAST closing fence,
|
|
402
|
+
// since task prompts may contain triple backticks in code examples
|
|
403
|
+
const jsonFenceMatch = rawOutput.match(/```json\s*\n([\s\S]*)```/)
|
|
386
404
|
if (!jsonFenceMatch) {
|
|
387
|
-
const preview = rawOutput.slice(0, 300)
|
|
388
405
|
throw new Error(
|
|
389
406
|
`Expected a fenced \`\`\`json block in the AI response but found none.\n\n` +
|
|
390
|
-
`
|
|
391
|
-
`
|
|
407
|
+
`Adapter: ${adapterName} | Output length: ${rawOutput.length} chars\n\n` +
|
|
408
|
+
`Raw output:\n${rawOutput}\n\n` +
|
|
409
|
+
`Tip: re-run with --verbose to see the full adapter output.`
|
|
392
410
|
)
|
|
393
411
|
}
|
|
394
412
|
const jsonContent = jsonFenceMatch[1].trim()
|
|
@@ -417,7 +435,7 @@ export async function runPromptStep(opts: PromptStepOptions): Promise<PromptStep
|
|
|
417
435
|
// convoy-spec (default)
|
|
418
436
|
const yamlContent = extractYamlBlock(rawOutput)
|
|
419
437
|
if (!yamlContent) {
|
|
420
|
-
const preview = rawOutput.slice(0,
|
|
438
|
+
const preview = rawOutput.slice(0, 10_000)
|
|
421
439
|
throw new Error(
|
|
422
440
|
`No YAML code block found in the agent response.\n\nRaw output (truncated):\n${preview}`
|
|
423
441
|
)
|
|
@@ -51,21 +51,21 @@
|
|
|
51
51
|
"name": "docs/api-v2-contract.json",
|
|
52
52
|
"type": "json",
|
|
53
53
|
"task_id": "api-t1",
|
|
54
|
-
"created_at": "2026-03-
|
|
54
|
+
"created_at": "2026-03-18T16:27:21.682Z"
|
|
55
55
|
},
|
|
56
56
|
{
|
|
57
57
|
"id": "artifact-demo-api-v2-reports-security-gate-failure-md",
|
|
58
58
|
"name": "reports/security-gate-failure.md",
|
|
59
59
|
"type": "summary",
|
|
60
60
|
"task_id": "api-t3",
|
|
61
|
-
"created_at": "2026-03-
|
|
61
|
+
"created_at": "2026-03-18T16:27:21.682Z"
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
"id": "artifact-demo-api-v2-src-api-rate-limiter-ts",
|
|
65
65
|
"name": "src/api/rate-limiter.ts",
|
|
66
66
|
"type": "file",
|
|
67
67
|
"task_id": "api-t2",
|
|
68
|
-
"created_at": "2026-03-
|
|
68
|
+
"created_at": "2026-03-18T16:27:21.682Z"
|
|
69
69
|
}
|
|
70
70
|
],
|
|
71
71
|
"has_more_events": false,
|