opencastle 0.30.1 → 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 +120 -59
- 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 +1 -1
- package/dist/cli/plan.d.ts.map +1 -1
- package/dist/cli/plan.js +43 -10
- 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 +142 -61
- package/src/cli/plan.ts +47 -11
- 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 +20 -34
- package/src/orchestrator/prompts/validate-prd.prompt.md +1 -1
package/src/cli/pipeline.ts
CHANGED
|
@@ -4,6 +4,7 @@ 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 { 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,7 +183,7 @@ 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
|
|
188
187
|
const mcpServers = await readProjectMcpServers(process.cwd())
|
|
189
188
|
const sharedOpts = {
|
|
190
189
|
adapterName: opts.adapter ?? undefined,
|
|
@@ -320,8 +319,26 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
320
319
|
}
|
|
321
320
|
|
|
322
321
|
// ── Complexity-aware strategy decision ────────────────────────────────────
|
|
323
|
-
const
|
|
324
|
-
|
|
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
|
+
}
|
|
325
342
|
|
|
326
343
|
if (complexity) {
|
|
327
344
|
if (complexity.recommended_strategy === 'chain' && complexity.convoy_groups.length > 1) {
|
|
@@ -360,16 +377,21 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
360
377
|
)
|
|
361
378
|
)
|
|
362
379
|
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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')
|
|
373
395
|
const groupSpecPath = resolve(convoyDir, `${group.name}.convoy.yml`)
|
|
374
396
|
|
|
375
397
|
let groupResult
|
|
@@ -377,8 +399,8 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
377
399
|
groupResult = await runPromptStep({
|
|
378
400
|
...sharedOpts,
|
|
379
401
|
template: 'generate-convoy',
|
|
380
|
-
|
|
381
|
-
contextText:
|
|
402
|
+
goalText: chainGoal,
|
|
403
|
+
contextText: prdContent,
|
|
382
404
|
outputPath: groupSpecPath,
|
|
383
405
|
})
|
|
384
406
|
} catch (err) {
|
|
@@ -397,13 +419,13 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
397
419
|
const valStep = groupStep + 1
|
|
398
420
|
console.log(stepLabel(valStep, totalGroupSteps, `Validating spec: ${group.name}…`))
|
|
399
421
|
|
|
400
|
-
|
|
422
|
+
let currentSpecContent = await readFile(resolvedGroupSpecPath, 'utf8')
|
|
401
423
|
let groupValidation
|
|
402
424
|
try {
|
|
403
425
|
groupValidation = await runPromptStep({
|
|
404
426
|
...sharedOpts,
|
|
405
427
|
template: 'validate-convoy',
|
|
406
|
-
goalText:
|
|
428
|
+
goalText: currentSpecContent,
|
|
407
429
|
})
|
|
408
430
|
} catch (err) {
|
|
409
431
|
console.error(
|
|
@@ -412,40 +434,92 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
412
434
|
process.exit(1)
|
|
413
435
|
}
|
|
414
436
|
|
|
415
|
-
if (
|
|
416
|
-
console.log(c.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
goalText: groupSpecContent,
|
|
425
|
-
contextText: groupValidation.errors ?? groupValidation.rawOutput,
|
|
426
|
-
outputPath: resolvedGroupSpecPath,
|
|
427
|
-
})
|
|
428
|
-
} catch (err) {
|
|
429
|
-
console.error(
|
|
430
|
-
`\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`)
|
|
431
446
|
)
|
|
432
|
-
|
|
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
|
+
}
|
|
433
496
|
}
|
|
434
497
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
+
}
|
|
438
511
|
}
|
|
439
512
|
}
|
|
440
513
|
}
|
|
441
514
|
|
|
442
515
|
// Build master pipeline spec (version 2)
|
|
443
|
-
const
|
|
516
|
+
const chainPrdContent = await readFile(prdPath, 'utf8')
|
|
517
|
+
const featureNameMatch = chainPrdContent.match(/^# (.+?)\s*(?:—|-)?\s*PRD/m)
|
|
444
518
|
const featureName = featureNameMatch
|
|
445
519
|
? featureNameMatch[1].trim().toLowerCase().replace(/[^a-z0-9]+/g, '-')
|
|
446
520
|
: 'feature'
|
|
447
521
|
|
|
448
|
-
const branchMatch =
|
|
522
|
+
const branchMatch = chainPrdContent.match(/`feat\/([^`]+)`/)
|
|
449
523
|
const branch = branchMatch ? `feat/${branchMatch[1]}` : `feat/${featureName}`
|
|
450
524
|
|
|
451
525
|
const masterSpec = {
|
|
@@ -482,6 +556,7 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
482
556
|
}
|
|
483
557
|
} finally {
|
|
484
558
|
closePrompts()
|
|
559
|
+
await cleanupAdapters()
|
|
485
560
|
}
|
|
486
561
|
return
|
|
487
562
|
} else {
|
|
@@ -491,16 +566,20 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
491
566
|
}
|
|
492
567
|
}
|
|
493
568
|
|
|
494
|
-
// ──
|
|
495
|
-
const genStep = opts.skipValidation ?
|
|
569
|
+
// ── Generate convoy spec ──────────────────────────────────────────────────
|
|
570
|
+
const genStep = opts.skipValidation ? 3 : 5
|
|
496
571
|
console.log(stepLabel(genStep, totalSteps, 'Generating convoy spec…'))
|
|
497
572
|
|
|
573
|
+
const singlePrdContent = await readFile(prdPath, 'utf8')
|
|
574
|
+
const singleGoal = complexity?.original_prompt ?? opts.text ?? ''
|
|
575
|
+
|
|
498
576
|
let specPath: string
|
|
499
577
|
try {
|
|
500
578
|
const result = await runPromptStep({
|
|
501
579
|
...sharedOpts,
|
|
502
580
|
template: 'generate-convoy',
|
|
503
|
-
|
|
581
|
+
goalText: singleGoal,
|
|
582
|
+
contextText: singlePrdContent,
|
|
504
583
|
outputPath: opts.outputSpec ? resolve(process.cwd(), opts.outputSpec) : undefined,
|
|
505
584
|
})
|
|
506
585
|
specPath = result.outputPath!
|
|
@@ -516,8 +595,9 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
516
595
|
return
|
|
517
596
|
}
|
|
518
597
|
|
|
519
|
-
// ──
|
|
520
|
-
|
|
598
|
+
// ── Validate convoy spec ──────────────────────────────────────────────
|
|
599
|
+
const valStep = opts.skipValidation ? 4 : 6
|
|
600
|
+
console.log(stepLabel(valStep, totalSteps, 'Validating convoy spec…'))
|
|
521
601
|
|
|
522
602
|
const specContent = await readFile(specPath, 'utf8')
|
|
523
603
|
let validationErrors: string
|
|
@@ -531,7 +611,7 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
531
611
|
goalText: specContent,
|
|
532
612
|
})
|
|
533
613
|
} catch (err) {
|
|
534
|
-
console.error(`\n ✗ Step
|
|
614
|
+
console.error(`\n ✗ Step ${valStep} failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
535
615
|
process.exit(1)
|
|
536
616
|
}
|
|
537
617
|
|
|
@@ -547,13 +627,13 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
547
627
|
console.log()
|
|
548
628
|
}
|
|
549
629
|
|
|
550
|
-
// ──
|
|
551
|
-
const
|
|
630
|
+
// ── Fix convoy spec (up to 2 retries) ─────────────────────────────────
|
|
631
|
+
const fixStep = opts.skipValidation ? 4 : 7
|
|
552
632
|
let fixedSpecContent = specContent
|
|
553
633
|
|
|
554
634
|
for (let attempt = 1; attempt <= MAX_FIX_RETRIES; attempt++) {
|
|
555
635
|
const label = `Fix attempt ${attempt}/${MAX_FIX_RETRIES}…`
|
|
556
|
-
console.log(stepLabel(
|
|
636
|
+
console.log(stepLabel(fixStep, totalSteps, label))
|
|
557
637
|
|
|
558
638
|
let fixResult
|
|
559
639
|
try {
|
|
@@ -641,5 +721,6 @@ async function printFinalSummary(
|
|
|
641
721
|
}
|
|
642
722
|
} finally {
|
|
643
723
|
closePrompts()
|
|
724
|
+
await cleanupAdapters()
|
|
644
725
|
}
|
|
645
726
|
}
|
package/src/cli/plan.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
3
|
import { resolve, join, basename } from 'node:path'
|
|
4
4
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
5
|
-
import { getAdapter, detectAdapter } from './run/adapters/index.js'
|
|
5
|
+
import { getAdapter, detectAdapter, cleanupAdapters } from './run/adapters/index.js'
|
|
6
6
|
import { parseTaskSpecText } from './run/schema.js'
|
|
7
7
|
import { c } from './prompt.js'
|
|
8
8
|
import type { CliContext, Task } from './types.js'
|
|
@@ -19,11 +19,13 @@ const HELP = `
|
|
|
19
19
|
--text, -t <text> Inline text to use as {{goal}} (alternative to --file)
|
|
20
20
|
--template <name> Prompt template name (default: generate-convoy)
|
|
21
21
|
Built-in templates:
|
|
22
|
-
generate-prd
|
|
23
|
-
validate-prd
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
generate-prd — Write a PRD from a feature prompt
|
|
23
|
+
validate-prd — Check a PRD for completeness
|
|
24
|
+
fix-prd — Fix validation errors in a PRD
|
|
25
|
+
assess-complexity — Assess PRD complexity (returns JSON)
|
|
26
|
+
generate-convoy — Generate a convoy spec from a PRD (default)
|
|
27
|
+
validate-convoy — Check a convoy spec for correctness
|
|
28
|
+
fix-convoy — Fix validation errors in a convoy spec
|
|
27
29
|
--context <path> Optional path to an additional context file (fills {{context}})
|
|
28
30
|
--context-text <text> Inline text to fill {{context}} (alternative to --context)
|
|
29
31
|
--output, -o <path> Output path override (skipped for validation output)
|
|
@@ -77,7 +79,7 @@ export interface PromptStepResult {
|
|
|
77
79
|
/** Raw text returned by the AI adapter (or assembled prompt on dry-run) */
|
|
78
80
|
rawOutput: string
|
|
79
81
|
/** How the output was interpreted */
|
|
80
|
-
outputType: 'convoy-spec' | 'prd' | 'validation'
|
|
82
|
+
outputType: 'convoy-spec' | 'prd' | 'validation' | 'json'
|
|
81
83
|
/** Set when outputType === 'validation' */
|
|
82
84
|
isValid?: boolean
|
|
83
85
|
/** Set when outputType === 'validation' and isValid === false */
|
|
@@ -150,9 +152,19 @@ function parseFrontmatter(text: string): Record<string, string> {
|
|
|
150
152
|
|
|
151
153
|
/** Extract YAML content from a fenced ```yaml ... ``` block. */
|
|
152
154
|
function extractYamlBlock(text: string): string | null {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return
|
|
155
|
+
// 1. Prefer explicit yaml/yml fence
|
|
156
|
+
const yamlFence = text.match(/```ya?ml\s*\n([\s\S]*?)```/)
|
|
157
|
+
if (yamlFence) return yamlFence[1].trim()
|
|
158
|
+
|
|
159
|
+
// 2. Fallback: any code fence whose content looks like a convoy spec
|
|
160
|
+
// Must contain at least `name:` AND `tasks:` to avoid false positives
|
|
161
|
+
const genericFences = [...text.matchAll(/```\s*\n([\s\S]*?)```/g)]
|
|
162
|
+
for (const m of genericFences) {
|
|
163
|
+
const content = m[1].trim()
|
|
164
|
+
if (/^name:/m.test(content) && /^tasks:/m.test(content)) return content
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return null
|
|
156
168
|
}
|
|
157
169
|
|
|
158
170
|
/**
|
|
@@ -274,7 +286,7 @@ export async function runPromptStep(opts: PromptStepOptions): Promise<PromptStep
|
|
|
274
286
|
|
|
275
287
|
const rawTemplate = await readFile(templatePath, 'utf8')
|
|
276
288
|
const frontmatter = parseFrontmatter(rawTemplate)
|
|
277
|
-
const outputType = (frontmatter['output'] ?? 'convoy-spec') as 'convoy-spec' | 'prd' | 'validation'
|
|
289
|
+
const outputType = (frontmatter['output'] ?? 'convoy-spec') as 'convoy-spec' | 'prd' | 'validation' | 'json'
|
|
278
290
|
const template = stripFrontmatter(rawTemplate)
|
|
279
291
|
|
|
280
292
|
let goalContent = opts.goalText ?? ''
|
|
@@ -351,6 +363,18 @@ export async function runPromptStep(opts: PromptStepOptions): Promise<PromptStep
|
|
|
351
363
|
return { outputPath: null, rawOutput, outputType, isValid, errors }
|
|
352
364
|
}
|
|
353
365
|
|
|
366
|
+
if (outputType === 'json') {
|
|
367
|
+
// Extract JSON from fenced block or raw output
|
|
368
|
+
const jsonMatch = rawOutput.match(/```(?:json)?\s*\n([\s\S]*?)```/)
|
|
369
|
+
const jsonContent = jsonMatch ? jsonMatch[1].trim() : rawOutput.trim()
|
|
370
|
+
const outputPath = opts.outputPath ?? null
|
|
371
|
+
if (outputPath) {
|
|
372
|
+
await mkdir(resolve(outputPath, '..'), { recursive: true })
|
|
373
|
+
await writeFile(outputPath, jsonContent + '\n', 'utf8')
|
|
374
|
+
}
|
|
375
|
+
return { outputPath, rawOutput: jsonContent, outputType }
|
|
376
|
+
}
|
|
377
|
+
|
|
354
378
|
if (outputType === 'prd') {
|
|
355
379
|
const content = extractMarkdownBody(rawOutput)
|
|
356
380
|
let outputPath = opts.outputPath ?? null
|
|
@@ -585,6 +609,16 @@ export default async function plan({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
585
609
|
console.log(`\n ${c.dim('Next step:')} opencastle plan --file ${relPath} --template validate-prd`)
|
|
586
610
|
break
|
|
587
611
|
}
|
|
612
|
+
case 'json': {
|
|
613
|
+
if (result.outputPath) {
|
|
614
|
+
const relP = result.outputPath.startsWith(process.cwd())
|
|
615
|
+
? result.outputPath.slice(process.cwd().length + 1)
|
|
616
|
+
: result.outputPath
|
|
617
|
+
console.log(c.green(` ✓ JSON written to ${relP}`))
|
|
618
|
+
}
|
|
619
|
+
console.log(result.rawOutput)
|
|
620
|
+
break
|
|
621
|
+
}
|
|
588
622
|
default: {
|
|
589
623
|
const relPath = result.outputPath!.startsWith(process.cwd())
|
|
590
624
|
? result.outputPath!.slice(process.cwd().length + 1)
|
|
@@ -599,4 +633,6 @@ export default async function plan({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
599
633
|
`)
|
|
600
634
|
}
|
|
601
635
|
}
|
|
636
|
+
|
|
637
|
+
await cleanupAdapters()
|
|
602
638
|
}
|
|
@@ -108,7 +108,7 @@ async function executeViaSdk(task: Task, options: ExecuteOptions = {}): Promise<
|
|
|
108
108
|
} : undefined
|
|
109
109
|
return {
|
|
110
110
|
success: true,
|
|
111
|
-
output: output.slice(0,
|
|
111
|
+
output: output.slice(0, 100_000),
|
|
112
112
|
exitCode: 0,
|
|
113
113
|
usage: usageResult,
|
|
114
114
|
}
|
|
@@ -246,3 +246,13 @@ export function kill(task: Task): void {
|
|
|
246
246
|
if (mode === 'sdk') killSdk(task)
|
|
247
247
|
else killCli(task)
|
|
248
248
|
}
|
|
249
|
+
|
|
250
|
+
export async function cleanup(): Promise<void> {
|
|
251
|
+
if (clientPromise) {
|
|
252
|
+
try {
|
|
253
|
+
const client = await clientPromise
|
|
254
|
+
await client.stop()
|
|
255
|
+
} catch { /* ignore */ }
|
|
256
|
+
clientPromise = null
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -45,6 +45,19 @@ export async function detectAdapter(): Promise<string | null> {
|
|
|
45
45
|
return null
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Clean up all loaded adapters (stop SDK clients, close connections).
|
|
50
|
+
* Call this before process exit to avoid hanging.
|
|
51
|
+
*/
|
|
52
|
+
export async function cleanupAdapters(): Promise<void> {
|
|
53
|
+
for (const loader of Object.values(ADAPTERS)) {
|
|
54
|
+
try {
|
|
55
|
+
const mod = await loader()
|
|
56
|
+
await mod.cleanup?.()
|
|
57
|
+
} catch { /* ignore */ }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
/**
|
|
49
62
|
* List all registered adapters with their availability status.
|
|
50
63
|
*/
|
package/src/cli/run.ts
CHANGED
|
@@ -756,6 +756,31 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
756
756
|
if (spec.branch) console.log(` Branch: ${spec.branch}`)
|
|
757
757
|
if (spec.gates?.length) console.log(` Gates: ${spec.gates.length} validation commands`)
|
|
758
758
|
|
|
759
|
+
// ── Pre-flight: handle uncommitted changes before branch switch ──
|
|
760
|
+
let pipelineDidStash = false
|
|
761
|
+
if (spec.branch) {
|
|
762
|
+
const { execFile: execFileCb } = await import('node:child_process')
|
|
763
|
+
const { promisify } = await import('node:util')
|
|
764
|
+
const execFile = promisify(execFileCb)
|
|
765
|
+
const { stdout: statusOut } = await execFile('git', ['status', '--porcelain'], {
|
|
766
|
+
cwd: process.cwd(),
|
|
767
|
+
})
|
|
768
|
+
if (statusOut.trim()) {
|
|
769
|
+
console.log(`\n ${c.yellow('⚠')} Uncommitted changes detected.`)
|
|
770
|
+
const shouldStash = await confirm('Stash changes and continue?', true)
|
|
771
|
+
if (!shouldStash) {
|
|
772
|
+
console.log(' Aborted. Commit or stash your changes manually, then retry.')
|
|
773
|
+
closePrompts()
|
|
774
|
+
process.exit(1)
|
|
775
|
+
}
|
|
776
|
+
await execFile('git', ['stash', 'push', '-m', 'opencastle: auto-stash before pipeline'], {
|
|
777
|
+
cwd: process.cwd(),
|
|
778
|
+
})
|
|
779
|
+
pipelineDidStash = true
|
|
780
|
+
console.log(` ${c.green('✓')} Changes stashed.`)
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
759
784
|
const { startDashboardServer } = await import('./dashboard.js')
|
|
760
785
|
let pipelineDashboardResult: { server: import('node:http').Server; port: number; url: string } | null = null
|
|
761
786
|
try {
|
|
@@ -789,12 +814,31 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
789
814
|
throw err
|
|
790
815
|
}
|
|
791
816
|
printPipelineResult(pipelineResult)
|
|
817
|
+
if (pipelineDidStash) {
|
|
818
|
+
const { execFile: execFileCb } = await import('node:child_process')
|
|
819
|
+
const { promisify } = await import('node:util')
|
|
820
|
+
const execFile = promisify(execFileCb)
|
|
821
|
+
try {
|
|
822
|
+
await execFile('git', ['stash', 'pop'], { cwd: process.cwd() })
|
|
823
|
+
console.log(` ${c.green('✓')} Stashed changes restored.`)
|
|
824
|
+
} catch {
|
|
825
|
+
console.log(` ${c.yellow('⚠')} Could not restore stash automatically. Run \`git stash pop\` manually.`)
|
|
826
|
+
}
|
|
827
|
+
}
|
|
792
828
|
if (pipelineDashboardResult) {
|
|
793
829
|
console.log(`\n ${c.dim('Results saved to .opencastle/convoy.db')}`)
|
|
794
|
-
console.log(` ${c.dim('
|
|
795
|
-
|
|
830
|
+
console.log(` ${c.dim('Dashboard:')} ${pipelineDashboardResult.url}`)
|
|
831
|
+
console.log(`\n Press Ctrl+C to stop`)
|
|
832
|
+
const exitCode = pipelineResult.status !== 'done' ? 1 : 0
|
|
833
|
+
process.on('SIGINT', () => {
|
|
834
|
+
console.log('\n Dashboard stopped.\n')
|
|
835
|
+
pipelineDashboardResult!.server.close()
|
|
836
|
+
process.exit(exitCode)
|
|
837
|
+
})
|
|
838
|
+
} else {
|
|
839
|
+
process.exit(pipelineResult.status !== 'done' ? 1 : 0)
|
|
796
840
|
}
|
|
797
|
-
|
|
841
|
+
return
|
|
798
842
|
}
|
|
799
843
|
|
|
800
844
|
// ── Convoy engine path (version: 1 specs) ────────────────────
|
|
@@ -882,11 +926,6 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
882
926
|
throw err
|
|
883
927
|
}
|
|
884
928
|
printConvoyResult(result)
|
|
885
|
-
if (dashboardResult) {
|
|
886
|
-
console.log(`\n ${c.dim('Results saved to .opencastle/convoy.db')}`)
|
|
887
|
-
console.log(` ${c.dim('View again:')} opencastle dashboard`)
|
|
888
|
-
dashboardResult.server.close()
|
|
889
|
-
}
|
|
890
929
|
if (didStash) {
|
|
891
930
|
const { execFile: execFileCb } = await import('node:child_process')
|
|
892
931
|
const { promisify } = await import('node:util')
|
|
@@ -898,7 +937,19 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
898
937
|
console.log(` ${c.yellow('⚠')} Could not restore stash automatically. Run \`git stash pop\` manually.`)
|
|
899
938
|
}
|
|
900
939
|
}
|
|
901
|
-
|
|
940
|
+
if (dashboardResult) {
|
|
941
|
+
console.log(`\n ${c.dim('Results saved to .opencastle/convoy.db')}`)
|
|
942
|
+
console.log(` ${c.dim('Dashboard:')} ${dashboardResult.url}`)
|
|
943
|
+
console.log(`\n Press Ctrl+C to stop`)
|
|
944
|
+
const exitCode = result.status !== 'done' ? 1 : 0
|
|
945
|
+
process.on('SIGINT', () => {
|
|
946
|
+
console.log('\n Dashboard stopped.\n')
|
|
947
|
+
dashboardResult!.server.close()
|
|
948
|
+
process.exit(exitCode)
|
|
949
|
+
})
|
|
950
|
+
} else {
|
|
951
|
+
process.exit(result.status !== 'done' ? 1 : 0)
|
|
952
|
+
}
|
|
902
953
|
}
|
|
903
954
|
|
|
904
955
|
// ── Legacy executor path ──────────────────────────────────────
|
package/src/cli/types.ts
CHANGED
|
@@ -300,6 +300,8 @@ export interface AgentAdapter {
|
|
|
300
300
|
kill?(_task: Task): void;
|
|
301
301
|
/** Whether the adapter supports reusing sessions across multi-step task steps. Defaults to false. */
|
|
302
302
|
supportsSessionContinuity?(): boolean;
|
|
303
|
+
/** Clean up any long-lived resources (SDK clients, open connections) so the process can exit. */
|
|
304
|
+
cleanup?(): Promise<void>;
|
|
303
305
|
}
|
|
304
306
|
|
|
305
307
|
/** Options for agent execution. */
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
|
-
"hash": "
|
|
2
|
+
"hash": "92391349",
|
|
3
3
|
"configHash": "30f8ea04",
|
|
4
|
-
"lockfileHash": "
|
|
5
|
-
"browserHash": "
|
|
4
|
+
"lockfileHash": "f32f6327",
|
|
5
|
+
"browserHash": "a3143d98",
|
|
6
6
|
"optimized": {
|
|
7
7
|
"astro > cssesc": {
|
|
8
8
|
"src": "../../../../../node_modules/cssesc/cssesc.js",
|
|
9
9
|
"file": "astro___cssesc.js",
|
|
10
|
-
"fileHash": "
|
|
10
|
+
"fileHash": "4da4d2a1",
|
|
11
11
|
"needsInterop": true
|
|
12
12
|
},
|
|
13
13
|
"astro > aria-query": {
|
|
14
14
|
"src": "../../../../../node_modules/aria-query/lib/index.js",
|
|
15
15
|
"file": "astro___aria-query.js",
|
|
16
|
-
"fileHash": "
|
|
16
|
+
"fileHash": "65e6b113",
|
|
17
17
|
"needsInterop": true
|
|
18
18
|
},
|
|
19
19
|
"astro > axobject-query": {
|
|
20
20
|
"src": "../../../../../node_modules/axobject-query/lib/index.js",
|
|
21
21
|
"file": "astro___axobject-query.js",
|
|
22
|
-
"fileHash": "
|
|
22
|
+
"fileHash": "bd7268ed",
|
|
23
23
|
"needsInterop": true
|
|
24
24
|
}
|
|
25
25
|
},
|