opencastle 0.31.0 → 0.31.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/bin/cli.mjs +4 -2
- package/dist/cli/convoy/spec-builder.d.ts +68 -0
- package/dist/cli/convoy/spec-builder.d.ts.map +1 -0
- package/dist/cli/convoy/spec-builder.js +179 -0
- package/dist/cli/convoy/spec-builder.js.map +1 -0
- package/dist/cli/convoy/spec-builder.test.d.ts +2 -0
- package/dist/cli/convoy/spec-builder.test.d.ts.map +1 -0
- package/dist/cli/convoy/spec-builder.test.js +453 -0
- package/dist/cli/convoy/spec-builder.test.js.map +1 -0
- package/dist/cli/pipeline.d.ts +1 -0
- package/dist/cli/pipeline.d.ts.map +1 -1
- package/dist/cli/pipeline.js +254 -185
- package/dist/cli/pipeline.js.map +1 -1
- package/dist/cli/pipeline.test.js +15 -1
- package/dist/cli/pipeline.test.js.map +1 -1
- package/dist/cli/plan.d.ts +1 -1
- package/dist/cli/plan.d.ts.map +1 -1
- package/dist/cli/plan.js +4 -4
- package/dist/cli/plan.js.map +1 -1
- package/dist/cli/prompt.js +2 -1
- package/dist/cli/prompt.js.map +1 -1
- package/dist/cli/run/adapters/claude.js +2 -2
- package/dist/cli/run/adapters/claude.js.map +1 -1
- package/dist/cli/run/adapters/copilot.js +2 -2
- package/dist/cli/run/adapters/copilot.js.map +1 -1
- package/dist/cli/run/adapters/cursor.js +1 -1
- package/dist/cli/run/adapters/cursor.js.map +1 -1
- package/dist/cli/run/adapters/opencode.js +1 -1
- package/dist/cli/run/adapters/opencode.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/convoy/spec-builder.test.ts +523 -0
- package/src/cli/convoy/spec-builder.ts +221 -0
- package/src/cli/pipeline.test.ts +21 -1
- package/src/cli/pipeline.ts +274 -224
- package/src/cli/plan.ts +5 -4
- package/src/cli/prompt.ts +1 -1
- package/src/cli/run/adapters/claude.ts +2 -2
- package/src/cli/run/adapters/copilot.ts +2 -2
- package/src/cli/run/adapters/cursor.ts +1 -1
- package/src/cli/run/adapters/opencode.ts +1 -1
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/orchestrator/prompts/fix-convoy.prompt.md +47 -56
- package/src/orchestrator/prompts/generate-convoy.prompt.md +85 -295
- package/src/orchestrator/prompts/validate-convoy.prompt.md +31 -42
package/src/cli/pipeline.ts
CHANGED
|
@@ -4,8 +4,12 @@ import { resolve, dirname } 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'
|
|
7
|
+
import type { PromptStepOptions } from './plan.js'
|
|
7
8
|
import { cleanupAdapters } from './run/adapters/index.js'
|
|
8
9
|
import type { CliContext } from './types.js'
|
|
10
|
+
import { parseYaml, validateSpec } from './run/schema.js'
|
|
11
|
+
import { buildConvoyYaml, parseTaskPlan, parsePatches, applyPatches, deriveSpecEnrichment } from './convoy/spec-builder.js'
|
|
12
|
+
import type { TaskPlan, SpecEnrichment } from './convoy/spec-builder.js'
|
|
9
13
|
|
|
10
14
|
export interface ConvoyGroup {
|
|
11
15
|
name: string
|
|
@@ -47,8 +51,15 @@ export function parseComplexityAssessment(jsonText: string): ComplexityAssessmen
|
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
53
|
|
|
54
|
+
export function deriveComplexityPath(prdPath: string): string {
|
|
55
|
+
if (prdPath.endsWith('.prd.md')) {
|
|
56
|
+
return prdPath.slice(0, -'.prd.md'.length) + '.complexity.json'
|
|
57
|
+
}
|
|
58
|
+
return prdPath + '.complexity.json'
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
const HELP = `
|
|
51
|
-
opencastle
|
|
62
|
+
opencastle start [options]
|
|
52
63
|
|
|
53
64
|
Run the full convoy generation pipeline from a feature prompt:
|
|
54
65
|
|
|
@@ -56,9 +67,9 @@ const HELP = `
|
|
|
56
67
|
Step 2 — Validate PRD (validate-prd)
|
|
57
68
|
Step 3 — Fix PRD (fix-prd, up to 2 retries if invalid)
|
|
58
69
|
Step 4 — Assess complexity (assess-complexity, determines single vs chain)
|
|
59
|
-
Step 5 — Generate
|
|
60
|
-
Step 6 — Validate convoy spec (
|
|
61
|
-
Step 7 — Fix convoy spec (
|
|
70
|
+
Step 5 — Generate task plan (generate-convoy outputs JSON, code builds YAML)
|
|
71
|
+
Step 6 — Validate convoy spec (programmatic + semantic validation)
|
|
72
|
+
Step 7 — Fix convoy spec (patch-based fixing, up to 2 retries)
|
|
62
73
|
|
|
63
74
|
Options:
|
|
64
75
|
--text, -t <text> Feature prompt text (required, unless --prd is set)
|
|
@@ -68,6 +79,7 @@ const HELP = `
|
|
|
68
79
|
--adapter, -a <name> Override agent runtime adapter
|
|
69
80
|
--verbose Show full agent output for each step
|
|
70
81
|
--dry-run Generate and print the PRD prompt only, then stop
|
|
82
|
+
--complexity <path> Skip complexity assessment — use an existing complexity file
|
|
71
83
|
--skip-validation Skip PRD and convoy validation (steps 2, 3, 6, 7)
|
|
72
84
|
--help, -h Show this help
|
|
73
85
|
`
|
|
@@ -75,6 +87,7 @@ const HELP = `
|
|
|
75
87
|
interface PipelineOptions {
|
|
76
88
|
text: string | null
|
|
77
89
|
prd: string | null
|
|
90
|
+
complexity: string | null
|
|
78
91
|
outputPrd: string | null
|
|
79
92
|
outputSpec: string | null
|
|
80
93
|
adapter: string | null
|
|
@@ -88,6 +101,7 @@ function parseArgs(args: string[]): PipelineOptions {
|
|
|
88
101
|
const opts: PipelineOptions = {
|
|
89
102
|
text: null,
|
|
90
103
|
prd: null,
|
|
104
|
+
complexity: null,
|
|
91
105
|
outputPrd: null,
|
|
92
106
|
outputSpec: null,
|
|
93
107
|
adapter: null,
|
|
@@ -113,6 +127,10 @@ function parseArgs(args: string[]): PipelineOptions {
|
|
|
113
127
|
if (i + 1 >= args.length) { console.error(' ✗ --prd requires a path'); process.exit(1) }
|
|
114
128
|
opts.prd = args[++i]
|
|
115
129
|
break
|
|
130
|
+
case '--complexity':
|
|
131
|
+
if (i + 1 >= args.length) { console.error(' ✗ --complexity requires a path'); process.exit(1) }
|
|
132
|
+
opts.complexity = args[++i]
|
|
133
|
+
break
|
|
116
134
|
case '--output-prd':
|
|
117
135
|
if (i + 1 >= args.length) { console.error(' ✗ --output-prd requires a path'); process.exit(1) }
|
|
118
136
|
opts.outputPrd = args[++i]
|
|
@@ -192,7 +210,7 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
192
210
|
...(mcpServers.length ? { mcpServers } : {}),
|
|
193
211
|
}
|
|
194
212
|
|
|
195
|
-
console.log(c.bold('\n opencastle
|
|
213
|
+
console.log(c.bold('\n opencastle start\n'))
|
|
196
214
|
|
|
197
215
|
// ── Step 1: Generate PRD ──────────────────────────────────────────────────
|
|
198
216
|
let prdPath: string
|
|
@@ -309,7 +327,7 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
309
327
|
console.log(
|
|
310
328
|
c.dim(`\n The PRD has been saved to ${relPath(prdPath)} with the best available fixes.\n`) +
|
|
311
329
|
c.dim(` Review the remaining issues above and edit the file manually, then re-run with:\n`) +
|
|
312
|
-
` opencastle
|
|
330
|
+
` opencastle start --prd ${relPath(prdPath)}${opts.adapter ? ` --adapter ${opts.adapter}` : ''}\n`
|
|
313
331
|
)
|
|
314
332
|
process.exit(1)
|
|
315
333
|
}
|
|
@@ -320,20 +338,61 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
320
338
|
|
|
321
339
|
// ── Complexity-aware strategy decision ────────────────────────────────────
|
|
322
340
|
const complexityStep = opts.skipValidation ? 2 : 4
|
|
323
|
-
console.log(stepLabel(complexityStep, totalSteps, 'Assessing complexity…'))
|
|
324
341
|
|
|
325
342
|
let complexity: ComplexityAssessment | null = null
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
343
|
+
const complexityFilePath = opts.complexity
|
|
344
|
+
? resolve(process.cwd(), opts.complexity)
|
|
345
|
+
: deriveComplexityPath(prdPath)
|
|
346
|
+
|
|
347
|
+
if (opts.complexity) {
|
|
348
|
+
if (!existsSync(complexityFilePath)) {
|
|
349
|
+
console.error(` ✗ Complexity file not found: ${opts.complexity}`)
|
|
350
|
+
process.exit(1)
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
const raw = await readFile(complexityFilePath, 'utf8')
|
|
354
|
+
complexity = parseComplexityAssessment(raw)
|
|
355
|
+
if (complexity) {
|
|
356
|
+
console.log(c.dim(` [−] Using existing complexity assessment: ${relPath(complexityFilePath)}`))
|
|
357
|
+
} else {
|
|
358
|
+
console.error(` ✗ Invalid complexity file: ${opts.complexity}`)
|
|
359
|
+
process.exit(1)
|
|
360
|
+
}
|
|
361
|
+
} catch (err) {
|
|
362
|
+
console.error(` ✗ Failed to read complexity file: ${err instanceof Error ? err.message : String(err)}`)
|
|
363
|
+
process.exit(1)
|
|
364
|
+
}
|
|
365
|
+
} else if (existsSync(complexityFilePath)) {
|
|
366
|
+
try {
|
|
367
|
+
const raw = await readFile(complexityFilePath, 'utf8')
|
|
368
|
+
const cached = parseComplexityAssessment(raw)
|
|
369
|
+
if (cached) {
|
|
370
|
+
complexity = cached
|
|
371
|
+
console.log(c.dim(` [−] Using existing complexity assessment: ${relPath(complexityFilePath)}`))
|
|
372
|
+
}
|
|
373
|
+
} catch {
|
|
374
|
+
// ignore — fall through to LLM assessment
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (!complexity) {
|
|
379
|
+
console.log(stepLabel(complexityStep, totalSteps, 'Assessing complexity…'))
|
|
380
|
+
try {
|
|
381
|
+
const complexityResult = await runPromptStep({
|
|
382
|
+
...sharedOpts,
|
|
383
|
+
template: 'assess-complexity',
|
|
384
|
+
filePath: prdPath,
|
|
385
|
+
contextText: opts.text ?? undefined,
|
|
386
|
+
})
|
|
387
|
+
complexity = parseComplexityAssessment(complexityResult.rawOutput)
|
|
388
|
+
if (complexity) {
|
|
389
|
+
await writeFile(complexityFilePath, JSON.stringify(complexity, null, 2), 'utf8')
|
|
390
|
+
console.log(c.green(` ✓ Complexity assessment saved to ${relPath(complexityFilePath)}\n`))
|
|
391
|
+
}
|
|
392
|
+
} catch (err) {
|
|
393
|
+
console.warn(c.yellow(` ⚠ Complexity assessment failed: ${err instanceof Error ? err.message : String(err)}`))
|
|
394
|
+
console.warn(c.dim(` Falling back to single convoy strategy.\n`))
|
|
395
|
+
}
|
|
337
396
|
}
|
|
338
397
|
|
|
339
398
|
if (!complexity) {
|
|
@@ -360,22 +419,10 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
360
419
|
const convoyDir = resolve(process.cwd(), '.opencastle', 'convoys')
|
|
361
420
|
await mkdir(convoyDir, { recursive: true })
|
|
362
421
|
|
|
363
|
-
const genBaseStep = opts.skipValidation ? 2 : 4
|
|
364
422
|
const groupSpecPaths: string[] = []
|
|
365
|
-
const totalGroupSteps =
|
|
366
|
-
(opts.skipValidation ? 2 : 3) + complexity.convoy_groups.length * (opts.skipValidation ? 1 : 2)
|
|
367
423
|
|
|
368
424
|
for (let i = 0; i < complexity.convoy_groups.length; i++) {
|
|
369
425
|
const group = complexity.convoy_groups[i]
|
|
370
|
-
const groupStep = genBaseStep + i * (opts.skipValidation ? 1 : 2)
|
|
371
|
-
|
|
372
|
-
console.log(
|
|
373
|
-
stepLabel(
|
|
374
|
-
groupStep,
|
|
375
|
-
totalGroupSteps,
|
|
376
|
-
`Generating convoy spec for group: ${group.name}…`
|
|
377
|
-
)
|
|
378
|
-
)
|
|
379
426
|
|
|
380
427
|
const chainGoal = [
|
|
381
428
|
complexity.original_prompt,
|
|
@@ -394,122 +441,16 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
394
441
|
const prdContent = await readFile(prdPath, 'utf8')
|
|
395
442
|
const groupSpecPath = resolve(convoyDir, `${group.name}.convoy.yml`)
|
|
396
443
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
} catch (err) {
|
|
407
|
-
console.error(
|
|
408
|
-
`\n ✗ Step ${groupStep} failed: ${err instanceof Error ? err.message : String(err)}`
|
|
409
|
-
)
|
|
410
|
-
process.exit(1)
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const resolvedGroupSpecPath = groupResult.outputPath ?? groupSpecPath
|
|
444
|
+
const { specPath: resolvedGroupSpecPath } = await generateAndValidateSpec({
|
|
445
|
+
sharedOpts,
|
|
446
|
+
goalText: chainGoal,
|
|
447
|
+
contextText: prdContent,
|
|
448
|
+
specPath: groupSpecPath,
|
|
449
|
+
skipValidation: opts.skipValidation,
|
|
450
|
+
groupName: group.name,
|
|
451
|
+
enrichment: complexity ? deriveSpecEnrichment(complexity) : undefined,
|
|
452
|
+
})
|
|
414
453
|
groupSpecPaths.push(resolvedGroupSpecPath)
|
|
415
|
-
|
|
416
|
-
console.log(c.green(` ✓ Group spec written to ${relPath(resolvedGroupSpecPath)}\n`))
|
|
417
|
-
|
|
418
|
-
if (!opts.skipValidation) {
|
|
419
|
-
const valStep = groupStep + 1
|
|
420
|
-
console.log(stepLabel(valStep, totalGroupSteps, `Validating spec: ${group.name}…`))
|
|
421
|
-
|
|
422
|
-
let currentSpecContent = await readFile(resolvedGroupSpecPath, 'utf8')
|
|
423
|
-
let groupValidation
|
|
424
|
-
try {
|
|
425
|
-
groupValidation = await runPromptStep({
|
|
426
|
-
...sharedOpts,
|
|
427
|
-
template: 'validate-convoy',
|
|
428
|
-
goalText: currentSpecContent,
|
|
429
|
-
})
|
|
430
|
-
} catch (err) {
|
|
431
|
-
console.error(
|
|
432
|
-
`\n ✗ Validation failed for group ${group.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
433
|
-
)
|
|
434
|
-
process.exit(1)
|
|
435
|
-
}
|
|
436
|
-
|
|
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`)
|
|
446
|
-
)
|
|
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
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
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
|
-
}
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
454
|
}
|
|
514
455
|
|
|
515
456
|
// Build master pipeline spec (version 2)
|
|
@@ -567,99 +508,84 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
567
508
|
}
|
|
568
509
|
|
|
569
510
|
// ── Generate convoy spec ──────────────────────────────────────────────────
|
|
570
|
-
const genStep = opts.skipValidation ? 3 : 5
|
|
571
|
-
console.log(stepLabel(genStep, totalSteps, 'Generating convoy spec…'))
|
|
572
|
-
|
|
573
511
|
const singlePrdContent = await readFile(prdPath, 'utf8')
|
|
574
512
|
const singleGoal = complexity?.original_prompt ?? opts.text ?? ''
|
|
575
513
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
})
|
|
585
|
-
specPath = result.outputPath!
|
|
586
|
-
} catch (err) {
|
|
587
|
-
console.error(`\n ✗ Step ${genStep} failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
588
|
-
process.exit(1)
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
console.log(c.green(` ✓ Convoy spec written to ${relPath(specPath)}\n`))
|
|
514
|
+
const specResult = await generateAndValidateSpec({
|
|
515
|
+
sharedOpts,
|
|
516
|
+
goalText: singleGoal,
|
|
517
|
+
contextText: singlePrdContent,
|
|
518
|
+
specPath: opts.outputSpec ? resolve(process.cwd(), opts.outputSpec) : undefined,
|
|
519
|
+
skipValidation: opts.skipValidation,
|
|
520
|
+
enrichment: complexity ? deriveSpecEnrichment(complexity) : undefined,
|
|
521
|
+
})
|
|
592
522
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
return
|
|
596
|
-
}
|
|
523
|
+
await printFinalSummary(prdPath, specResult.specPath, opts, pkgRoot)
|
|
524
|
+
}
|
|
597
525
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
526
|
+
async function fixViaPatch(
|
|
527
|
+
taskPlan: TaskPlan,
|
|
528
|
+
errors: string,
|
|
529
|
+
sharedOpts: Omit<PromptStepOptions, 'template' | 'goalText' | 'contextText'>,
|
|
530
|
+
specPath: string,
|
|
531
|
+
enrichment?: SpecEnrichment,
|
|
532
|
+
): Promise<TaskPlan> {
|
|
533
|
+
let currentPlan = taskPlan
|
|
534
|
+
let currentErrors = errors
|
|
601
535
|
|
|
602
|
-
|
|
603
|
-
|
|
536
|
+
for (let attempt = 1; attempt <= MAX_FIX_RETRIES; attempt++) {
|
|
537
|
+
console.log(c.dim(` Fix attempt ${attempt}/${MAX_FIX_RETRIES}…`))
|
|
604
538
|
|
|
605
|
-
|
|
606
|
-
let result
|
|
539
|
+
let fixResult
|
|
607
540
|
try {
|
|
608
|
-
|
|
541
|
+
fixResult = await runPromptStep({
|
|
609
542
|
...sharedOpts,
|
|
610
|
-
template: '
|
|
611
|
-
goalText:
|
|
543
|
+
template: 'fix-convoy',
|
|
544
|
+
goalText: JSON.stringify(currentPlan, null, 2),
|
|
545
|
+
contextText: currentErrors,
|
|
612
546
|
})
|
|
613
547
|
} catch (err) {
|
|
614
|
-
console.error(`\n ✗
|
|
548
|
+
console.error(`\n ✗ Fix attempt ${attempt} failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
615
549
|
process.exit(1)
|
|
616
550
|
}
|
|
617
551
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
552
|
+
const patches = parsePatches(fixResult.rawOutput)
|
|
553
|
+
if (!patches || patches.length === 0) {
|
|
554
|
+
console.warn(c.yellow(` ⚠ No valid patches returned`))
|
|
555
|
+
if (attempt >= MAX_FIX_RETRIES) break
|
|
556
|
+
continue
|
|
622
557
|
}
|
|
623
558
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
console.log(c.dim(validationErrors))
|
|
627
|
-
console.log()
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// ── Fix convoy spec (up to 2 retries) ─────────────────────────────────
|
|
631
|
-
const fixStep = opts.skipValidation ? 4 : 7
|
|
632
|
-
let fixedSpecContent = specContent
|
|
633
|
-
|
|
634
|
-
for (let attempt = 1; attempt <= MAX_FIX_RETRIES; attempt++) {
|
|
635
|
-
const label = `Fix attempt ${attempt}/${MAX_FIX_RETRIES}…`
|
|
636
|
-
console.log(stepLabel(fixStep, totalSteps, label))
|
|
559
|
+
console.log(c.dim(` Applied ${patches.length} patches`))
|
|
560
|
+
currentPlan = applyPatches(currentPlan, patches)
|
|
637
561
|
|
|
638
|
-
|
|
562
|
+
// Rebuild YAML and re-validate
|
|
563
|
+
const yaml = buildConvoyYaml(currentPlan, enrichment)
|
|
639
564
|
try {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
565
|
+
const parsed = parseYaml(yaml)
|
|
566
|
+
const { valid, errors: schemaErrors } = validateSpec(parsed)
|
|
567
|
+
if (!valid) {
|
|
568
|
+
currentErrors = schemaErrors.map(e => `- Schema: ${e}`).join('\n')
|
|
569
|
+
if (attempt < MAX_FIX_RETRIES) {
|
|
570
|
+
console.log(c.yellow(` ⚠ Still has schema issues — retrying…\n`))
|
|
571
|
+
console.log(c.dim(currentErrors))
|
|
572
|
+
}
|
|
573
|
+
continue
|
|
574
|
+
}
|
|
647
575
|
} catch (err) {
|
|
648
|
-
|
|
649
|
-
|
|
576
|
+
currentErrors = `YAML error: ${err instanceof Error ? err.message : String(err)}`
|
|
577
|
+
continue
|
|
650
578
|
}
|
|
651
579
|
|
|
580
|
+
await writeFile(specPath, yaml, 'utf8')
|
|
652
581
|
console.log(c.dim(` Re-validating after fix…`))
|
|
653
582
|
|
|
654
|
-
// Read the newly written spec
|
|
655
|
-
fixedSpecContent = await readFile(specPath, 'utf8')
|
|
656
|
-
|
|
657
583
|
let revalidation
|
|
658
584
|
try {
|
|
659
585
|
revalidation = await runPromptStep({
|
|
660
586
|
...sharedOpts,
|
|
661
587
|
template: 'validate-convoy',
|
|
662
|
-
goalText:
|
|
588
|
+
goalText: yaml,
|
|
663
589
|
})
|
|
664
590
|
} catch (err) {
|
|
665
591
|
console.error(`\n ✗ Re-validation failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
@@ -667,32 +593,156 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
667
593
|
}
|
|
668
594
|
|
|
669
595
|
if (revalidation.isValid) {
|
|
670
|
-
console.log(c.green(` ✓
|
|
671
|
-
|
|
672
|
-
return
|
|
596
|
+
console.log(c.green(` ✓ Fixed and validated\n`))
|
|
597
|
+
return currentPlan
|
|
673
598
|
}
|
|
674
599
|
|
|
675
|
-
|
|
676
|
-
|
|
600
|
+
currentErrors = revalidation.errors ?? revalidation.rawOutput
|
|
677
601
|
if (attempt < MAX_FIX_RETRIES) {
|
|
678
|
-
console.log(c.yellow(` ⚠ Still has issues
|
|
679
|
-
console.log(c.dim(
|
|
680
|
-
console.log()
|
|
602
|
+
console.log(c.yellow(` ⚠ Still has issues — retrying…\n`))
|
|
603
|
+
console.log(c.dim(currentErrors))
|
|
681
604
|
}
|
|
682
605
|
}
|
|
683
606
|
|
|
684
|
-
//
|
|
685
|
-
|
|
607
|
+
// Exhausted — write best effort and exit
|
|
608
|
+
await writeFile(specPath, buildConvoyYaml(currentPlan, enrichment), 'utf8')
|
|
609
|
+
console.log(c.red(`\n ✗ Could not auto-fix after ${MAX_FIX_RETRIES} attempts.\n`))
|
|
686
610
|
console.log(` Remaining issues:\n`)
|
|
687
|
-
console.log(
|
|
611
|
+
console.log(currentErrors)
|
|
688
612
|
console.log(
|
|
689
|
-
c.dim(`\n
|
|
690
|
-
|
|
691
|
-
|
|
613
|
+
c.dim(`\n Spec saved to ${relPath(specPath)} with best available fixes.\n`) +
|
|
614
|
+
c.dim(` Edit manually, then re-validate with:\n`) +
|
|
615
|
+
` opencastle plan --file ${relPath(specPath)} --template validate-convoy\n`
|
|
692
616
|
)
|
|
693
617
|
process.exit(1)
|
|
694
618
|
}
|
|
695
619
|
|
|
620
|
+
async function generateAndValidateSpec(params: {
|
|
621
|
+
sharedOpts: Omit<PromptStepOptions, 'template' | 'goalText' | 'contextText'>
|
|
622
|
+
goalText: string
|
|
623
|
+
contextText: string
|
|
624
|
+
specPath?: string
|
|
625
|
+
skipValidation: boolean
|
|
626
|
+
groupName?: string
|
|
627
|
+
enrichment?: SpecEnrichment
|
|
628
|
+
}): Promise<{ specPath: string; taskPlan: TaskPlan }> {
|
|
629
|
+
const label = params.groupName
|
|
630
|
+
? `Generating task plan: ${params.groupName}…`
|
|
631
|
+
: 'Generating task plan…'
|
|
632
|
+
console.log(c.cyan(` ${label}`))
|
|
633
|
+
|
|
634
|
+
let taskPlanResult
|
|
635
|
+
try {
|
|
636
|
+
taskPlanResult = await runPromptStep({
|
|
637
|
+
...params.sharedOpts,
|
|
638
|
+
template: 'generate-convoy',
|
|
639
|
+
goalText: params.goalText,
|
|
640
|
+
contextText: params.contextText,
|
|
641
|
+
})
|
|
642
|
+
} catch (err) {
|
|
643
|
+
console.error(`\n ✗ Task plan generation failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
644
|
+
process.exit(1)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
let taskPlan = parseTaskPlan(taskPlanResult.rawOutput)
|
|
648
|
+
if (!taskPlan) {
|
|
649
|
+
console.log(c.yellow(` ⚠ Failed to parse task plan JSON — retrying generation…\n`))
|
|
650
|
+
if (params.sharedOpts.verbose) {
|
|
651
|
+
console.log(c.dim(taskPlanResult.rawOutput.slice(0, 500)))
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
let retryResult
|
|
655
|
+
try {
|
|
656
|
+
retryResult = await runPromptStep({
|
|
657
|
+
...params.sharedOpts,
|
|
658
|
+
template: 'generate-convoy',
|
|
659
|
+
goalText: params.goalText,
|
|
660
|
+
contextText: params.contextText,
|
|
661
|
+
})
|
|
662
|
+
} catch (err) {
|
|
663
|
+
console.error(`\n ✗ Retry failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
664
|
+
process.exit(1)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
taskPlan = parseTaskPlan(retryResult.rawOutput)
|
|
668
|
+
if (!taskPlan) {
|
|
669
|
+
console.error(' ✗ Failed to parse task plan JSON after retry')
|
|
670
|
+
console.error(c.dim(retryResult.rawOutput.slice(0, 500)))
|
|
671
|
+
process.exit(1)
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
console.log(c.green(` ✓ Task plan generated (${taskPlan.tasks.length} tasks)`))
|
|
676
|
+
|
|
677
|
+
// Derive spec path from plan name if not provided
|
|
678
|
+
let resolvedSpecPath = params.specPath
|
|
679
|
+
if (!resolvedSpecPath) {
|
|
680
|
+
const convoyDir = resolve(process.cwd(), '.opencastle', 'convoys')
|
|
681
|
+
await mkdir(convoyDir, { recursive: true })
|
|
682
|
+
const kebab = taskPlan.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
|
|
683
|
+
resolvedSpecPath = resolve(convoyDir, `${kebab}.convoy.yml`)
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Build YAML from JSON task plan
|
|
687
|
+
let yamlContent = buildConvoyYaml(taskPlan, params.enrichment)
|
|
688
|
+
await mkdir(resolve(resolvedSpecPath, '..'), { recursive: true })
|
|
689
|
+
await writeFile(resolvedSpecPath, yamlContent, 'utf8')
|
|
690
|
+
console.log(c.green(` ✓ Convoy spec written to ${relPath(resolvedSpecPath)}\n`))
|
|
691
|
+
|
|
692
|
+
if (!params.skipValidation) {
|
|
693
|
+
// Programmatic validation first
|
|
694
|
+
try {
|
|
695
|
+
const parsed = parseYaml(yamlContent)
|
|
696
|
+
const { valid, errors: schemaErrors } = validateSpec(parsed)
|
|
697
|
+
if (!valid) {
|
|
698
|
+
console.log(c.yellow(` ⚠ Schema validation issues — auto-fixing…\n`))
|
|
699
|
+
const errorText = schemaErrors.map(e => `- Schema: ${e}`).join('\n')
|
|
700
|
+
console.log(c.dim(errorText))
|
|
701
|
+
console.log()
|
|
702
|
+
taskPlan = await fixViaPatch(taskPlan, errorText, params.sharedOpts, resolvedSpecPath, params.enrichment)
|
|
703
|
+
yamlContent = buildConvoyYaml(taskPlan, params.enrichment)
|
|
704
|
+
await writeFile(resolvedSpecPath, yamlContent, 'utf8')
|
|
705
|
+
} else {
|
|
706
|
+
console.log(c.dim(` ✓ Schema validation passed`))
|
|
707
|
+
}
|
|
708
|
+
} catch (err) {
|
|
709
|
+
console.warn(c.yellow(` ⚠ YAML warning: ${err instanceof Error ? err.message : String(err)}`))
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Semantic validation (LLM)
|
|
713
|
+
const valLabel = params.groupName
|
|
714
|
+
? `Validating spec: ${params.groupName}…`
|
|
715
|
+
: 'Validating convoy spec…'
|
|
716
|
+
console.log(c.cyan(` ${valLabel}`))
|
|
717
|
+
|
|
718
|
+
let semanticResult
|
|
719
|
+
try {
|
|
720
|
+
semanticResult = await runPromptStep({
|
|
721
|
+
...params.sharedOpts,
|
|
722
|
+
template: 'validate-convoy',
|
|
723
|
+
goalText: await readFile(resolvedSpecPath, 'utf8'),
|
|
724
|
+
})
|
|
725
|
+
} catch (err) {
|
|
726
|
+
console.error(`\n ✗ Semantic validation failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
727
|
+
process.exit(1)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (semanticResult.isValid) {
|
|
731
|
+
console.log(c.green(` ✓ Spec is valid\n`))
|
|
732
|
+
} else {
|
|
733
|
+
const semanticErrors = semanticResult.errors ?? semanticResult.rawOutput
|
|
734
|
+
console.log(c.yellow(` ⚠ Semantic issues — auto-fixing…\n`))
|
|
735
|
+
console.log(c.dim(semanticErrors))
|
|
736
|
+
console.log()
|
|
737
|
+
taskPlan = await fixViaPatch(taskPlan, semanticErrors, params.sharedOpts, resolvedSpecPath, params.enrichment)
|
|
738
|
+
yamlContent = buildConvoyYaml(taskPlan, params.enrichment)
|
|
739
|
+
await writeFile(resolvedSpecPath, yamlContent, 'utf8')
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return { specPath: resolvedSpecPath, taskPlan }
|
|
744
|
+
}
|
|
745
|
+
|
|
696
746
|
async function printFinalSummary(
|
|
697
747
|
prdPath: string,
|
|
698
748
|
specPath: string,
|
package/src/cli/plan.ts
CHANGED
|
@@ -268,7 +268,7 @@ function startProgress(templateName: string): () => void {
|
|
|
268
268
|
|
|
269
269
|
/**
|
|
270
270
|
* Execute a single prompt template step via an AI adapter.
|
|
271
|
-
* Used by the
|
|
271
|
+
* Used by the start command to chain steps programmatically.
|
|
272
272
|
*/
|
|
273
273
|
export async function runPromptStep(opts: PromptStepOptions): Promise<PromptStepResult> {
|
|
274
274
|
const templateName = opts.template ?? 'generate-convoy'
|
|
@@ -364,9 +364,10 @@ export async function runPromptStep(opts: PromptStepOptions): Promise<PromptStep
|
|
|
364
364
|
}
|
|
365
365
|
|
|
366
366
|
if (outputType === 'json') {
|
|
367
|
-
// Extract JSON from fenced block
|
|
368
|
-
const
|
|
369
|
-
const jsonContent =
|
|
367
|
+
// Extract JSON from a ```json fenced block only
|
|
368
|
+
const jsonFenceMatch = rawOutput.match(/```json\s*\n([\s\S]*)```/)
|
|
369
|
+
const jsonContent = jsonFenceMatch ? jsonFenceMatch[1].trim() : rawOutput.trim()
|
|
370
|
+
|
|
370
371
|
const outputPath = opts.outputPath ?? null
|
|
371
372
|
if (outputPath) {
|
|
372
373
|
await mkdir(resolve(outputPath, '..'), { recursive: true })
|
package/src/cli/prompt.ts
CHANGED
|
@@ -120,7 +120,7 @@ export function closePrompts(): void {
|
|
|
120
120
|
// Node.js TTY stdin can remain active even after readline.close() — unref it
|
|
121
121
|
// so the process exits cleanly.
|
|
122
122
|
stdin.pause();
|
|
123
|
-
stdin.unref();
|
|
123
|
+
if (typeof stdin.unref === 'function') stdin.unref();
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/**
|