opencastle 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/pipeline.d.ts +2 -1
- package/dist/cli/pipeline.d.ts.map +1 -1
- package/dist/cli/pipeline.js +123 -60
- 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 +9 -1
- package/dist/cli/plan.d.ts.map +1 -1
- package/dist/cli/plan.js +120 -11
- 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 +145 -62
- package/src/cli/plan.ts +130 -12
- 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 +25 -36
- package/src/orchestrator/prompts/validate-prd.prompt.md +1 -1
package/src/cli/plan.ts
CHANGED
|
@@ -2,10 +2,11 @@ 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'
|
|
9
|
+
import type { MCPServerConfig } from './convoy/types.js'
|
|
9
10
|
|
|
10
11
|
const HELP = `
|
|
11
12
|
opencastle plan [options]
|
|
@@ -18,11 +19,13 @@ const HELP = `
|
|
|
18
19
|
--text, -t <text> Inline text to use as {{goal}} (alternative to --file)
|
|
19
20
|
--template <name> Prompt template name (default: generate-convoy)
|
|
20
21
|
Built-in templates:
|
|
21
|
-
generate-prd
|
|
22
|
-
validate-prd
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
26
29
|
--context <path> Optional path to an additional context file (fills {{context}})
|
|
27
30
|
--context-text <text> Inline text to fill {{context}} (alternative to --context)
|
|
28
31
|
--output, -o <path> Output path override (skipped for validation output)
|
|
@@ -66,6 +69,8 @@ export interface PromptStepOptions {
|
|
|
66
69
|
dryRun?: boolean
|
|
67
70
|
/** Absolute path to the opencastle package root (for locating prompt templates) */
|
|
68
71
|
pkgRoot: string
|
|
72
|
+
/** MCP servers to make available to the AI adapter during execution. */
|
|
73
|
+
mcpServers?: MCPServerConfig[]
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
export interface PromptStepResult {
|
|
@@ -74,7 +79,7 @@ export interface PromptStepResult {
|
|
|
74
79
|
/** Raw text returned by the AI adapter (or assembled prompt on dry-run) */
|
|
75
80
|
rawOutput: string
|
|
76
81
|
/** How the output was interpreted */
|
|
77
|
-
outputType: 'convoy-spec' | 'prd' | 'validation'
|
|
82
|
+
outputType: 'convoy-spec' | 'prd' | 'validation' | 'json'
|
|
78
83
|
/** Set when outputType === 'validation' */
|
|
79
84
|
isValid?: boolean
|
|
80
85
|
/** Set when outputType === 'validation' and isValid === false */
|
|
@@ -147,9 +152,19 @@ function parseFrontmatter(text: string): Record<string, string> {
|
|
|
147
152
|
|
|
148
153
|
/** Extract YAML content from a fenced ```yaml ... ``` block. */
|
|
149
154
|
function extractYamlBlock(text: string): string | null {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
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
|
|
153
168
|
}
|
|
154
169
|
|
|
155
170
|
/**
|
|
@@ -221,6 +236,34 @@ function parseValidationResult(output: string): { isValid: boolean; errors: stri
|
|
|
221
236
|
return { isValid: false, errors: errorsMatch ? errorsMatch[1].trim() : trimmed }
|
|
222
237
|
}
|
|
223
238
|
|
|
239
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
240
|
+
|
|
241
|
+
const TEMPLATE_MESSAGES: Record<string, string> = {
|
|
242
|
+
'generate-prd': 'Generating PRD…',
|
|
243
|
+
'generate-convoy': 'Generating convoy spec…',
|
|
244
|
+
'validate-prd': 'Validating PRD…',
|
|
245
|
+
'validate-convoy': 'Validating convoy spec…',
|
|
246
|
+
'fix-prd': 'Fixing PRD…',
|
|
247
|
+
'fix-convoy': 'Fixing convoy spec…',
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Show an in-place spinner with elapsed time during a long-running adapter call. Returns a stop function. */
|
|
251
|
+
function startProgress(templateName: string): () => void {
|
|
252
|
+
const message = TEMPLATE_MESSAGES[templateName] ?? `Running ${templateName}…`
|
|
253
|
+
const startTime = Date.now()
|
|
254
|
+
let frame = 0
|
|
255
|
+
const interval = setInterval(() => {
|
|
256
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000)
|
|
257
|
+
const spinner = SPINNER_FRAMES[frame % SPINNER_FRAMES.length]!
|
|
258
|
+
process.stdout.write(c.dim(`\r ${spinner} ${message} (${elapsed}s)`))
|
|
259
|
+
frame++
|
|
260
|
+
}, 250)
|
|
261
|
+
return () => {
|
|
262
|
+
clearInterval(interval)
|
|
263
|
+
process.stdout.write('\r' + ' '.repeat(60) + '\r')
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
224
267
|
// ── Exported programmatic API ───────────────────────────────────────────────
|
|
225
268
|
|
|
226
269
|
/**
|
|
@@ -243,7 +286,7 @@ export async function runPromptStep(opts: PromptStepOptions): Promise<PromptStep
|
|
|
243
286
|
|
|
244
287
|
const rawTemplate = await readFile(templatePath, 'utf8')
|
|
245
288
|
const frontmatter = parseFrontmatter(rawTemplate)
|
|
246
|
-
const outputType = (frontmatter['output'] ?? 'convoy-spec') as 'convoy-spec' | 'prd' | 'validation'
|
|
289
|
+
const outputType = (frontmatter['output'] ?? 'convoy-spec') as 'convoy-spec' | 'prd' | 'validation' | 'json'
|
|
247
290
|
const template = stripFrontmatter(rawTemplate)
|
|
248
291
|
|
|
249
292
|
let goalContent = opts.goalText ?? ''
|
|
@@ -303,7 +346,16 @@ export async function runPromptStep(opts: PromptStepOptions): Promise<PromptStep
|
|
|
303
346
|
max_retries: 1,
|
|
304
347
|
}
|
|
305
348
|
|
|
306
|
-
const
|
|
349
|
+
const stop = opts.verbose ? null : startProgress(templateName)
|
|
350
|
+
let execResult
|
|
351
|
+
try {
|
|
352
|
+
execResult = await adapter.execute(task, {
|
|
353
|
+
verbose: opts.verbose ?? false,
|
|
354
|
+
...(opts.mcpServers?.length ? { mcpServers: opts.mcpServers } : {}),
|
|
355
|
+
})
|
|
356
|
+
} finally {
|
|
357
|
+
stop?.()
|
|
358
|
+
}
|
|
307
359
|
const rawOutput = execResult.output
|
|
308
360
|
|
|
309
361
|
if (outputType === 'validation') {
|
|
@@ -311,6 +363,18 @@ export async function runPromptStep(opts: PromptStepOptions): Promise<PromptStep
|
|
|
311
363
|
return { outputPath: null, rawOutput, outputType, isValid, errors }
|
|
312
364
|
}
|
|
313
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
|
+
|
|
314
378
|
if (outputType === 'prd') {
|
|
315
379
|
const content = extractMarkdownBody(rawOutput)
|
|
316
380
|
let outputPath = opts.outputPath ?? null
|
|
@@ -355,6 +419,48 @@ export async function runPromptStep(opts: PromptStepOptions): Promise<PromptStep
|
|
|
355
419
|
return { outputPath, rawOutput, outputType, isValid: schemaValid }
|
|
356
420
|
}
|
|
357
421
|
|
|
422
|
+
/**
|
|
423
|
+
* Read MCP server configurations from the project's MCP config file.
|
|
424
|
+
* Checks in priority order: .vscode/mcp.json, .cursor/mcp.json, .claude/mcp.json, mcp.json
|
|
425
|
+
*/
|
|
426
|
+
export async function readProjectMcpServers(projectRoot: string): Promise<MCPServerConfig[]> {
|
|
427
|
+
const candidates = [
|
|
428
|
+
join(projectRoot, '.vscode', 'mcp.json'),
|
|
429
|
+
join(projectRoot, '.cursor', 'mcp.json'),
|
|
430
|
+
join(projectRoot, '.claude', 'mcp.json'),
|
|
431
|
+
join(projectRoot, 'mcp.json'),
|
|
432
|
+
]
|
|
433
|
+
|
|
434
|
+
for (const filePath of candidates) {
|
|
435
|
+
if (!existsSync(filePath)) continue
|
|
436
|
+
try {
|
|
437
|
+
const raw = await readFile(filePath, 'utf8')
|
|
438
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>
|
|
439
|
+
|
|
440
|
+
// VS Code format: { servers: { name: { type, command, args } } }
|
|
441
|
+
// Cursor/Claude format: { mcpServers: { name: { command, args } } }
|
|
442
|
+
const serversMap =
|
|
443
|
+
(parsed['servers'] as Record<string, unknown> | undefined) ??
|
|
444
|
+
(parsed['mcpServers'] as Record<string, unknown> | undefined)
|
|
445
|
+
|
|
446
|
+
if (!serversMap || typeof serversMap !== 'object') continue
|
|
447
|
+
|
|
448
|
+
return Object.entries(serversMap).map(([name, cfg]) => {
|
|
449
|
+
const c = cfg as Record<string, unknown>
|
|
450
|
+
const server: MCPServerConfig = { name, type: (c['type'] as string) ?? 'stdio' }
|
|
451
|
+
if (typeof c['command'] === 'string') server.command = c['command']
|
|
452
|
+
if (Array.isArray(c['args'])) server.args = c['args'] as string[]
|
|
453
|
+
if (typeof c['url'] === 'string') server.url = c['url']
|
|
454
|
+
return server
|
|
455
|
+
})
|
|
456
|
+
} catch {
|
|
457
|
+
return []
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return []
|
|
462
|
+
}
|
|
463
|
+
|
|
358
464
|
// ── CLI argument parsing ────────────────────────────────────────────────────
|
|
359
465
|
|
|
360
466
|
function parseArgs(args: string[]): PlanOptions {
|
|
@@ -503,6 +609,16 @@ export default async function plan({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
503
609
|
console.log(`\n ${c.dim('Next step:')} opencastle plan --file ${relPath} --template validate-prd`)
|
|
504
610
|
break
|
|
505
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
|
+
}
|
|
506
622
|
default: {
|
|
507
623
|
const relPath = result.outputPath!.startsWith(process.cwd())
|
|
508
624
|
? result.outputPath!.slice(process.cwd().length + 1)
|
|
@@ -517,4 +633,6 @@ export default async function plan({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
517
633
|
`)
|
|
518
634
|
}
|
|
519
635
|
}
|
|
636
|
+
|
|
637
|
+
await cleanupAdapters()
|
|
520
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
|
},
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 'Assess PRD complexity and recommend convoy strategy (single vs chain). Returns structured JSON consumed by the pipeline.'
|
|
3
|
+
agent: 'Reviewer'
|
|
4
|
+
output: json
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<!-- ⚠️ This file is managed by OpenCastle. Edits will be overwritten on update. Customize in the .opencastle/ directory instead. -->
|
|
8
|
+
|
|
9
|
+
# Assess PRD Complexity
|
|
10
|
+
|
|
11
|
+
Analyze the PRD below and produce a complexity assessment as a **single JSON object**. This JSON is consumed programmatically by the pipeline to decide whether to generate one convoy spec or a chain of convoy specs.
|
|
12
|
+
|
|
13
|
+
## PRD to Analyze
|
|
14
|
+
|
|
15
|
+
{{goal}}
|
|
16
|
+
|
|
17
|
+
## Original User Prompt
|
|
18
|
+
|
|
19
|
+
{{context}}
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Output Rules
|
|
24
|
+
|
|
25
|
+
**CRITICAL:** Return ONLY a single fenced JSON block — no prose, no explanation, no markdown headings. Start your response with the opening fence and end with the closing fence.
|
|
26
|
+
|
|
27
|
+
## Required JSON Schema
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"original_prompt": "<string>",
|
|
32
|
+
"total_tasks": <number>,
|
|
33
|
+
"total_phases": <number>,
|
|
34
|
+
"domains": ["<string>", ...],
|
|
35
|
+
"estimated_duration_minutes": <number>,
|
|
36
|
+
"complexity": "low" | "medium" | "high",
|
|
37
|
+
"recommended_strategy": "single" | "chain",
|
|
38
|
+
"chain_rationale": "<string — empty when strategy is single>",
|
|
39
|
+
"convoy_groups": [
|
|
40
|
+
{
|
|
41
|
+
"name": "<kebab-case-name>",
|
|
42
|
+
"description": "<one sentence>",
|
|
43
|
+
"phases": [<phase numbers>],
|
|
44
|
+
"depends_on": ["<group name>", ...]
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Field Rules
|
|
51
|
+
|
|
52
|
+
- `original_prompt`: Copy the user's original feature request verbatim from the "Original User Prompt" section above. If that section is empty, extract a one-sentence summary from the PRD's Overview section.
|
|
53
|
+
- `total_tasks`: Count of individual workstreams in the Task Breakdown.
|
|
54
|
+
- `total_phases`: Count of phases in the Task Breakdown.
|
|
55
|
+
- `domains`: List of technical domains involved (e.g., "frontend", "api", "database", "testing", "config").
|
|
56
|
+
- `estimated_duration_minutes`: Rough estimate assuming AI agent execution (not human).
|
|
57
|
+
- `complexity`: `"low"` (1–4 tasks), `"medium"` (5–8 tasks), `"high"` (9+ tasks).
|
|
58
|
+
- `recommended_strategy`:
|
|
59
|
+
- `"single"` when: total tasks ≤ 8, OR total phases ≤ 3, OR all tasks are tightly coupled with heavy cross-phase file sharing.
|
|
60
|
+
- `"chain"` when: total tasks > 8 AND total phases > 3 AND domains have natural boundaries — AND splitting improves failure isolation, observability, or retry granularity.
|
|
61
|
+
- `chain_rationale`: Only filled when `recommended_strategy` is `"chain"` — explain WHY splitting benefits this specific feature.
|
|
62
|
+
- `convoy_groups`:
|
|
63
|
+
- When `"single"`: exactly one group covering all phases.
|
|
64
|
+
- When `"chain"`: 2–4 groups with explicit `depends_on` order. Each group covers a coherent domain boundary.
|
|
65
|
+
- **Minimum 3 tasks per group.** Never create a group that would produce a convoy with only 1–2 tasks — merge small groups with adjacent ones. A convoy with a single task is pointless overhead.
|
|
66
|
+
- **Do NOT map phases 1:1 to groups.** Groups should bundle multiple related phases when tasks are tightly coupled (e.g., config + data in one group, components + pages in another). Only split at genuine domain boundaries where failure isolation matters.
|
|
67
|
+
- Maximum 3 groups for projects with ≤ 15 tasks. Maximum 4 groups for 16+ tasks.
|
|
@@ -9,11 +9,13 @@ agent: 'Team Lead (OpenCastle)'
|
|
|
9
9
|
|
|
10
10
|
You are the Team Lead. The user wants to run `opencastle run` to execute a batch of tasks autonomously via the convoy engine. Your job is to produce a valid `.convoy.yml` file they can feed to the CLI. Derive a short, descriptive, kebab-case filename from the user's goal (2–4 words max) and use it as the filename — for example `auth-refactor.convoy.yml` or `add-search.convoy.yml`. Always use the `.convoy.yml` extension. Store all generated convoy specs in the `.opencastle/convoys/` directory (create it if it doesn't exist).
|
|
11
11
|
|
|
12
|
+
> **⚠️ OUTPUT FORMAT: Your entire response must be a single ` ```yaml ` fenced code block containing the convoy spec. Do NOT output any text, explanations, summaries, or DAG diagrams before or after the YAML block. The parser only reads the ` ```yaml ` fence — everything else causes a failure.**
|
|
13
|
+
|
|
12
14
|
## User Goal
|
|
13
15
|
|
|
14
16
|
{{goal}}
|
|
15
17
|
|
|
16
|
-
##
|
|
18
|
+
## PRD Reference
|
|
17
19
|
|
|
18
20
|
{{context}}
|
|
19
21
|
|
|
@@ -311,24 +313,17 @@ For complex tasks, consider using `steps` to break the prompt into sequential su
|
|
|
311
313
|
|
|
312
314
|
### Chain Mode (Subset Generation)
|
|
313
315
|
|
|
314
|
-
When the `{{
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
"total_groups": 3,
|
|
324
|
-
"group_index": 1
|
|
325
|
-
}
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
When this context is present:
|
|
329
|
-
- **Only** generate tasks for the phases listed in `group_phases`. Do not include tasks from other phases.
|
|
316
|
+
When the `{{goal}}` section contains a "Convoy Group Scope" heading, you are generating ONE convoy spec that is part of a larger convoy chain. The goal will contain:
|
|
317
|
+
|
|
318
|
+
- The original user prompt
|
|
319
|
+
- The group name, description, phases to cover, and dependency info
|
|
320
|
+
|
|
321
|
+
The full PRD is available in the `{{context}}` section as reference.
|
|
322
|
+
|
|
323
|
+
When chain mode is detected:
|
|
324
|
+
- **Only** generate tasks for the phases listed in the group scope. Do not include tasks from other phases.
|
|
330
325
|
- Use `version: 1` — this spec is a single convoy, not a pipeline.
|
|
331
|
-
- Derive the convoy `name` from
|
|
326
|
+
- Derive the convoy `name` from the group name (e.g., "Database Setup").
|
|
332
327
|
- Derive the `branch` from the PRD's feature name, but it will be overridden by the pipeline anyway.
|
|
333
328
|
- Keep all other conventions (prompts, files, gates, etc.) the same as for single-spec generation.
|
|
334
329
|
|
|
@@ -346,7 +341,7 @@ Before presenting the YAML, mentally verify:
|
|
|
346
341
|
|
|
347
342
|
### 7. Output
|
|
348
343
|
|
|
349
|
-
|
|
344
|
+
Your response must contain **ONLY** a single ` ```yaml ` fenced code block — no text before it, no text after it, no explanations, no summaries, no DAG diagrams. The pipeline parser will only extract content from the ` ```yaml ` fence. Any other text in your response is discarded and may cause parsing failures.
|
|
350
345
|
|
|
351
346
|
````yaml
|
|
352
347
|
# .opencastle/convoys/<feature-name>.convoy.yml
|
|
@@ -393,9 +388,4 @@ gates:
|
|
|
393
388
|
gate_retries: 1
|
|
394
389
|
````
|
|
395
390
|
|
|
396
|
-
Also provide:
|
|
397
|
-
1. A **DAG summary** showing the phase structure so the user can verify execution order.
|
|
398
|
-
2. An **estimated total duration** (sum of timeouts on the critical path).
|
|
399
|
-
3. A `--dry-run` command they can use to validate: `npx opencastle run -f .opencastle/convoys/<feature-name>.convoy.yml --dry-run`
|
|
400
|
-
|
|
401
391
|
|
|
@@ -22,13 +22,20 @@ You are the Team Lead. Convert the feature request below into a structured Produ
|
|
|
22
22
|
|
|
23
23
|
## Research Before Writing
|
|
24
24
|
|
|
25
|
-
If the feature request involves a specific person, place, organization, topic, or any real-world subject
|
|
25
|
+
If the feature request involves a specific person, place, organization, topic, or any real-world subject:
|
|
26
26
|
|
|
27
|
-
**
|
|
27
|
+
1. **Search the internet first** if web search or fetch tools are available (e.g. `fetch_webpage`, web search MCP, or similar). Use the search results to gather accurate facts, names, dates, descriptions, and other details.
|
|
28
|
+
2. **If web search tools are unavailable or return no useful results**, you may use your training knowledge — but clearly mark any such content with:
|
|
29
|
+
> ℹ️ Content based on training data — verify before launch.
|
|
30
|
+
3. **Never fabricate or hallucinate content.** If you genuinely have no knowledge about a real-world subject and cannot search, state what is unknown and use placeholder text. This applies to all content: bios, descriptions, histories, statistics, quotes, and any factual claims.
|
|
31
|
+
|
|
32
|
+
## Output Rules
|
|
33
|
+
|
|
34
|
+
**CRITICAL:** Return the PRD as your text response. Do NOT create any files. Do NOT use file-writing tools. Simply output the full PRD document as text. Do not wrap it in a code fence — start directly with the `#` heading. Do not summarize — output the complete document.
|
|
28
35
|
|
|
29
36
|
## Required PRD Structure
|
|
30
37
|
|
|
31
|
-
Produce the PRD in Markdown using **exactly** the sections below. Do not skip or merge sections.
|
|
38
|
+
Produce the PRD in Markdown using **exactly** the sections below. Do not skip or merge sections.
|
|
32
39
|
|
|
33
40
|
---
|
|
34
41
|
|
|
@@ -53,6 +60,12 @@ Explicit exclusions — what this work does **not** cover. If nothing is exclude
|
|
|
53
60
|
|
|
54
61
|
For each primary scenario, write a user story + binary acceptance criteria. Criteria must be testable (pass/fail — no subjective language).
|
|
55
62
|
|
|
63
|
+
**Quality rules for acceptance criteria (the validator WILL reject violations):**
|
|
64
|
+
- Every criterion must be evaluable as deterministic pass/fail — no subjective language ("looks good", "feels responsive", "is clean", "visually distinct")
|
|
65
|
+
- Do NOT use modal verbs that imply optionality: "should", "might", "could", "may"
|
|
66
|
+
- Do NOT use vague qualifiers: "or equivalent", "or similar", "as needed"
|
|
67
|
+
- State exact expected values (e.g., exact heading text, exact attribute names)
|
|
68
|
+
|
|
56
69
|
**US-1: [Short title]**
|
|
57
70
|
As a [user type], I want [action] so that [benefit].
|
|
58
71
|
|
|
@@ -73,7 +86,7 @@ Specific technical constraints the implementation must respect:
|
|
|
73
86
|
|
|
74
87
|
## Implementation Scope
|
|
75
88
|
|
|
76
|
-
List **every file and directory** that will be created, modified, or deleted. Use specific paths — not broad paths like `src/`. Group by concern.
|
|
89
|
+
List **every file and directory** that will be created, modified, or deleted. Use specific paths — not broad paths like `src/`. Group by concern. Use compact file lists — group related files with commas instead of separate rows when they share a concern. Do NOT use glob patterns (`*`, `**`). Every concern must list at least one specific file.
|
|
77
90
|
|
|
78
91
|
| Concern | Files / Directories |
|
|
79
92
|
|---------|---------------------|
|
|
@@ -92,6 +105,14 @@ List **every file and directory** that will be created, modified, or deleted. Us
|
|
|
92
105
|
|
|
93
106
|
Decompose into the minimum number of phases. Tasks in the same phase run in parallel and **must not share any files**.
|
|
94
107
|
|
|
108
|
+
Keep task descriptions **brief** — 1 sentence each. List only file paths, not explanations. Prefer compact formatting.
|
|
109
|
+
|
|
110
|
+
**Quality rules (the validator WILL reject violations):**
|
|
111
|
+
- Each workstream must list exact files it will modify
|
|
112
|
+
- No two parallel workstreams (same phase) may claim the same file
|
|
113
|
+
- Phases must have explicit dependency declarations (`depends on: Phase N`)
|
|
114
|
+
- No circular dependencies
|
|
115
|
+
|
|
95
116
|
```
|
|
96
117
|
Phase 1 — Foundation (parallel, no dependencies):
|
|
97
118
|
- [Workstream A title]: [2-sentence description]
|
|
@@ -124,35 +145,3 @@ Measurable, binary checks that confirm the feature is shippable:
|
|
|
124
145
|
- **[Open question]**: [What needs to be decided before implementation can start]
|
|
125
146
|
|
|
126
147
|
If there are no risks or open questions, write "None identified."
|
|
127
|
-
|
|
128
|
-
## Complexity Assessment
|
|
129
|
-
|
|
130
|
-
Produce a fenced JSON block with the following fields. This is consumed programmatically by the pipeline to decide whether to generate a single convoy spec or a convoy chain.
|
|
131
|
-
|
|
132
|
-
```json
|
|
133
|
-
{
|
|
134
|
-
"total_tasks": 12,
|
|
135
|
-
"total_phases": 4,
|
|
136
|
-
"domains": ["database", "api", "frontend", "testing"],
|
|
137
|
-
"estimated_duration_minutes": 120,
|
|
138
|
-
"complexity": "low",
|
|
139
|
-
"recommended_strategy": "single",
|
|
140
|
-
"chain_rationale": "",
|
|
141
|
-
"convoy_groups": [
|
|
142
|
-
{
|
|
143
|
-
"name": "full-implementation",
|
|
144
|
-
"description": "All phases in a single convoy",
|
|
145
|
-
"phases": [1, 2, 3, 4],
|
|
146
|
-
"depends_on": []
|
|
147
|
-
}
|
|
148
|
-
]
|
|
149
|
-
}
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
**Strategy decision rules:**
|
|
153
|
-
- Use `"single"` when: total tasks ≤ 8, or total phases ≤ 3, or all tasks are tightly coupled with heavy cross-phase file sharing.
|
|
154
|
-
- Use `"chain"` when: total tasks > 8 AND total phases > 3 AND domains have natural boundaries (e.g., database changes are independent from frontend components from test suites) — AND splitting would improve failure isolation, observability, or retry granularity.
|
|
155
|
-
- When `"single"`: provide exactly one convoy group covering all phases.
|
|
156
|
-
- When `"chain"`: provide 2–4 convoy groups with explicit `depends_on` order. Each group should cover a coherent domain boundary.
|
|
157
|
-
- `complexity` values: `"low"` (1–4 tasks), `"medium"` (5–8 tasks), `"high"` (9+ tasks).
|
|
158
|
-
- `chain_rationale` is only filled when `recommended_strategy` is `"chain"` — explain WHY splitting benefits this specific feature.
|
|
@@ -56,7 +56,7 @@ Evaluate **every item** below. If ALL items pass, respond `VALID`. If ANY item f
|
|
|
56
56
|
|
|
57
57
|
### Language Quality
|
|
58
58
|
|
|
59
|
-
- [ ] No
|
|
59
|
+
- [ ] No **domain-specific** acronyms or jargon used without explanation (standard software acronyms like API, CSS, HTML, CI/CD, CMS, SDK, CLI, URL, JSON, REST, SQL, SSR, SSG, CDN, DNS, TLS, JWT, OAuth, CRUD, DOM, UI, UX, HTTP, HTTPS, LTS, WCAG, RTL, MCP, PRD, E2E are considered universally understood and do not need expansion)
|
|
60
60
|
- [ ] No conflicting requirements (e.g., "must be fast AND run full suite on every change")
|
|
61
61
|
- [ ] Section content is not placeholder/template text (e.g., "2–3 sentences about…", "Description here")
|
|
62
62
|
|