opencastle 0.29.0 → 0.30.1
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/engine.js +1 -1
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/pipeline.d.ts +17 -0
- package/dist/cli/pipeline.d.ts.map +1 -1
- package/dist/cli/pipeline.js +241 -20
- package/dist/cli/pipeline.js.map +1 -1
- package/dist/cli/pipeline.test.d.ts +2 -0
- package/dist/cli/pipeline.test.d.ts.map +1 -0
- package/dist/cli/pipeline.test.js +178 -0
- package/dist/cli/pipeline.test.js.map +1 -0
- package/dist/cli/plan.d.ts +8 -0
- package/dist/cli/plan.d.ts.map +1 -1
- package/dist/cli/plan.js +77 -1
- package/dist/cli/plan.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/convoy/engine.ts +1 -1
- package/src/cli/pipeline.test.ts +191 -0
- package/src/cli/pipeline.ts +305 -22
- package/src/cli/plan.ts +83 -1
- package/src/dashboard/dist/index.html +398 -5
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/src/pages/index.astro +490 -4
- package/src/orchestrator/agents/team-lead.agent.md +13 -0
- package/src/orchestrator/prompts/fix-prd.prompt.md +58 -0
- package/src/orchestrator/prompts/generate-convoy.prompt.md +23 -0
- package/src/orchestrator/prompts/generate-prd.prompt.md +37 -2
package/src/cli/pipeline.ts
CHANGED
|
@@ -1,10 +1,56 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises'
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
|
-
import { resolve } from 'node:path'
|
|
3
|
+
import { resolve, dirname } from 'node:path'
|
|
4
|
+
import { stringify } from 'yaml'
|
|
4
5
|
import { c, confirm, closePrompts } from './prompt.js'
|
|
5
|
-
import { runPromptStep } from './plan.js'
|
|
6
|
+
import { runPromptStep, readProjectMcpServers } from './plan.js'
|
|
6
7
|
import type { CliContext } from './types.js'
|
|
7
8
|
|
|
9
|
+
export interface ConvoyGroup {
|
|
10
|
+
name: string
|
|
11
|
+
description: string
|
|
12
|
+
phases: number[]
|
|
13
|
+
depends_on: string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ComplexityAssessment {
|
|
17
|
+
total_tasks: number
|
|
18
|
+
total_phases: number
|
|
19
|
+
domains: string[]
|
|
20
|
+
estimated_duration_minutes?: number
|
|
21
|
+
complexity: 'low' | 'medium' | 'high'
|
|
22
|
+
recommended_strategy: 'single' | 'chain'
|
|
23
|
+
chain_rationale?: string
|
|
24
|
+
convoy_groups: ConvoyGroup[]
|
|
25
|
+
}
|
|
26
|
+
|
|
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
|
+
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(jsonMatch[1].trim()) as ComplexityAssessment
|
|
37
|
+
// Validate required fields
|
|
38
|
+
if (
|
|
39
|
+
typeof parsed.total_tasks !== 'number' ||
|
|
40
|
+
typeof parsed.total_phases !== 'number' ||
|
|
41
|
+
!Array.isArray(parsed.domains) ||
|
|
42
|
+
!parsed.complexity ||
|
|
43
|
+
!parsed.recommended_strategy ||
|
|
44
|
+
!Array.isArray(parsed.convoy_groups)
|
|
45
|
+
) {
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
return parsed
|
|
49
|
+
} catch {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
8
54
|
const HELP = `
|
|
9
55
|
opencastle pipeline [options]
|
|
10
56
|
|
|
@@ -12,9 +58,10 @@ const HELP = `
|
|
|
12
58
|
|
|
13
59
|
Step 1 — Generate PRD (generate-prd)
|
|
14
60
|
Step 2 — Validate PRD (validate-prd)
|
|
15
|
-
Step 3 —
|
|
16
|
-
Step 4 —
|
|
17
|
-
Step 5 —
|
|
61
|
+
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)
|
|
18
65
|
|
|
19
66
|
Options:
|
|
20
67
|
--text, -t <text> Feature prompt text (required, unless --prd is set)
|
|
@@ -137,11 +184,13 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
137
184
|
}
|
|
138
185
|
}
|
|
139
186
|
|
|
140
|
-
const totalSteps = opts.skipValidation ? 3 :
|
|
187
|
+
const totalSteps = opts.skipValidation ? 3 : 6
|
|
188
|
+
const mcpServers = await readProjectMcpServers(process.cwd())
|
|
141
189
|
const sharedOpts = {
|
|
142
190
|
adapterName: opts.adapter ?? undefined,
|
|
143
191
|
verbose: opts.verbose,
|
|
144
192
|
pkgRoot,
|
|
193
|
+
...(mcpServers.length ? { mcpServers } : {}),
|
|
145
194
|
}
|
|
146
195
|
|
|
147
196
|
console.log(c.bold('\n opencastle pipeline\n'))
|
|
@@ -196,20 +245,254 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
196
245
|
}
|
|
197
246
|
|
|
198
247
|
if (!result.isValid) {
|
|
199
|
-
|
|
200
|
-
console.log(
|
|
248
|
+
let prdValidationErrors = result.errors ?? result.rawOutput
|
|
249
|
+
console.log(c.yellow(` ⚠ PRD has validation issues — attempting auto-fix…\n`))
|
|
250
|
+
console.log(c.dim(prdValidationErrors))
|
|
251
|
+
console.log()
|
|
252
|
+
|
|
253
|
+
// ── Step 3: Fix PRD (up to 2 retries) ──────────────────────────────────
|
|
254
|
+
const MAX_PRD_FIX_RETRIES = 2
|
|
255
|
+
let fixedPrdContent = prdContent
|
|
256
|
+
let prdFixed = false
|
|
257
|
+
|
|
258
|
+
for (let attempt = 1; attempt <= MAX_PRD_FIX_RETRIES; attempt++) {
|
|
259
|
+
const label = `Fix PRD attempt ${attempt}/${MAX_PRD_FIX_RETRIES}…`
|
|
260
|
+
console.log(stepLabel(3, totalSteps, label))
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
await runPromptStep({
|
|
264
|
+
...sharedOpts,
|
|
265
|
+
template: 'fix-prd',
|
|
266
|
+
goalText: fixedPrdContent,
|
|
267
|
+
contextText: prdValidationErrors,
|
|
268
|
+
outputPath: prdPath, // overwrite in place
|
|
269
|
+
})
|
|
270
|
+
} catch (err) {
|
|
271
|
+
console.error(`\n ✗ Step 3 (attempt ${attempt}) failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
272
|
+
process.exit(1)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.log(c.dim(` Re-validating after fix…`))
|
|
276
|
+
|
|
277
|
+
fixedPrdContent = await readFile(prdPath, 'utf8')
|
|
278
|
+
|
|
279
|
+
let revalidation
|
|
280
|
+
try {
|
|
281
|
+
revalidation = await runPromptStep({
|
|
282
|
+
...sharedOpts,
|
|
283
|
+
template: 'validate-prd',
|
|
284
|
+
goalText: fixedPrdContent,
|
|
285
|
+
})
|
|
286
|
+
} catch (err) {
|
|
287
|
+
console.error(`\n ✗ Re-validation failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
288
|
+
process.exit(1)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (revalidation.isValid) {
|
|
292
|
+
console.log(c.green(` ✓ PRD fixed and validated\n`))
|
|
293
|
+
prdFixed = true
|
|
294
|
+
break
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
prdValidationErrors = revalidation.errors ?? revalidation.rawOutput
|
|
298
|
+
|
|
299
|
+
if (attempt < MAX_PRD_FIX_RETRIES) {
|
|
300
|
+
console.log(c.yellow(` ⚠ Still has issues after fix attempt ${attempt} — retrying…\n`))
|
|
301
|
+
console.log(c.dim(prdValidationErrors))
|
|
302
|
+
console.log()
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!prdFixed) {
|
|
307
|
+
console.log(c.red(`\n ✗ Could not auto-fix the PRD after ${MAX_PRD_FIX_RETRIES} attempts.\n`))
|
|
308
|
+
console.log(` Remaining issues:\n`)
|
|
309
|
+
console.log(prdValidationErrors)
|
|
310
|
+
console.log(
|
|
311
|
+
c.dim(`\n The PRD has been saved to ${relPath(prdPath)} with the best available fixes.\n`) +
|
|
312
|
+
c.dim(` Review the remaining issues above and edit the file manually, then re-run with:\n`) +
|
|
313
|
+
` opencastle pipeline --prd ${relPath(prdPath)}${opts.adapter ? ` --adapter ${opts.adapter}` : ''}\n`
|
|
314
|
+
)
|
|
315
|
+
process.exit(1)
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
console.log(c.green(` ✓ PRD is valid\n`))
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ── Complexity-aware strategy decision ────────────────────────────────────
|
|
323
|
+
const prdContentForComplexity = await readFile(prdPath, 'utf8')
|
|
324
|
+
const complexity = parseComplexityAssessment(prdContentForComplexity)
|
|
325
|
+
|
|
326
|
+
if (complexity) {
|
|
327
|
+
if (complexity.recommended_strategy === 'chain' && complexity.convoy_groups.length > 1) {
|
|
201
328
|
console.log(
|
|
202
|
-
c.
|
|
203
|
-
`
|
|
329
|
+
c.cyan(` ℹ`) +
|
|
330
|
+
` Complexity: ${complexity.complexity} | Strategy: chain | ${complexity.convoy_groups.length} convoy groups\n`
|
|
331
|
+
)
|
|
332
|
+
console.log(` Chain plan:`)
|
|
333
|
+
for (let i = 0; i < complexity.convoy_groups.length; i++) {
|
|
334
|
+
const g = complexity.convoy_groups[i]
|
|
335
|
+
const depStr =
|
|
336
|
+
g.depends_on.length > 0 ? ` → depends on: ${g.depends_on.join(', ')}` : ''
|
|
337
|
+
console.log(
|
|
338
|
+
` ${i + 1}. ${g.name.padEnd(20)} (phases: ${g.phases.join(', ')})${depStr}`
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
console.log()
|
|
342
|
+
|
|
343
|
+
const convoyDir = resolve(process.cwd(), '.opencastle', 'convoys')
|
|
344
|
+
await mkdir(convoyDir, { recursive: true })
|
|
345
|
+
|
|
346
|
+
const genBaseStep = opts.skipValidation ? 2 : 4
|
|
347
|
+
const groupSpecPaths: string[] = []
|
|
348
|
+
const totalGroupSteps =
|
|
349
|
+
(opts.skipValidation ? 2 : 3) + complexity.convoy_groups.length * (opts.skipValidation ? 1 : 2)
|
|
350
|
+
|
|
351
|
+
for (let i = 0; i < complexity.convoy_groups.length; i++) {
|
|
352
|
+
const group = complexity.convoy_groups[i]
|
|
353
|
+
const groupStep = genBaseStep + i * (opts.skipValidation ? 1 : 2)
|
|
354
|
+
|
|
355
|
+
console.log(
|
|
356
|
+
stepLabel(
|
|
357
|
+
groupStep,
|
|
358
|
+
totalGroupSteps,
|
|
359
|
+
`Generating convoy spec for group: ${group.name}…`
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
const chainContext = JSON.stringify({
|
|
364
|
+
mode: 'chain_subset',
|
|
365
|
+
group_name: group.name,
|
|
366
|
+
group_description: group.description,
|
|
367
|
+
group_phases: group.phases,
|
|
368
|
+
depends_on_groups: group.depends_on,
|
|
369
|
+
total_groups: complexity.convoy_groups.length,
|
|
370
|
+
group_index: i + 1,
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
const groupSpecPath = resolve(convoyDir, `${group.name}.convoy.yml`)
|
|
374
|
+
|
|
375
|
+
let groupResult
|
|
376
|
+
try {
|
|
377
|
+
groupResult = await runPromptStep({
|
|
378
|
+
...sharedOpts,
|
|
379
|
+
template: 'generate-convoy',
|
|
380
|
+
filePath: prdPath,
|
|
381
|
+
contextText: chainContext,
|
|
382
|
+
outputPath: groupSpecPath,
|
|
383
|
+
})
|
|
384
|
+
} catch (err) {
|
|
385
|
+
console.error(
|
|
386
|
+
`\n ✗ Step ${groupStep} failed: ${err instanceof Error ? err.message : String(err)}`
|
|
387
|
+
)
|
|
388
|
+
process.exit(1)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const resolvedGroupSpecPath = groupResult.outputPath ?? groupSpecPath
|
|
392
|
+
groupSpecPaths.push(resolvedGroupSpecPath)
|
|
393
|
+
|
|
394
|
+
console.log(c.green(` ✓ Group spec written to ${relPath(resolvedGroupSpecPath)}\n`))
|
|
395
|
+
|
|
396
|
+
if (!opts.skipValidation) {
|
|
397
|
+
const valStep = groupStep + 1
|
|
398
|
+
console.log(stepLabel(valStep, totalGroupSteps, `Validating spec: ${group.name}…`))
|
|
399
|
+
|
|
400
|
+
const groupSpecContent = await readFile(resolvedGroupSpecPath, 'utf8')
|
|
401
|
+
let groupValidation
|
|
402
|
+
try {
|
|
403
|
+
groupValidation = await runPromptStep({
|
|
404
|
+
...sharedOpts,
|
|
405
|
+
template: 'validate-convoy',
|
|
406
|
+
goalText: groupSpecContent,
|
|
407
|
+
})
|
|
408
|
+
} catch (err) {
|
|
409
|
+
console.error(
|
|
410
|
+
`\n ✗ Validation failed for group ${group.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
411
|
+
)
|
|
412
|
+
process.exit(1)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (!groupValidation.isValid) {
|
|
416
|
+
console.log(c.yellow(` ⚠ Spec has issues — attempting one auto-fix…\n`))
|
|
417
|
+
console.log(c.dim(groupValidation.errors ?? groupValidation.rawOutput))
|
|
418
|
+
console.log()
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
await runPromptStep({
|
|
422
|
+
...sharedOpts,
|
|
423
|
+
template: 'fix-convoy',
|
|
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)}`
|
|
431
|
+
)
|
|
432
|
+
process.exit(1)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
console.log(c.dim(` Applied fix for ${group.name}\n`))
|
|
436
|
+
} else {
|
|
437
|
+
console.log(c.green(` ✓ Spec valid\n`))
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Build master pipeline spec (version 2)
|
|
443
|
+
const featureNameMatch = prdContentForComplexity.match(/^# (.+?)\s*(?:—|-)?\s*PRD/m)
|
|
444
|
+
const featureName = featureNameMatch
|
|
445
|
+
? featureNameMatch[1].trim().toLowerCase().replace(/[^a-z0-9]+/g, '-')
|
|
446
|
+
: 'feature'
|
|
447
|
+
|
|
448
|
+
const branchMatch = prdContentForComplexity.match(/`feat\/([^`]+)`/)
|
|
449
|
+
const branch = branchMatch ? `feat/${branchMatch[1]}` : `feat/${featureName}`
|
|
450
|
+
|
|
451
|
+
const masterSpec = {
|
|
452
|
+
name: featureNameMatch ? featureNameMatch[1].trim() : 'Feature Pipeline',
|
|
453
|
+
version: 2,
|
|
454
|
+
branch,
|
|
455
|
+
on_failure: 'stop',
|
|
456
|
+
depends_on_convoy: groupSpecPaths.map(p => relPath(p)),
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const masterSpecPath = resolve(convoyDir, `${featureName}-pipeline.convoy.yml`)
|
|
460
|
+
await writeFile(masterSpecPath, stringify(masterSpec), 'utf8')
|
|
461
|
+
|
|
462
|
+
console.log(c.green(` ✓ Generated convoy chain:\n`))
|
|
463
|
+
for (const p of groupSpecPaths) {
|
|
464
|
+
console.log(` ${relPath(p)}`)
|
|
465
|
+
}
|
|
466
|
+
console.log(` ${relPath(masterSpecPath)} ${c.dim('(master)')}`)
|
|
467
|
+
console.log()
|
|
468
|
+
console.log(
|
|
469
|
+
` ${c.dim('Preview:')} npx opencastle run -f ${relPath(masterSpecPath)} --dry-run\n` +
|
|
470
|
+
` ${c.dim('Execute:')} npx opencastle run -f ${relPath(masterSpecPath)}\n`
|
|
204
471
|
)
|
|
205
|
-
process.exit(1)
|
|
206
|
-
}
|
|
207
472
|
|
|
208
|
-
|
|
473
|
+
try {
|
|
474
|
+
const shouldRun = await confirm('Run the convoy chain now?', true)
|
|
475
|
+
if (shouldRun) {
|
|
476
|
+
closePrompts()
|
|
477
|
+
const runModule = await import('./run.js')
|
|
478
|
+
const runArgs = ['-f', masterSpecPath]
|
|
479
|
+
if (opts.adapter) runArgs.push('-a', opts.adapter)
|
|
480
|
+
if (opts.verbose) runArgs.push('--verbose')
|
|
481
|
+
await runModule.default({ args: runArgs, pkgRoot })
|
|
482
|
+
}
|
|
483
|
+
} finally {
|
|
484
|
+
closePrompts()
|
|
485
|
+
}
|
|
486
|
+
return
|
|
487
|
+
} else {
|
|
488
|
+
console.log(
|
|
489
|
+
c.cyan(` ℹ`) + ` Complexity: ${complexity.complexity} | Strategy: single\n`
|
|
490
|
+
)
|
|
491
|
+
}
|
|
209
492
|
}
|
|
210
493
|
|
|
211
|
-
// ── Step
|
|
212
|
-
const genStep = opts.skipValidation ? 2 :
|
|
494
|
+
// ── Step 4: Generate convoy spec ──────────────────────────────────────────
|
|
495
|
+
const genStep = opts.skipValidation ? 2 : 4
|
|
213
496
|
console.log(stepLabel(genStep, totalSteps, 'Generating convoy spec…'))
|
|
214
497
|
|
|
215
498
|
let specPath: string
|
|
@@ -233,8 +516,8 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
233
516
|
return
|
|
234
517
|
}
|
|
235
518
|
|
|
236
|
-
// ── Step
|
|
237
|
-
console.log(stepLabel(
|
|
519
|
+
// ── Step 5: Validate convoy spec ──────────────────────────────────────────
|
|
520
|
+
console.log(stepLabel(5, totalSteps, 'Validating convoy spec…'))
|
|
238
521
|
|
|
239
522
|
const specContent = await readFile(specPath, 'utf8')
|
|
240
523
|
let validationErrors: string
|
|
@@ -248,7 +531,7 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
248
531
|
goalText: specContent,
|
|
249
532
|
})
|
|
250
533
|
} catch (err) {
|
|
251
|
-
console.error(`\n ✗ Step
|
|
534
|
+
console.error(`\n ✗ Step 5 failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
252
535
|
process.exit(1)
|
|
253
536
|
}
|
|
254
537
|
|
|
@@ -264,13 +547,13 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
264
547
|
console.log()
|
|
265
548
|
}
|
|
266
549
|
|
|
267
|
-
// ── Step
|
|
550
|
+
// ── Step 6: Fix convoy spec (up to 2 retries) ─────────────────────────────
|
|
268
551
|
const MAX_FIX_RETRIES = 2
|
|
269
552
|
let fixedSpecContent = specContent
|
|
270
553
|
|
|
271
554
|
for (let attempt = 1; attempt <= MAX_FIX_RETRIES; attempt++) {
|
|
272
555
|
const label = `Fix attempt ${attempt}/${MAX_FIX_RETRIES}…`
|
|
273
|
-
console.log(stepLabel(
|
|
556
|
+
console.log(stepLabel(6, totalSteps, label))
|
|
274
557
|
|
|
275
558
|
let fixResult
|
|
276
559
|
try {
|
|
@@ -282,7 +565,7 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
282
565
|
outputPath: specPath, // overwrite in place
|
|
283
566
|
})
|
|
284
567
|
} catch (err) {
|
|
285
|
-
console.error(`\n ✗ Step
|
|
568
|
+
console.error(`\n ✗ Step 6 (attempt ${attempt}) failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
286
569
|
process.exit(1)
|
|
287
570
|
}
|
|
288
571
|
|
package/src/cli/plan.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { getAdapter, detectAdapter } 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'
|
|
9
|
+
import type { MCPServerConfig } from './convoy/types.js'
|
|
9
10
|
|
|
10
11
|
const HELP = `
|
|
11
12
|
opencastle plan [options]
|
|
@@ -66,6 +67,8 @@ export interface PromptStepOptions {
|
|
|
66
67
|
dryRun?: boolean
|
|
67
68
|
/** Absolute path to the opencastle package root (for locating prompt templates) */
|
|
68
69
|
pkgRoot: string
|
|
70
|
+
/** MCP servers to make available to the AI adapter during execution. */
|
|
71
|
+
mcpServers?: MCPServerConfig[]
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
export interface PromptStepResult {
|
|
@@ -221,6 +224,34 @@ function parseValidationResult(output: string): { isValid: boolean; errors: stri
|
|
|
221
224
|
return { isValid: false, errors: errorsMatch ? errorsMatch[1].trim() : trimmed }
|
|
222
225
|
}
|
|
223
226
|
|
|
227
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
228
|
+
|
|
229
|
+
const TEMPLATE_MESSAGES: Record<string, string> = {
|
|
230
|
+
'generate-prd': 'Generating PRD…',
|
|
231
|
+
'generate-convoy': 'Generating convoy spec…',
|
|
232
|
+
'validate-prd': 'Validating PRD…',
|
|
233
|
+
'validate-convoy': 'Validating convoy spec…',
|
|
234
|
+
'fix-prd': 'Fixing PRD…',
|
|
235
|
+
'fix-convoy': 'Fixing convoy spec…',
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Show an in-place spinner with elapsed time during a long-running adapter call. Returns a stop function. */
|
|
239
|
+
function startProgress(templateName: string): () => void {
|
|
240
|
+
const message = TEMPLATE_MESSAGES[templateName] ?? `Running ${templateName}…`
|
|
241
|
+
const startTime = Date.now()
|
|
242
|
+
let frame = 0
|
|
243
|
+
const interval = setInterval(() => {
|
|
244
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000)
|
|
245
|
+
const spinner = SPINNER_FRAMES[frame % SPINNER_FRAMES.length]!
|
|
246
|
+
process.stdout.write(c.dim(`\r ${spinner} ${message} (${elapsed}s)`))
|
|
247
|
+
frame++
|
|
248
|
+
}, 250)
|
|
249
|
+
return () => {
|
|
250
|
+
clearInterval(interval)
|
|
251
|
+
process.stdout.write('\r' + ' '.repeat(60) + '\r')
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
224
255
|
// ── Exported programmatic API ───────────────────────────────────────────────
|
|
225
256
|
|
|
226
257
|
/**
|
|
@@ -303,7 +334,16 @@ export async function runPromptStep(opts: PromptStepOptions): Promise<PromptStep
|
|
|
303
334
|
max_retries: 1,
|
|
304
335
|
}
|
|
305
336
|
|
|
306
|
-
const
|
|
337
|
+
const stop = opts.verbose ? null : startProgress(templateName)
|
|
338
|
+
let execResult
|
|
339
|
+
try {
|
|
340
|
+
execResult = await adapter.execute(task, {
|
|
341
|
+
verbose: opts.verbose ?? false,
|
|
342
|
+
...(opts.mcpServers?.length ? { mcpServers: opts.mcpServers } : {}),
|
|
343
|
+
})
|
|
344
|
+
} finally {
|
|
345
|
+
stop?.()
|
|
346
|
+
}
|
|
307
347
|
const rawOutput = execResult.output
|
|
308
348
|
|
|
309
349
|
if (outputType === 'validation') {
|
|
@@ -355,6 +395,48 @@ export async function runPromptStep(opts: PromptStepOptions): Promise<PromptStep
|
|
|
355
395
|
return { outputPath, rawOutput, outputType, isValid: schemaValid }
|
|
356
396
|
}
|
|
357
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Read MCP server configurations from the project's MCP config file.
|
|
400
|
+
* Checks in priority order: .vscode/mcp.json, .cursor/mcp.json, .claude/mcp.json, mcp.json
|
|
401
|
+
*/
|
|
402
|
+
export async function readProjectMcpServers(projectRoot: string): Promise<MCPServerConfig[]> {
|
|
403
|
+
const candidates = [
|
|
404
|
+
join(projectRoot, '.vscode', 'mcp.json'),
|
|
405
|
+
join(projectRoot, '.cursor', 'mcp.json'),
|
|
406
|
+
join(projectRoot, '.claude', 'mcp.json'),
|
|
407
|
+
join(projectRoot, 'mcp.json'),
|
|
408
|
+
]
|
|
409
|
+
|
|
410
|
+
for (const filePath of candidates) {
|
|
411
|
+
if (!existsSync(filePath)) continue
|
|
412
|
+
try {
|
|
413
|
+
const raw = await readFile(filePath, 'utf8')
|
|
414
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>
|
|
415
|
+
|
|
416
|
+
// VS Code format: { servers: { name: { type, command, args } } }
|
|
417
|
+
// Cursor/Claude format: { mcpServers: { name: { command, args } } }
|
|
418
|
+
const serversMap =
|
|
419
|
+
(parsed['servers'] as Record<string, unknown> | undefined) ??
|
|
420
|
+
(parsed['mcpServers'] as Record<string, unknown> | undefined)
|
|
421
|
+
|
|
422
|
+
if (!serversMap || typeof serversMap !== 'object') continue
|
|
423
|
+
|
|
424
|
+
return Object.entries(serversMap).map(([name, cfg]) => {
|
|
425
|
+
const c = cfg as Record<string, unknown>
|
|
426
|
+
const server: MCPServerConfig = { name, type: (c['type'] as string) ?? 'stdio' }
|
|
427
|
+
if (typeof c['command'] === 'string') server.command = c['command']
|
|
428
|
+
if (Array.isArray(c['args'])) server.args = c['args'] as string[]
|
|
429
|
+
if (typeof c['url'] === 'string') server.url = c['url']
|
|
430
|
+
return server
|
|
431
|
+
})
|
|
432
|
+
} catch {
|
|
433
|
+
return []
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return []
|
|
438
|
+
}
|
|
439
|
+
|
|
358
440
|
// ── CLI argument parsing ────────────────────────────────────────────────────
|
|
359
441
|
|
|
360
442
|
function parseArgs(args: string[]): PlanOptions {
|