opencastle 0.31.0 → 0.31.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/bin/cli.mjs +4 -2
- package/dist/cli/convoy/spec-builder.d.ts +68 -0
- package/dist/cli/convoy/spec-builder.d.ts.map +1 -0
- package/dist/cli/convoy/spec-builder.js +179 -0
- package/dist/cli/convoy/spec-builder.js.map +1 -0
- package/dist/cli/convoy/spec-builder.test.d.ts +2 -0
- package/dist/cli/convoy/spec-builder.test.d.ts.map +1 -0
- package/dist/cli/convoy/spec-builder.test.js +453 -0
- package/dist/cli/convoy/spec-builder.test.js.map +1 -0
- package/dist/cli/pipeline.d.ts +1 -0
- package/dist/cli/pipeline.d.ts.map +1 -1
- package/dist/cli/pipeline.js +254 -185
- package/dist/cli/pipeline.js.map +1 -1
- package/dist/cli/pipeline.test.js +15 -1
- package/dist/cli/pipeline.test.js.map +1 -1
- package/dist/cli/plan.d.ts +1 -1
- package/dist/cli/plan.d.ts.map +1 -1
- package/dist/cli/plan.js +4 -4
- package/dist/cli/plan.js.map +1 -1
- package/dist/cli/prompt.js +2 -1
- package/dist/cli/prompt.js.map +1 -1
- package/dist/cli/run/adapters/claude.js +2 -2
- package/dist/cli/run/adapters/claude.js.map +1 -1
- package/dist/cli/run/adapters/copilot.js +2 -2
- package/dist/cli/run/adapters/copilot.js.map +1 -1
- package/dist/cli/run/adapters/cursor.js +1 -1
- package/dist/cli/run/adapters/cursor.js.map +1 -1
- package/dist/cli/run/adapters/opencode.js +1 -1
- package/dist/cli/run/adapters/opencode.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/convoy/spec-builder.test.ts +523 -0
- package/src/cli/convoy/spec-builder.ts +221 -0
- package/src/cli/pipeline.test.ts +21 -1
- package/src/cli/pipeline.ts +274 -224
- package/src/cli/plan.ts +5 -4
- package/src/cli/prompt.ts +1 -1
- package/src/cli/run/adapters/claude.ts +2 -2
- package/src/cli/run/adapters/copilot.ts +2 -2
- package/src/cli/run/adapters/cursor.ts +1 -1
- package/src/cli/run/adapters/opencode.ts +1 -1
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/orchestrator/prompts/fix-convoy.prompt.md +47 -56
- package/src/orchestrator/prompts/generate-convoy.prompt.md +85 -295
- package/src/orchestrator/prompts/validate-convoy.prompt.md +31 -42
package/dist/cli/pipeline.js
CHANGED
|
@@ -5,6 +5,8 @@ import { stringify } from 'yaml';
|
|
|
5
5
|
import { c, confirm, closePrompts } from './prompt.js';
|
|
6
6
|
import { runPromptStep, readProjectMcpServers } from './plan.js';
|
|
7
7
|
import { cleanupAdapters } from './run/adapters/index.js';
|
|
8
|
+
import { parseYaml, validateSpec } from './run/schema.js';
|
|
9
|
+
import { buildConvoyYaml, parseTaskPlan, parsePatches, applyPatches, deriveSpecEnrichment } from './convoy/spec-builder.js';
|
|
8
10
|
export function parseComplexityAssessment(jsonText) {
|
|
9
11
|
try {
|
|
10
12
|
const parsed = JSON.parse(jsonText.trim());
|
|
@@ -24,8 +26,14 @@ export function parseComplexityAssessment(jsonText) {
|
|
|
24
26
|
return null;
|
|
25
27
|
}
|
|
26
28
|
}
|
|
29
|
+
export function deriveComplexityPath(prdPath) {
|
|
30
|
+
if (prdPath.endsWith('.prd.md')) {
|
|
31
|
+
return prdPath.slice(0, -'.prd.md'.length) + '.complexity.json';
|
|
32
|
+
}
|
|
33
|
+
return prdPath + '.complexity.json';
|
|
34
|
+
}
|
|
27
35
|
const HELP = `
|
|
28
|
-
opencastle
|
|
36
|
+
opencastle start [options]
|
|
29
37
|
|
|
30
38
|
Run the full convoy generation pipeline from a feature prompt:
|
|
31
39
|
|
|
@@ -33,9 +41,9 @@ const HELP = `
|
|
|
33
41
|
Step 2 — Validate PRD (validate-prd)
|
|
34
42
|
Step 3 — Fix PRD (fix-prd, up to 2 retries if invalid)
|
|
35
43
|
Step 4 — Assess complexity (assess-complexity, determines single vs chain)
|
|
36
|
-
Step 5 — Generate
|
|
37
|
-
Step 6 — Validate convoy spec (
|
|
38
|
-
Step 7 — Fix convoy spec (
|
|
44
|
+
Step 5 — Generate task plan (generate-convoy outputs JSON, code builds YAML)
|
|
45
|
+
Step 6 — Validate convoy spec (programmatic + semantic validation)
|
|
46
|
+
Step 7 — Fix convoy spec (patch-based fixing, up to 2 retries)
|
|
39
47
|
|
|
40
48
|
Options:
|
|
41
49
|
--text, -t <text> Feature prompt text (required, unless --prd is set)
|
|
@@ -45,6 +53,7 @@ const HELP = `
|
|
|
45
53
|
--adapter, -a <name> Override agent runtime adapter
|
|
46
54
|
--verbose Show full agent output for each step
|
|
47
55
|
--dry-run Generate and print the PRD prompt only, then stop
|
|
56
|
+
--complexity <path> Skip complexity assessment — use an existing complexity file
|
|
48
57
|
--skip-validation Skip PRD and convoy validation (steps 2, 3, 6, 7)
|
|
49
58
|
--help, -h Show this help
|
|
50
59
|
`;
|
|
@@ -52,6 +61,7 @@ function parseArgs(args) {
|
|
|
52
61
|
const opts = {
|
|
53
62
|
text: null,
|
|
54
63
|
prd: null,
|
|
64
|
+
complexity: null,
|
|
55
65
|
outputPrd: null,
|
|
56
66
|
outputSpec: null,
|
|
57
67
|
adapter: null,
|
|
@@ -82,6 +92,13 @@ function parseArgs(args) {
|
|
|
82
92
|
}
|
|
83
93
|
opts.prd = args[++i];
|
|
84
94
|
break;
|
|
95
|
+
case '--complexity':
|
|
96
|
+
if (i + 1 >= args.length) {
|
|
97
|
+
console.error(' ✗ --complexity requires a path');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
opts.complexity = args[++i];
|
|
101
|
+
break;
|
|
85
102
|
case '--output-prd':
|
|
86
103
|
if (i + 1 >= args.length) {
|
|
87
104
|
console.error(' ✗ --output-prd requires a path');
|
|
@@ -159,7 +176,7 @@ export default async function pipeline({ args, pkgRoot }) {
|
|
|
159
176
|
pkgRoot,
|
|
160
177
|
...(mcpServers.length ? { mcpServers } : {}),
|
|
161
178
|
};
|
|
162
|
-
console.log(c.bold('\n opencastle
|
|
179
|
+
console.log(c.bold('\n opencastle start\n'));
|
|
163
180
|
// ── Step 1: Generate PRD ──────────────────────────────────────────────────
|
|
164
181
|
let prdPath;
|
|
165
182
|
if (opts.prd) {
|
|
@@ -262,7 +279,7 @@ export default async function pipeline({ args, pkgRoot }) {
|
|
|
262
279
|
console.log(prdValidationErrors);
|
|
263
280
|
console.log(c.dim(`\n The PRD has been saved to ${relPath(prdPath)} with the best available fixes.\n`) +
|
|
264
281
|
c.dim(` Review the remaining issues above and edit the file manually, then re-run with:\n`) +
|
|
265
|
-
` opencastle
|
|
282
|
+
` opencastle start --prd ${relPath(prdPath)}${opts.adapter ? ` --adapter ${opts.adapter}` : ''}\n`);
|
|
266
283
|
process.exit(1);
|
|
267
284
|
}
|
|
268
285
|
}
|
|
@@ -272,20 +289,63 @@ export default async function pipeline({ args, pkgRoot }) {
|
|
|
272
289
|
}
|
|
273
290
|
// ── Complexity-aware strategy decision ────────────────────────────────────
|
|
274
291
|
const complexityStep = opts.skipValidation ? 2 : 4;
|
|
275
|
-
console.log(stepLabel(complexityStep, totalSteps, 'Assessing complexity…'));
|
|
276
292
|
let complexity = null;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
293
|
+
const complexityFilePath = opts.complexity
|
|
294
|
+
? resolve(process.cwd(), opts.complexity)
|
|
295
|
+
: deriveComplexityPath(prdPath);
|
|
296
|
+
if (opts.complexity) {
|
|
297
|
+
if (!existsSync(complexityFilePath)) {
|
|
298
|
+
console.error(` ✗ Complexity file not found: ${opts.complexity}`);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
const raw = await readFile(complexityFilePath, 'utf8');
|
|
303
|
+
complexity = parseComplexityAssessment(raw);
|
|
304
|
+
if (complexity) {
|
|
305
|
+
console.log(c.dim(` [−] Using existing complexity assessment: ${relPath(complexityFilePath)}`));
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
console.error(` ✗ Invalid complexity file: ${opts.complexity}`);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
console.error(` ✗ Failed to read complexity file: ${err instanceof Error ? err.message : String(err)}`);
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
285
316
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
317
|
+
else if (existsSync(complexityFilePath)) {
|
|
318
|
+
try {
|
|
319
|
+
const raw = await readFile(complexityFilePath, 'utf8');
|
|
320
|
+
const cached = parseComplexityAssessment(raw);
|
|
321
|
+
if (cached) {
|
|
322
|
+
complexity = cached;
|
|
323
|
+
console.log(c.dim(` [−] Using existing complexity assessment: ${relPath(complexityFilePath)}`));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// ignore — fall through to LLM assessment
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (!complexity) {
|
|
331
|
+
console.log(stepLabel(complexityStep, totalSteps, 'Assessing complexity…'));
|
|
332
|
+
try {
|
|
333
|
+
const complexityResult = await runPromptStep({
|
|
334
|
+
...sharedOpts,
|
|
335
|
+
template: 'assess-complexity',
|
|
336
|
+
filePath: prdPath,
|
|
337
|
+
contextText: opts.text ?? undefined,
|
|
338
|
+
});
|
|
339
|
+
complexity = parseComplexityAssessment(complexityResult.rawOutput);
|
|
340
|
+
if (complexity) {
|
|
341
|
+
await writeFile(complexityFilePath, JSON.stringify(complexity, null, 2), 'utf8');
|
|
342
|
+
console.log(c.green(` ✓ Complexity assessment saved to ${relPath(complexityFilePath)}\n`));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
console.warn(c.yellow(` ⚠ Complexity assessment failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
347
|
+
console.warn(c.dim(` Falling back to single convoy strategy.\n`));
|
|
348
|
+
}
|
|
289
349
|
}
|
|
290
350
|
if (!complexity) {
|
|
291
351
|
console.log(c.dim(` Could not determine complexity — using single convoy strategy.\n`));
|
|
@@ -303,13 +363,9 @@ export default async function pipeline({ args, pkgRoot }) {
|
|
|
303
363
|
console.log();
|
|
304
364
|
const convoyDir = resolve(process.cwd(), '.opencastle', 'convoys');
|
|
305
365
|
await mkdir(convoyDir, { recursive: true });
|
|
306
|
-
const genBaseStep = opts.skipValidation ? 2 : 4;
|
|
307
366
|
const groupSpecPaths = [];
|
|
308
|
-
const totalGroupSteps = (opts.skipValidation ? 2 : 3) + complexity.convoy_groups.length * (opts.skipValidation ? 1 : 2);
|
|
309
367
|
for (let i = 0; i < complexity.convoy_groups.length; i++) {
|
|
310
368
|
const group = complexity.convoy_groups[i];
|
|
311
|
-
const groupStep = genBaseStep + i * (opts.skipValidation ? 1 : 2);
|
|
312
|
-
console.log(stepLabel(groupStep, totalGroupSteps, `Generating convoy spec for group: ${group.name}…`));
|
|
313
369
|
const chainGoal = [
|
|
314
370
|
complexity.original_prompt,
|
|
315
371
|
'',
|
|
@@ -325,97 +381,16 @@ export default async function pipeline({ args, pkgRoot }) {
|
|
|
325
381
|
].filter(Boolean).join('\n');
|
|
326
382
|
const prdContent = await readFile(prdPath, 'utf8');
|
|
327
383
|
const groupSpecPath = resolve(convoyDir, `${group.name}.convoy.yml`);
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
catch (err) {
|
|
339
|
-
console.error(`\n ✗ Step ${groupStep} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
340
|
-
process.exit(1);
|
|
341
|
-
}
|
|
342
|
-
const resolvedGroupSpecPath = groupResult.outputPath ?? groupSpecPath;
|
|
384
|
+
const { specPath: resolvedGroupSpecPath } = await generateAndValidateSpec({
|
|
385
|
+
sharedOpts,
|
|
386
|
+
goalText: chainGoal,
|
|
387
|
+
contextText: prdContent,
|
|
388
|
+
specPath: groupSpecPath,
|
|
389
|
+
skipValidation: opts.skipValidation,
|
|
390
|
+
groupName: group.name,
|
|
391
|
+
enrichment: complexity ? deriveSpecEnrichment(complexity) : undefined,
|
|
392
|
+
});
|
|
343
393
|
groupSpecPaths.push(resolvedGroupSpecPath);
|
|
344
|
-
console.log(c.green(` ✓ Group spec written to ${relPath(resolvedGroupSpecPath)}\n`));
|
|
345
|
-
if (!opts.skipValidation) {
|
|
346
|
-
const valStep = groupStep + 1;
|
|
347
|
-
console.log(stepLabel(valStep, totalGroupSteps, `Validating spec: ${group.name}…`));
|
|
348
|
-
let currentSpecContent = await readFile(resolvedGroupSpecPath, 'utf8');
|
|
349
|
-
let groupValidation;
|
|
350
|
-
try {
|
|
351
|
-
groupValidation = await runPromptStep({
|
|
352
|
-
...sharedOpts,
|
|
353
|
-
template: 'validate-convoy',
|
|
354
|
-
goalText: currentSpecContent,
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
catch (err) {
|
|
358
|
-
console.error(`\n ✗ Validation failed for group ${group.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
359
|
-
process.exit(1);
|
|
360
|
-
}
|
|
361
|
-
if (groupValidation.isValid) {
|
|
362
|
-
console.log(c.green(` ✓ Spec valid\n`));
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
let groupErrors = groupValidation.errors ?? groupValidation.rawOutput;
|
|
366
|
-
let groupFixed = false;
|
|
367
|
-
for (let attempt = 1; attempt <= MAX_FIX_RETRIES; attempt++) {
|
|
368
|
-
console.log(c.yellow(` ⚠ Spec has issues — fix attempt ${attempt}/${MAX_FIX_RETRIES} for ${group.name}…\n`));
|
|
369
|
-
console.log(c.dim(groupErrors));
|
|
370
|
-
console.log();
|
|
371
|
-
try {
|
|
372
|
-
await runPromptStep({
|
|
373
|
-
...sharedOpts,
|
|
374
|
-
template: 'fix-convoy',
|
|
375
|
-
goalText: currentSpecContent,
|
|
376
|
-
contextText: groupErrors,
|
|
377
|
-
outputPath: resolvedGroupSpecPath,
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
catch (err) {
|
|
381
|
-
console.error(`\n ✗ Fix failed for group ${group.name} (attempt ${attempt}): ${err instanceof Error ? err.message : String(err)}`);
|
|
382
|
-
process.exit(1);
|
|
383
|
-
}
|
|
384
|
-
console.log(c.dim(` Re-validating ${group.name} after fix…`));
|
|
385
|
-
currentSpecContent = await readFile(resolvedGroupSpecPath, 'utf8');
|
|
386
|
-
let revalidation;
|
|
387
|
-
try {
|
|
388
|
-
revalidation = await runPromptStep({
|
|
389
|
-
...sharedOpts,
|
|
390
|
-
template: 'validate-convoy',
|
|
391
|
-
goalText: currentSpecContent,
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
catch (err) {
|
|
395
|
-
console.error(`\n ✗ Re-validation failed for group ${group.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
396
|
-
process.exit(1);
|
|
397
|
-
}
|
|
398
|
-
if (revalidation.isValid) {
|
|
399
|
-
console.log(c.green(` ✓ ${group.name} fixed and validated\n`));
|
|
400
|
-
groupFixed = true;
|
|
401
|
-
break;
|
|
402
|
-
}
|
|
403
|
-
groupErrors = revalidation.errors ?? revalidation.rawOutput;
|
|
404
|
-
if (attempt < MAX_FIX_RETRIES) {
|
|
405
|
-
console.log(c.yellow(` ⚠ Still has issues after fix attempt ${attempt} — retrying…\n`));
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
if (!groupFixed) {
|
|
409
|
-
console.log(c.red(`\n ✗ Could not auto-fix convoy spec for group ${group.name} after ${MAX_FIX_RETRIES} attempts.\n`));
|
|
410
|
-
console.log(` Remaining issues:\n`);
|
|
411
|
-
console.log(groupErrors);
|
|
412
|
-
console.log(c.dim(`\n The spec has been saved to ${relPath(resolvedGroupSpecPath)} with best available fixes.\n`) +
|
|
413
|
-
c.dim(` Review the issues above and edit manually, then re-validate with:\n`) +
|
|
414
|
-
` opencastle plan --file ${relPath(resolvedGroupSpecPath)} --template validate-convoy\n`);
|
|
415
|
-
process.exit(1);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
394
|
}
|
|
420
395
|
// Build master pipeline spec (version 2)
|
|
421
396
|
const chainPrdContent = await readFile(prdPath, 'utf8');
|
|
@@ -466,87 +441,71 @@ export default async function pipeline({ args, pkgRoot }) {
|
|
|
466
441
|
}
|
|
467
442
|
}
|
|
468
443
|
// ── Generate convoy spec ──────────────────────────────────────────────────
|
|
469
|
-
const genStep = opts.skipValidation ? 3 : 5;
|
|
470
|
-
console.log(stepLabel(genStep, totalSteps, 'Generating convoy spec…'));
|
|
471
444
|
const singlePrdContent = await readFile(prdPath, 'utf8');
|
|
472
445
|
const singleGoal = complexity?.original_prompt ?? opts.text ?? '';
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if (opts.skipValidation) {
|
|
490
|
-
await printFinalSummary(prdPath, specPath, opts, pkgRoot);
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
// ── Validate convoy spec ──────────────────────────────────────────────
|
|
494
|
-
const valStep = opts.skipValidation ? 4 : 6;
|
|
495
|
-
console.log(stepLabel(valStep, totalSteps, 'Validating convoy spec…'));
|
|
496
|
-
const specContent = await readFile(specPath, 'utf8');
|
|
497
|
-
let validationErrors;
|
|
498
|
-
{
|
|
499
|
-
let result;
|
|
446
|
+
const specResult = await generateAndValidateSpec({
|
|
447
|
+
sharedOpts,
|
|
448
|
+
goalText: singleGoal,
|
|
449
|
+
contextText: singlePrdContent,
|
|
450
|
+
specPath: opts.outputSpec ? resolve(process.cwd(), opts.outputSpec) : undefined,
|
|
451
|
+
skipValidation: opts.skipValidation,
|
|
452
|
+
enrichment: complexity ? deriveSpecEnrichment(complexity) : undefined,
|
|
453
|
+
});
|
|
454
|
+
await printFinalSummary(prdPath, specResult.specPath, opts, pkgRoot);
|
|
455
|
+
}
|
|
456
|
+
async function fixViaPatch(taskPlan, errors, sharedOpts, specPath, enrichment) {
|
|
457
|
+
let currentPlan = taskPlan;
|
|
458
|
+
let currentErrors = errors;
|
|
459
|
+
for (let attempt = 1; attempt <= MAX_FIX_RETRIES; attempt++) {
|
|
460
|
+
console.log(c.dim(` Fix attempt ${attempt}/${MAX_FIX_RETRIES}…`));
|
|
461
|
+
let fixResult;
|
|
500
462
|
try {
|
|
501
|
-
|
|
463
|
+
fixResult = await runPromptStep({
|
|
502
464
|
...sharedOpts,
|
|
503
|
-
template: '
|
|
504
|
-
goalText:
|
|
465
|
+
template: 'fix-convoy',
|
|
466
|
+
goalText: JSON.stringify(currentPlan, null, 2),
|
|
467
|
+
contextText: currentErrors,
|
|
505
468
|
});
|
|
506
469
|
}
|
|
507
470
|
catch (err) {
|
|
508
|
-
console.error(`\n ✗
|
|
471
|
+
console.error(`\n ✗ Fix attempt ${attempt} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
509
472
|
process.exit(1);
|
|
510
473
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
474
|
+
const patches = parsePatches(fixResult.rawOutput);
|
|
475
|
+
if (!patches || patches.length === 0) {
|
|
476
|
+
console.warn(c.yellow(` ⚠ No valid patches returned`));
|
|
477
|
+
if (attempt >= MAX_FIX_RETRIES)
|
|
478
|
+
break;
|
|
479
|
+
continue;
|
|
515
480
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
// ── Fix convoy spec (up to 2 retries) ─────────────────────────────────
|
|
522
|
-
const fixStep = opts.skipValidation ? 4 : 7;
|
|
523
|
-
let fixedSpecContent = specContent;
|
|
524
|
-
for (let attempt = 1; attempt <= MAX_FIX_RETRIES; attempt++) {
|
|
525
|
-
const label = `Fix attempt ${attempt}/${MAX_FIX_RETRIES}…`;
|
|
526
|
-
console.log(stepLabel(fixStep, totalSteps, label));
|
|
527
|
-
let fixResult;
|
|
481
|
+
console.log(c.dim(` Applied ${patches.length} patches`));
|
|
482
|
+
currentPlan = applyPatches(currentPlan, patches);
|
|
483
|
+
// Rebuild YAML and re-validate
|
|
484
|
+
const yaml = buildConvoyYaml(currentPlan, enrichment);
|
|
528
485
|
try {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
486
|
+
const parsed = parseYaml(yaml);
|
|
487
|
+
const { valid, errors: schemaErrors } = validateSpec(parsed);
|
|
488
|
+
if (!valid) {
|
|
489
|
+
currentErrors = schemaErrors.map(e => `- Schema: ${e}`).join('\n');
|
|
490
|
+
if (attempt < MAX_FIX_RETRIES) {
|
|
491
|
+
console.log(c.yellow(` ⚠ Still has schema issues — retrying…\n`));
|
|
492
|
+
console.log(c.dim(currentErrors));
|
|
493
|
+
}
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
536
496
|
}
|
|
537
497
|
catch (err) {
|
|
538
|
-
|
|
539
|
-
|
|
498
|
+
currentErrors = `YAML error: ${err instanceof Error ? err.message : String(err)}`;
|
|
499
|
+
continue;
|
|
540
500
|
}
|
|
501
|
+
await writeFile(specPath, yaml, 'utf8');
|
|
541
502
|
console.log(c.dim(` Re-validating after fix…`));
|
|
542
|
-
// Read the newly written spec
|
|
543
|
-
fixedSpecContent = await readFile(specPath, 'utf8');
|
|
544
503
|
let revalidation;
|
|
545
504
|
try {
|
|
546
505
|
revalidation = await runPromptStep({
|
|
547
506
|
...sharedOpts,
|
|
548
507
|
template: 'validate-convoy',
|
|
549
|
-
goalText:
|
|
508
|
+
goalText: yaml,
|
|
550
509
|
});
|
|
551
510
|
}
|
|
552
511
|
catch (err) {
|
|
@@ -554,26 +513,136 @@ export default async function pipeline({ args, pkgRoot }) {
|
|
|
554
513
|
process.exit(1);
|
|
555
514
|
}
|
|
556
515
|
if (revalidation.isValid) {
|
|
557
|
-
console.log(c.green(` ✓
|
|
558
|
-
|
|
559
|
-
return;
|
|
516
|
+
console.log(c.green(` ✓ Fixed and validated\n`));
|
|
517
|
+
return currentPlan;
|
|
560
518
|
}
|
|
561
|
-
|
|
519
|
+
currentErrors = revalidation.errors ?? revalidation.rawOutput;
|
|
562
520
|
if (attempt < MAX_FIX_RETRIES) {
|
|
563
|
-
console.log(c.yellow(` ⚠ Still has issues
|
|
564
|
-
console.log(c.dim(
|
|
565
|
-
console.log();
|
|
521
|
+
console.log(c.yellow(` ⚠ Still has issues — retrying…\n`));
|
|
522
|
+
console.log(c.dim(currentErrors));
|
|
566
523
|
}
|
|
567
524
|
}
|
|
568
|
-
//
|
|
569
|
-
|
|
525
|
+
// Exhausted — write best effort and exit
|
|
526
|
+
await writeFile(specPath, buildConvoyYaml(currentPlan, enrichment), 'utf8');
|
|
527
|
+
console.log(c.red(`\n ✗ Could not auto-fix after ${MAX_FIX_RETRIES} attempts.\n`));
|
|
570
528
|
console.log(` Remaining issues:\n`);
|
|
571
|
-
console.log(
|
|
572
|
-
console.log(c.dim(`\n
|
|
573
|
-
c.dim(`
|
|
529
|
+
console.log(currentErrors);
|
|
530
|
+
console.log(c.dim(`\n Spec saved to ${relPath(specPath)} with best available fixes.\n`) +
|
|
531
|
+
c.dim(` Edit manually, then re-validate with:\n`) +
|
|
574
532
|
` opencastle plan --file ${relPath(specPath)} --template validate-convoy\n`);
|
|
575
533
|
process.exit(1);
|
|
576
534
|
}
|
|
535
|
+
async function generateAndValidateSpec(params) {
|
|
536
|
+
const label = params.groupName
|
|
537
|
+
? `Generating task plan: ${params.groupName}…`
|
|
538
|
+
: 'Generating task plan…';
|
|
539
|
+
console.log(c.cyan(` ${label}`));
|
|
540
|
+
let taskPlanResult;
|
|
541
|
+
try {
|
|
542
|
+
taskPlanResult = await runPromptStep({
|
|
543
|
+
...params.sharedOpts,
|
|
544
|
+
template: 'generate-convoy',
|
|
545
|
+
goalText: params.goalText,
|
|
546
|
+
contextText: params.contextText,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
catch (err) {
|
|
550
|
+
console.error(`\n ✗ Task plan generation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
let taskPlan = parseTaskPlan(taskPlanResult.rawOutput);
|
|
554
|
+
if (!taskPlan) {
|
|
555
|
+
console.log(c.yellow(` ⚠ Failed to parse task plan JSON — retrying generation…\n`));
|
|
556
|
+
if (params.sharedOpts.verbose) {
|
|
557
|
+
console.log(c.dim(taskPlanResult.rawOutput.slice(0, 500)));
|
|
558
|
+
}
|
|
559
|
+
let retryResult;
|
|
560
|
+
try {
|
|
561
|
+
retryResult = await runPromptStep({
|
|
562
|
+
...params.sharedOpts,
|
|
563
|
+
template: 'generate-convoy',
|
|
564
|
+
goalText: params.goalText,
|
|
565
|
+
contextText: params.contextText,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
console.error(`\n ✗ Retry failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
570
|
+
process.exit(1);
|
|
571
|
+
}
|
|
572
|
+
taskPlan = parseTaskPlan(retryResult.rawOutput);
|
|
573
|
+
if (!taskPlan) {
|
|
574
|
+
console.error(' ✗ Failed to parse task plan JSON after retry');
|
|
575
|
+
console.error(c.dim(retryResult.rawOutput.slice(0, 500)));
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
console.log(c.green(` ✓ Task plan generated (${taskPlan.tasks.length} tasks)`));
|
|
580
|
+
// Derive spec path from plan name if not provided
|
|
581
|
+
let resolvedSpecPath = params.specPath;
|
|
582
|
+
if (!resolvedSpecPath) {
|
|
583
|
+
const convoyDir = resolve(process.cwd(), '.opencastle', 'convoys');
|
|
584
|
+
await mkdir(convoyDir, { recursive: true });
|
|
585
|
+
const kebab = taskPlan.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
586
|
+
resolvedSpecPath = resolve(convoyDir, `${kebab}.convoy.yml`);
|
|
587
|
+
}
|
|
588
|
+
// Build YAML from JSON task plan
|
|
589
|
+
let yamlContent = buildConvoyYaml(taskPlan, params.enrichment);
|
|
590
|
+
await mkdir(resolve(resolvedSpecPath, '..'), { recursive: true });
|
|
591
|
+
await writeFile(resolvedSpecPath, yamlContent, 'utf8');
|
|
592
|
+
console.log(c.green(` ✓ Convoy spec written to ${relPath(resolvedSpecPath)}\n`));
|
|
593
|
+
if (!params.skipValidation) {
|
|
594
|
+
// Programmatic validation first
|
|
595
|
+
try {
|
|
596
|
+
const parsed = parseYaml(yamlContent);
|
|
597
|
+
const { valid, errors: schemaErrors } = validateSpec(parsed);
|
|
598
|
+
if (!valid) {
|
|
599
|
+
console.log(c.yellow(` ⚠ Schema validation issues — auto-fixing…\n`));
|
|
600
|
+
const errorText = schemaErrors.map(e => `- Schema: ${e}`).join('\n');
|
|
601
|
+
console.log(c.dim(errorText));
|
|
602
|
+
console.log();
|
|
603
|
+
taskPlan = await fixViaPatch(taskPlan, errorText, params.sharedOpts, resolvedSpecPath, params.enrichment);
|
|
604
|
+
yamlContent = buildConvoyYaml(taskPlan, params.enrichment);
|
|
605
|
+
await writeFile(resolvedSpecPath, yamlContent, 'utf8');
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
console.log(c.dim(` ✓ Schema validation passed`));
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
catch (err) {
|
|
612
|
+
console.warn(c.yellow(` ⚠ YAML warning: ${err instanceof Error ? err.message : String(err)}`));
|
|
613
|
+
}
|
|
614
|
+
// Semantic validation (LLM)
|
|
615
|
+
const valLabel = params.groupName
|
|
616
|
+
? `Validating spec: ${params.groupName}…`
|
|
617
|
+
: 'Validating convoy spec…';
|
|
618
|
+
console.log(c.cyan(` ${valLabel}`));
|
|
619
|
+
let semanticResult;
|
|
620
|
+
try {
|
|
621
|
+
semanticResult = await runPromptStep({
|
|
622
|
+
...params.sharedOpts,
|
|
623
|
+
template: 'validate-convoy',
|
|
624
|
+
goalText: await readFile(resolvedSpecPath, 'utf8'),
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
catch (err) {
|
|
628
|
+
console.error(`\n ✗ Semantic validation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
if (semanticResult.isValid) {
|
|
632
|
+
console.log(c.green(` ✓ Spec is valid\n`));
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
const semanticErrors = semanticResult.errors ?? semanticResult.rawOutput;
|
|
636
|
+
console.log(c.yellow(` ⚠ Semantic issues — auto-fixing…\n`));
|
|
637
|
+
console.log(c.dim(semanticErrors));
|
|
638
|
+
console.log();
|
|
639
|
+
taskPlan = await fixViaPatch(taskPlan, semanticErrors, params.sharedOpts, resolvedSpecPath, params.enrichment);
|
|
640
|
+
yamlContent = buildConvoyYaml(taskPlan, params.enrichment);
|
|
641
|
+
await writeFile(resolvedSpecPath, yamlContent, 'utf8');
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return { specPath: resolvedSpecPath, taskPlan };
|
|
645
|
+
}
|
|
577
646
|
async function printFinalSummary(prdPath, specPath, opts, pkgRoot) {
|
|
578
647
|
const prd = relPath(prdPath);
|
|
579
648
|
const spec = relPath(specPath);
|