opencastle 0.31.3 → 0.31.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/convoy/spec-builder.d.ts.map +1 -1
- package/dist/cli/convoy/spec-builder.js +66 -1
- package/dist/cli/convoy/spec-builder.js.map +1 -1
- package/dist/cli/convoy/spec-builder.test.js +99 -2
- package/dist/cli/convoy/spec-builder.test.js.map +1 -1
- package/dist/cli/pipeline.d.ts +5 -0
- package/dist/cli/pipeline.d.ts.map +1 -1
- package/dist/cli/pipeline.js +204 -79
- package/dist/cli/pipeline.js.map +1 -1
- package/dist/cli/pipeline.test.js +122 -1
- package/dist/cli/pipeline.test.js.map +1 -1
- package/dist/cli/plan.d.ts.map +1 -1
- package/dist/cli/plan.js +27 -4
- package/dist/cli/plan.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +88 -18
- package/dist/cli/run.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/convoy/spec-builder.test.ts +108 -2
- package/src/cli/convoy/spec-builder.ts +66 -1
- package/src/cli/pipeline.test.ts +130 -1
- package/src/cli/pipeline.ts +224 -89
- package/src/cli/plan.ts +29 -4
- package/src/cli/run.ts +84 -18
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/orchestrator/prompts/validate-convoy.prompt.md +14 -11
- package/src/orchestrator/prompts/validate-prd.prompt.md +14 -11
package/src/cli/pipeline.ts
CHANGED
|
@@ -58,6 +58,125 @@ export function deriveComplexityPath(prdPath: string): string {
|
|
|
58
58
|
return prdPath + '.complexity.json'
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export function validateComplexityGroups(assessment: ComplexityAssessment): { valid: boolean; reason: string } {
|
|
62
|
+
const groups = assessment.convoy_groups
|
|
63
|
+
|
|
64
|
+
// Each group must reference at least 1 phase
|
|
65
|
+
for (const group of groups) {
|
|
66
|
+
if (group.phases.length === 0) {
|
|
67
|
+
return { valid: false, reason: `Group "${group.name}" has an empty phases array` }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Maximum group count: ≤3 for total_tasks ≤ 15, ≤4 for total_tasks > 15
|
|
72
|
+
const maxGroups = assessment.total_tasks > 15 ? 4 : 3
|
|
73
|
+
if (groups.length > maxGroups) {
|
|
74
|
+
return { valid: false, reason: `Too many groups: ${groups.length} exceeds maximum of ${maxGroups} for total_tasks=${assessment.total_tasks}` }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// No overlapping phases
|
|
78
|
+
const seenPhases = new Map<number, string>()
|
|
79
|
+
for (const group of groups) {
|
|
80
|
+
for (const phase of group.phases) {
|
|
81
|
+
if (seenPhases.has(phase)) {
|
|
82
|
+
return { valid: false, reason: `Phase ${phase} overlap: referenced by both "${seenPhases.get(phase)}" and "${group.name}"` }
|
|
83
|
+
}
|
|
84
|
+
seenPhases.set(phase, group.name)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Valid depends_on references
|
|
89
|
+
const groupNames = new Set(groups.map(g => g.name))
|
|
90
|
+
for (const group of groups) {
|
|
91
|
+
for (const dep of group.depends_on) {
|
|
92
|
+
if (!groupNames.has(dep)) {
|
|
93
|
+
return { valid: false, reason: `Group "${group.name}" depends_on "${dep}" which does not exist` }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// No dependency cycles (Kahn's algorithm)
|
|
99
|
+
const inDegree = new Map<string, number>()
|
|
100
|
+
const adjList = new Map<string, string[]>()
|
|
101
|
+
for (const group of groups) {
|
|
102
|
+
inDegree.set(group.name, 0)
|
|
103
|
+
adjList.set(group.name, [])
|
|
104
|
+
}
|
|
105
|
+
for (const group of groups) {
|
|
106
|
+
for (const dep of group.depends_on) {
|
|
107
|
+
adjList.get(dep)!.push(group.name)
|
|
108
|
+
inDegree.set(group.name, (inDegree.get(group.name) ?? 0) + 1)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const queue: string[] = []
|
|
112
|
+
for (const [name, degree] of inDegree) {
|
|
113
|
+
if (degree === 0) queue.push(name)
|
|
114
|
+
}
|
|
115
|
+
let visited = 0
|
|
116
|
+
while (queue.length > 0) {
|
|
117
|
+
const node = queue.shift()!
|
|
118
|
+
visited++
|
|
119
|
+
for (const neighbor of adjList.get(node) ?? []) {
|
|
120
|
+
const newDegree = (inDegree.get(neighbor) ?? 0) - 1
|
|
121
|
+
inDegree.set(neighbor, newDegree)
|
|
122
|
+
if (newDegree === 0) queue.push(neighbor)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (visited !== groups.length) {
|
|
126
|
+
return { valid: false, reason: 'Dependency cycle detected in convoy_groups' }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Group names must be kebab-case safe
|
|
130
|
+
const kebabCaseRegex = /^[a-z0-9]+(-[a-z0-9]+)*$/
|
|
131
|
+
for (const group of groups) {
|
|
132
|
+
if (!kebabCaseRegex.test(group.name)) {
|
|
133
|
+
return { valid: false, reason: `Group name "${group.name}" is not valid kebab-case` }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { valid: true, reason: '' }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function topologicalSortGroups(groups: ConvoyGroup[]): ConvoyGroup[] {
|
|
141
|
+
const groupMap = new Map<string, ConvoyGroup>()
|
|
142
|
+
const inDegree = new Map<string, number>()
|
|
143
|
+
const adjList = new Map<string, string[]>()
|
|
144
|
+
|
|
145
|
+
for (const group of groups) {
|
|
146
|
+
groupMap.set(group.name, group)
|
|
147
|
+
inDegree.set(group.name, 0)
|
|
148
|
+
adjList.set(group.name, [])
|
|
149
|
+
}
|
|
150
|
+
for (const group of groups) {
|
|
151
|
+
for (const dep of group.depends_on) {
|
|
152
|
+
adjList.get(dep)!.push(group.name)
|
|
153
|
+
inDegree.set(group.name, (inDegree.get(group.name) ?? 0) + 1)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const queue: string[] = []
|
|
158
|
+
for (const [name, degree] of inDegree) {
|
|
159
|
+
if (degree === 0) queue.push(name)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const sorted: ConvoyGroup[] = []
|
|
163
|
+
while (queue.length > 0) {
|
|
164
|
+
const node = queue.shift()!
|
|
165
|
+
sorted.push(groupMap.get(node)!)
|
|
166
|
+
for (const neighbor of adjList.get(node) ?? []) {
|
|
167
|
+
const newDegree = (inDegree.get(neighbor) ?? 0) - 1
|
|
168
|
+
inDegree.set(neighbor, newDegree)
|
|
169
|
+
if (newDegree === 0) queue.push(neighbor)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (sorted.length !== groups.length) {
|
|
174
|
+
throw new Error('Cycle detected in convoy_groups dependency graph')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return sorted
|
|
178
|
+
}
|
|
179
|
+
|
|
61
180
|
const HELP = `
|
|
62
181
|
opencastle start [options]
|
|
63
182
|
|
|
@@ -244,6 +363,12 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
244
363
|
console.log(c.green(` ✓ PRD written to ${relPath(prdPath)}\n`))
|
|
245
364
|
}
|
|
246
365
|
|
|
366
|
+
// Handle --dry-run when PRD was provided externally (not generated)
|
|
367
|
+
if (opts.dryRun && opts.prd) {
|
|
368
|
+
console.log(c.dim('\n [dry-run] Nothing to preview — PRD already provided via --prd. Remove --dry-run to continue.'))
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
|
|
247
372
|
// ── Step 2: Validate PRD ──────────────────────────────────────────────────
|
|
248
373
|
if (!opts.skipValidation) {
|
|
249
374
|
console.log(stepLabel(2, totalSteps, 'Validating PRD…'))
|
|
@@ -400,105 +525,115 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
|
|
|
400
525
|
|
|
401
526
|
if (complexity) {
|
|
402
527
|
if (complexity.recommended_strategy === 'chain' && complexity.convoy_groups.length > 1) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
528
|
+
// Validate complexity groups before chain generation
|
|
529
|
+
const groupValidation = validateComplexityGroups(complexity)
|
|
530
|
+
if (!groupValidation.valid) {
|
|
531
|
+
console.log(c.yellow(` ⚠ Complexity groups failed validation: ${groupValidation.reason}`))
|
|
532
|
+
console.log(c.yellow(` Falling back to single convoy strategy.\n`))
|
|
533
|
+
// Fall through to single-spec generation below
|
|
534
|
+
} else {
|
|
535
|
+
// Sort groups in dependency order
|
|
536
|
+
complexity.convoy_groups = topologicalSortGroups(complexity.convoy_groups)
|
|
412
537
|
console.log(
|
|
413
|
-
|
|
538
|
+
c.cyan(` ℹ`) +
|
|
539
|
+
` Complexity: ${complexity.complexity} | Strategy: chain | ${complexity.convoy_groups.length} convoy groups\n`
|
|
414
540
|
)
|
|
415
|
-
|
|
416
|
-
|
|
541
|
+
console.log(` Chain plan:`)
|
|
542
|
+
for (let i = 0; i < complexity.convoy_groups.length; i++) {
|
|
543
|
+
const g = complexity.convoy_groups[i]
|
|
544
|
+
const depStr =
|
|
545
|
+
g.depends_on.length > 0 ? ` → depends on: ${g.depends_on.join(', ')}` : ''
|
|
546
|
+
console.log(
|
|
547
|
+
` ${i + 1}. ${g.name.padEnd(20)} (phases: ${g.phases.join(', ')})${depStr}`
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
console.log()
|
|
417
551
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
552
|
+
const convoyDir = resolve(process.cwd(), '.opencastle', 'convoys')
|
|
553
|
+
await mkdir(convoyDir, { recursive: true })
|
|
554
|
+
|
|
555
|
+
const groupSpecPaths: string[] = []
|
|
556
|
+
|
|
557
|
+
for (let i = 0; i < complexity.convoy_groups.length; i++) {
|
|
558
|
+
const group = complexity.convoy_groups[i]
|
|
559
|
+
|
|
560
|
+
const chainGoal = [
|
|
561
|
+
complexity.original_prompt,
|
|
562
|
+
'',
|
|
563
|
+
'## Convoy Group Scope',
|
|
564
|
+
'',
|
|
565
|
+
`This is group **${i + 1} of ${complexity.convoy_groups.length}** in a convoy chain.`,
|
|
566
|
+
`Generate a convoy spec covering ONLY the phases listed below.`,
|
|
567
|
+
'',
|
|
568
|
+
`- **Group name:** ${group.name}`,
|
|
569
|
+
`- **Description:** ${group.description}`,
|
|
570
|
+
`- **Phases to include:** ${group.phases.join(', ')}`,
|
|
571
|
+
group.depends_on.length ? `- **Depends on groups:** ${group.depends_on.join(', ')}` : '',
|
|
572
|
+
].filter(Boolean).join('\n')
|
|
573
|
+
|
|
574
|
+
const prdContent = await readFile(prdPath, 'utf8')
|
|
575
|
+
const groupSpecPath = resolve(convoyDir, `${group.name}.convoy.yml`)
|
|
576
|
+
|
|
577
|
+
const { specPath: resolvedGroupSpecPath } = await generateAndValidateSpec({
|
|
578
|
+
sharedOpts,
|
|
579
|
+
goalText: chainGoal,
|
|
580
|
+
contextText: prdContent,
|
|
581
|
+
specPath: groupSpecPath,
|
|
582
|
+
skipValidation: opts.skipValidation,
|
|
583
|
+
groupName: group.name,
|
|
584
|
+
enrichment: complexity ? deriveSpecEnrichment(complexity) : undefined,
|
|
585
|
+
})
|
|
586
|
+
groupSpecPaths.push(resolvedGroupSpecPath)
|
|
587
|
+
}
|
|
454
588
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
589
|
+
// Build master pipeline spec (version 2)
|
|
590
|
+
const chainPrdContent = await readFile(prdPath, 'utf8')
|
|
591
|
+
const featureNameMatch = chainPrdContent.match(/^# (.+?)\s*(?:—|-)?\s*PRD/m)
|
|
592
|
+
const featureName = featureNameMatch
|
|
593
|
+
? featureNameMatch[1].trim().toLowerCase().replace(/[^a-z0-9]+/g, '-')
|
|
594
|
+
: 'feature'
|
|
595
|
+
|
|
596
|
+
const branchMatch = chainPrdContent.match(/`feat\/([^`]+)`/)
|
|
597
|
+
const branch = branchMatch ? `feat/${branchMatch[1]}` : `feat/${featureName}`
|
|
598
|
+
|
|
599
|
+
const masterSpec = {
|
|
600
|
+
name: featureNameMatch ? featureNameMatch[1].trim() : 'Feature Pipeline',
|
|
601
|
+
version: 2,
|
|
602
|
+
branch,
|
|
603
|
+
on_failure: 'stop',
|
|
604
|
+
depends_on_convoy: groupSpecPaths.map(p => relPath(p)),
|
|
605
|
+
}
|
|
472
606
|
|
|
473
|
-
|
|
474
|
-
|
|
607
|
+
const masterSpecPath = resolve(convoyDir, `${featureName}-pipeline.convoy.yml`)
|
|
608
|
+
await writeFile(masterSpecPath, stringify(masterSpec), 'utf8')
|
|
475
609
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
610
|
+
console.log(c.green(` ✓ Generated convoy chain:\n`))
|
|
611
|
+
for (const p of groupSpecPaths) {
|
|
612
|
+
console.log(` ${relPath(p)}`)
|
|
613
|
+
}
|
|
614
|
+
console.log(` ${relPath(masterSpecPath)} ${c.dim('(master)')}`)
|
|
615
|
+
console.log()
|
|
616
|
+
console.log(
|
|
617
|
+
` ${c.dim('Preview:')} npx opencastle run -f ${relPath(masterSpecPath)} --dry-run\n` +
|
|
618
|
+
` ${c.dim('Execute:')} npx opencastle run -f ${relPath(masterSpecPath)}\n`
|
|
619
|
+
)
|
|
486
620
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
621
|
+
try {
|
|
622
|
+
const shouldRun = await confirm('Run the convoy chain now?', true)
|
|
623
|
+
if (shouldRun) {
|
|
624
|
+
closePrompts()
|
|
625
|
+
const runModule = await import('./run.js')
|
|
626
|
+
const runArgs = ['-f', masterSpecPath]
|
|
627
|
+
if (opts.adapter) runArgs.push('-a', opts.adapter)
|
|
628
|
+
if (opts.verbose) runArgs.push('--verbose')
|
|
629
|
+
await runModule.default({ args: runArgs, pkgRoot })
|
|
630
|
+
}
|
|
631
|
+
} finally {
|
|
490
632
|
closePrompts()
|
|
491
|
-
|
|
492
|
-
const runArgs = ['-f', masterSpecPath]
|
|
493
|
-
if (opts.adapter) runArgs.push('-a', opts.adapter)
|
|
494
|
-
if (opts.verbose) runArgs.push('--verbose')
|
|
495
|
-
await runModule.default({ args: runArgs, pkgRoot })
|
|
633
|
+
await cleanupAdapters()
|
|
496
634
|
}
|
|
497
|
-
|
|
498
|
-
closePrompts()
|
|
499
|
-
await cleanupAdapters()
|
|
635
|
+
return
|
|
500
636
|
}
|
|
501
|
-
return
|
|
502
637
|
} else {
|
|
503
638
|
console.log(
|
|
504
639
|
c.cyan(` ℹ`) + ` Complexity: ${complexity.complexity} | Strategy: single\n`
|
package/src/cli/plan.ts
CHANGED
|
@@ -225,10 +225,27 @@ function extractMarkdownBody(output: string): string {
|
|
|
225
225
|
|
|
226
226
|
/**
|
|
227
227
|
* Parse a validation AI response for VALID / INVALID verdict.
|
|
228
|
-
*
|
|
228
|
+
* Prefers structured JSON output; falls back to VALID/INVALID keyword matching.
|
|
229
229
|
*/
|
|
230
230
|
function parseValidationResult(output: string): { isValid: boolean; errors: string } {
|
|
231
231
|
const trimmed = output.trim()
|
|
232
|
+
|
|
233
|
+
// Try structured JSON first (preferred format)
|
|
234
|
+
const jsonFenceMatch = trimmed.match(/```json\s*\n([\s\S]*?)```/)
|
|
235
|
+
if (jsonFenceMatch) {
|
|
236
|
+
try {
|
|
237
|
+
const parsed = JSON.parse(jsonFenceMatch[1].trim()) as { valid?: boolean; issues?: string[] }
|
|
238
|
+
if (typeof parsed.valid === 'boolean') {
|
|
239
|
+
if (parsed.valid) return { isValid: true, errors: '' }
|
|
240
|
+
const errors = Array.isArray(parsed.issues) ? parsed.issues.join('\n') : ''
|
|
241
|
+
return { isValid: false, errors }
|
|
242
|
+
}
|
|
243
|
+
} catch {
|
|
244
|
+
// JSON parse failed — fall through to regex fallback
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Fallback: regex-based parsing for backward compatibility
|
|
232
249
|
const hasInvalid = /\bINVALID\b/.test(trimmed)
|
|
233
250
|
const hasValid = /\bVALID\b/.test(trimmed)
|
|
234
251
|
if (hasValid && !hasInvalid) return { isValid: true, errors: '' }
|
|
@@ -364,9 +381,17 @@ export async function runPromptStep(opts: PromptStepOptions): Promise<PromptStep
|
|
|
364
381
|
}
|
|
365
382
|
|
|
366
383
|
if (outputType === 'json') {
|
|
367
|
-
// Extract JSON from a ```json fenced block
|
|
368
|
-
const jsonFenceMatch = rawOutput.match(/```json\s*\n([\s\S]
|
|
369
|
-
|
|
384
|
+
// Extract JSON from a ```json fenced block — fail fast if missing
|
|
385
|
+
const jsonFenceMatch = rawOutput.match(/```json\s*\n([\s\S]*?)```/)
|
|
386
|
+
if (!jsonFenceMatch) {
|
|
387
|
+
const preview = rawOutput.slice(0, 300)
|
|
388
|
+
throw new Error(
|
|
389
|
+
`Expected a fenced \`\`\`json block in the AI response but found none.\n\n` +
|
|
390
|
+
`Raw output (truncated):\n${preview}\n\n` +
|
|
391
|
+
`Tip: re-run with --verbose to see the full output.`
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
const jsonContent = jsonFenceMatch[1].trim()
|
|
370
395
|
|
|
371
396
|
const outputPath = opts.outputPath ?? null
|
|
372
397
|
if (outputPath) {
|
package/src/cli/run.ts
CHANGED
|
@@ -645,12 +645,14 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
645
645
|
template = parseFormula(formulaPath)
|
|
646
646
|
} catch (err: unknown) {
|
|
647
647
|
console.error(` ✗ ${(err as Error).message}`)
|
|
648
|
+
console.log(`\n ${c.dim('Resume:')} npx opencastle run --formula ${opts.formula}`)
|
|
648
649
|
process.exit(1)
|
|
649
650
|
}
|
|
650
651
|
|
|
651
652
|
const validation = validateTemplate(template)
|
|
652
653
|
if (!validation.valid) {
|
|
653
654
|
console.error(` ✗ Invalid formula template:\n • ${validation.errors.join('\n • ')}`)
|
|
655
|
+
console.log(`\n ${c.dim('Resume:')} npx opencastle run --formula ${opts.formula}`)
|
|
654
656
|
process.exit(1)
|
|
655
657
|
}
|
|
656
658
|
|
|
@@ -672,6 +674,7 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
672
674
|
spec = substituteVariables(template, opts.setVars)
|
|
673
675
|
} catch (err: unknown) {
|
|
674
676
|
console.error(` ✗ ${(err as Error).message}`)
|
|
677
|
+
console.log(`\n ${c.dim('Resume:')} npx opencastle run --formula ${opts.formula}`)
|
|
675
678
|
process.exit(1)
|
|
676
679
|
}
|
|
677
680
|
specText = yamlStringify(spec)
|
|
@@ -687,6 +690,7 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
687
690
|
} else {
|
|
688
691
|
console.error(` ✗ Cannot read task spec file: ${e.message}`)
|
|
689
692
|
}
|
|
693
|
+
console.log(`\n ${c.dim('Resume:')} npx opencastle run -f ${opts.file}`)
|
|
690
694
|
process.exit(1)
|
|
691
695
|
}
|
|
692
696
|
|
|
@@ -694,6 +698,7 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
694
698
|
spec = parseTaskSpecText(specText)
|
|
695
699
|
} catch (err: unknown) {
|
|
696
700
|
console.error(` ✗ ${(err as Error).message}`)
|
|
701
|
+
console.log(`\n ${c.dim('Resume:')} npx opencastle run -f ${opts.file}`)
|
|
697
702
|
process.exit(1)
|
|
698
703
|
}
|
|
699
704
|
}
|
|
@@ -745,6 +750,7 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
745
750
|
const available = await adapter.isAvailable()
|
|
746
751
|
if (!available) {
|
|
747
752
|
printAdapterError(detectionFailed, spec.adapter)
|
|
753
|
+
console.log(`\n ${c.dim('Resume:')} npx opencastle run -f ${opts.file}`)
|
|
748
754
|
process.exit(1)
|
|
749
755
|
}
|
|
750
756
|
|
|
@@ -762,22 +768,45 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
762
768
|
const { execFile: execFileCb } = await import('node:child_process')
|
|
763
769
|
const { promisify } = await import('node:util')
|
|
764
770
|
const execFile = promisify(execFileCb)
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
771
|
+
let statusOut: string
|
|
772
|
+
try {
|
|
773
|
+
const result = await execFile('git', ['status', '--porcelain'], {
|
|
774
|
+
cwd: process.cwd(),
|
|
775
|
+
})
|
|
776
|
+
statusOut = result.stdout
|
|
777
|
+
} catch {
|
|
778
|
+
console.error(` ✗ Git repository not found. A git repo is required when using the \`branch\` option.`)
|
|
779
|
+
console.error(` Run \`git init\` to initialize a repository.`)
|
|
780
|
+
console.log(`\n ${c.dim('Resume:')} npx opencastle run -f ${opts.file}`)
|
|
781
|
+
process.exit(1)
|
|
782
|
+
}
|
|
783
|
+
// Untracked files (??) don't block branch checkout — ignore them
|
|
784
|
+
const trackedChanges = statusOut
|
|
785
|
+
.split('\n')
|
|
786
|
+
.filter(line => line.trim() && !line.startsWith('??'))
|
|
787
|
+
.join('\n')
|
|
788
|
+
if (trackedChanges) {
|
|
769
789
|
console.log(`\n ${c.yellow('⚠')} Uncommitted changes detected.`)
|
|
770
790
|
const shouldStash = await confirm('Stash changes and continue?', true)
|
|
771
791
|
if (!shouldStash) {
|
|
772
792
|
console.log(' Aborted. Commit or stash your changes manually, then retry.')
|
|
793
|
+
console.log(`\n ${c.dim('Resume:')} npx opencastle run -f ${opts.file}`)
|
|
794
|
+
closePrompts()
|
|
795
|
+
process.exit(1)
|
|
796
|
+
}
|
|
797
|
+
try {
|
|
798
|
+
await execFile('git', ['stash', 'push', '-m', 'opencastle: auto-stash before pipeline'], {
|
|
799
|
+
cwd: process.cwd(),
|
|
800
|
+
})
|
|
801
|
+
pipelineDidStash = true
|
|
802
|
+
console.log(` ${c.green('✓')} Changes stashed.`)
|
|
803
|
+
} catch {
|
|
804
|
+
console.log(` ${c.yellow('⚠')} Could not stash changes automatically.`)
|
|
805
|
+
console.log(` Commit or stash your changes manually, then resume:\n`)
|
|
806
|
+
console.log(` ${c.dim('Resume:')} npx opencastle run -f ${opts.file}`)
|
|
773
807
|
closePrompts()
|
|
774
808
|
process.exit(1)
|
|
775
809
|
}
|
|
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
810
|
}
|
|
782
811
|
}
|
|
783
812
|
|
|
@@ -809,6 +838,7 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
809
838
|
} catch (err) {
|
|
810
839
|
if (err instanceof EngineAlreadyRunningError) {
|
|
811
840
|
console.error(` ✗ ${err.message}`)
|
|
841
|
+
console.log(`\n ${c.dim('Resume:')} npx opencastle run -f ${opts.file} --resume`)
|
|
812
842
|
process.exit(1)
|
|
813
843
|
}
|
|
814
844
|
throw err
|
|
@@ -829,6 +859,9 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
829
859
|
console.log(`\n ${c.dim('Results saved to .opencastle/convoy.db')}`)
|
|
830
860
|
console.log(` ${c.dim('Dashboard:')} ${pipelineDashboardResult.url}`)
|
|
831
861
|
console.log(`\n Press Ctrl+C to stop`)
|
|
862
|
+
if (pipelineResult.status !== 'done') {
|
|
863
|
+
console.log(`\n ${c.dim('Resume:')} npx opencastle run -f ${opts.file} --resume`)
|
|
864
|
+
}
|
|
832
865
|
const exitCode = pipelineResult.status !== 'done' ? 1 : 0
|
|
833
866
|
process.on('SIGINT', () => {
|
|
834
867
|
console.log('\n Dashboard stopped.\n')
|
|
@@ -836,6 +869,9 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
836
869
|
process.exit(exitCode)
|
|
837
870
|
})
|
|
838
871
|
} else {
|
|
872
|
+
if (pipelineResult.status !== 'done') {
|
|
873
|
+
console.log(`\n ${c.dim('Resume:')} npx opencastle run -f ${opts.file} --resume`)
|
|
874
|
+
}
|
|
839
875
|
process.exit(pipelineResult.status !== 'done' ? 1 : 0)
|
|
840
876
|
}
|
|
841
877
|
return
|
|
@@ -857,22 +893,45 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
857
893
|
const { execFile: execFileCb } = await import('node:child_process')
|
|
858
894
|
const { promisify } = await import('node:util')
|
|
859
895
|
const execFile = promisify(execFileCb)
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
896
|
+
let statusOut: string
|
|
897
|
+
try {
|
|
898
|
+
const result = await execFile('git', ['status', '--porcelain'], {
|
|
899
|
+
cwd: process.cwd(),
|
|
900
|
+
})
|
|
901
|
+
statusOut = result.stdout
|
|
902
|
+
} catch {
|
|
903
|
+
console.error(` ✗ Git repository not found. A git repo is required when using the \`branch\` option.`)
|
|
904
|
+
console.error(` Run \`git init\` to initialize a repository.`)
|
|
905
|
+
console.log(`\n ${c.dim('Resume:')} npx opencastle run -f ${opts.file}`)
|
|
906
|
+
process.exit(1)
|
|
907
|
+
}
|
|
908
|
+
// Untracked files (??) don't block branch checkout — ignore them
|
|
909
|
+
const trackedChanges = statusOut
|
|
910
|
+
.split('\n')
|
|
911
|
+
.filter(line => line.trim() && !line.startsWith('??'))
|
|
912
|
+
.join('\n')
|
|
913
|
+
if (trackedChanges) {
|
|
864
914
|
console.log(`\n ${c.yellow('⚠')} Uncommitted changes detected.`)
|
|
865
915
|
const shouldStash = await confirm('Stash changes and continue?', true)
|
|
866
916
|
if (!shouldStash) {
|
|
867
917
|
console.log(' Aborted. Commit or stash your changes manually, then retry.')
|
|
918
|
+
console.log(`\n ${c.dim('Resume:')} npx opencastle run -f ${opts.file}`)
|
|
919
|
+
closePrompts()
|
|
920
|
+
process.exit(1)
|
|
921
|
+
}
|
|
922
|
+
try {
|
|
923
|
+
await execFile('git', ['stash', 'push', '-m', 'opencastle: auto-stash before convoy'], {
|
|
924
|
+
cwd: process.cwd(),
|
|
925
|
+
})
|
|
926
|
+
didStash = true
|
|
927
|
+
console.log(` ${c.green('✓')} Changes stashed.`)
|
|
928
|
+
} catch {
|
|
929
|
+
console.log(` ${c.yellow('⚠')} Could not stash changes automatically.`)
|
|
930
|
+
console.log(` Commit or stash your changes manually, then resume:\n`)
|
|
931
|
+
console.log(` ${c.dim('Resume:')} npx opencastle run -f ${opts.file}`)
|
|
868
932
|
closePrompts()
|
|
869
933
|
process.exit(1)
|
|
870
934
|
}
|
|
871
|
-
await execFile('git', ['stash', 'push', '-m', 'opencastle: auto-stash before convoy'], {
|
|
872
|
-
cwd: process.cwd(),
|
|
873
|
-
})
|
|
874
|
-
didStash = true
|
|
875
|
-
console.log(` ${c.green('✓')} Changes stashed.`)
|
|
876
935
|
}
|
|
877
936
|
}
|
|
878
937
|
|
|
@@ -921,6 +980,7 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
921
980
|
} catch (err) {
|
|
922
981
|
if (err instanceof EngineAlreadyRunningError) {
|
|
923
982
|
console.error(` ✗ ${err.message}`)
|
|
983
|
+
console.log(`\n ${c.dim('Resume:')} npx opencastle run -f ${opts.file} --resume`)
|
|
924
984
|
process.exit(1)
|
|
925
985
|
}
|
|
926
986
|
throw err
|
|
@@ -941,6 +1001,9 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
941
1001
|
console.log(`\n ${c.dim('Results saved to .opencastle/convoy.db')}`)
|
|
942
1002
|
console.log(` ${c.dim('Dashboard:')} ${dashboardResult.url}`)
|
|
943
1003
|
console.log(`\n Press Ctrl+C to stop`)
|
|
1004
|
+
if (result.status !== 'done') {
|
|
1005
|
+
console.log(`\n ${c.dim('Resume:')} npx opencastle run -f ${opts.file} --resume`)
|
|
1006
|
+
}
|
|
944
1007
|
const exitCode = result.status !== 'done' ? 1 : 0
|
|
945
1008
|
process.on('SIGINT', () => {
|
|
946
1009
|
console.log('\n Dashboard stopped.\n')
|
|
@@ -948,6 +1011,9 @@ export default async function run({ args, pkgRoot }: CliContext): Promise<void>
|
|
|
948
1011
|
process.exit(exitCode)
|
|
949
1012
|
})
|
|
950
1013
|
} else {
|
|
1014
|
+
if (result.status !== 'done') {
|
|
1015
|
+
console.log(`\n ${c.dim('Resume:')} npx opencastle run -f ${opts.file} --resume`)
|
|
1016
|
+
}
|
|
951
1017
|
process.exit(result.status !== 'done' ? 1 : 0)
|
|
952
1018
|
}
|
|
953
1019
|
}
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
|
-
"hash": "
|
|
2
|
+
"hash": "dcf92252",
|
|
3
3
|
"configHash": "30f8ea04",
|
|
4
|
-
"lockfileHash": "
|
|
5
|
-
"browserHash": "
|
|
4
|
+
"lockfileHash": "afb56fd1",
|
|
5
|
+
"browserHash": "6d49cf21",
|
|
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": "53e7f26d",
|
|
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": "4d0f70f0",
|
|
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": "26bf71c8",
|
|
23
23
|
"needsInterop": true
|
|
24
24
|
}
|
|
25
25
|
},
|
|
@@ -65,20 +65,23 @@ The overall plan must make engineering sense.
|
|
|
65
65
|
|
|
66
66
|
## Output Format
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
Your entire response must be a single fenced JSON block — no text before or after:
|
|
69
69
|
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"valid": true
|
|
73
|
+
}
|
|
70
74
|
```
|
|
71
|
-
VALID
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
If any check fails:
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
INVALID
|
|
76
|
+
Or if any check fails:
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"valid": false,
|
|
81
|
+
"issues": [
|
|
82
|
+
"[Section name]: [Specific problem] — Fix: [What to change]"
|
|
83
|
+
]
|
|
84
|
+
}
|
|
82
85
|
```
|
|
83
86
|
|
|
84
|
-
List only real failures
|
|
87
|
+
List only real failures in `issues`. Do not list items that passed.
|