opencastle 0.32.8 → 0.32.10

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.
Files changed (33) hide show
  1. package/dist/cli/convoy/spec-builder.d.ts +16 -0
  2. package/dist/cli/convoy/spec-builder.d.ts.map +1 -1
  3. package/dist/cli/convoy/spec-builder.js +115 -62
  4. package/dist/cli/convoy/spec-builder.js.map +1 -1
  5. package/dist/cli/pipeline.d.ts.map +1 -1
  6. package/dist/cli/pipeline.js +279 -116
  7. package/dist/cli/pipeline.js.map +1 -1
  8. package/dist/cli/plan.d.ts.map +1 -1
  9. package/dist/cli/plan.js +19 -7
  10. package/dist/cli/plan.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/cli/convoy/spec-builder.ts +124 -58
  13. package/src/cli/pipeline.ts +324 -128
  14. package/src/cli/plan.ts +25 -7
  15. package/src/dashboard/dist/data/convoys/demo-api-v2.json +3 -3
  16. package/src/dashboard/dist/data/convoys/demo-auth-revamp.json +4 -4
  17. package/src/dashboard/dist/data/convoys/demo-dashboard-ui.json +18 -18
  18. package/src/dashboard/dist/data/convoys/demo-data-pipeline.json +9 -9
  19. package/src/dashboard/dist/data/convoys/demo-deploy-ci.json +1 -1
  20. package/src/dashboard/dist/data/convoys/demo-docs-update.json +3 -3
  21. package/src/dashboard/dist/data/convoys/demo-perf-opt.json +4 -4
  22. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  23. package/src/dashboard/public/data/convoys/demo-api-v2.json +3 -3
  24. package/src/dashboard/public/data/convoys/demo-auth-revamp.json +4 -4
  25. package/src/dashboard/public/data/convoys/demo-dashboard-ui.json +18 -18
  26. package/src/dashboard/public/data/convoys/demo-data-pipeline.json +9 -9
  27. package/src/dashboard/public/data/convoys/demo-deploy-ci.json +1 -1
  28. package/src/dashboard/public/data/convoys/demo-docs-update.json +3 -3
  29. package/src/dashboard/public/data/convoys/demo-perf-opt.json +4 -4
  30. package/src/orchestrator/prompts/fix-prd.prompt.md +4 -9
  31. package/src/orchestrator/prompts/generate-convoy.prompt.md +1 -0
  32. package/src/orchestrator/prompts/generate-prd.prompt.md +29 -0
  33. package/src/orchestrator/prompts/validate-prd.prompt.md +14 -37
@@ -1,4 +1,4 @@
1
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
1
+ import { readFile, writeFile, mkdir, unlink } from 'node:fs/promises';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
4
  import { stringify } from 'yaml';
@@ -6,7 +6,7 @@ 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
8
  import { parseYaml, validateSpec } from './run/schema.js';
9
- import { buildConvoyYaml, parseTaskPlan, parsePatches, applyPatches, deriveSpecEnrichment } from './convoy/spec-builder.js';
9
+ import { buildConvoyYaml, parseTaskPlanWithReason, parsePatches, applyPatches, deriveSpecEnrichment } from './convoy/spec-builder.js';
10
10
  function appendTaskComplexity(base, taskComplexity) {
11
11
  if (!taskComplexity?.length)
12
12
  return base;
@@ -18,6 +18,89 @@ function appendTaskComplexity(base, taskComplexity) {
18
18
  }
19
19
  return result;
20
20
  }
21
+ /**
22
+ * For chain mode, extract only the PRD sections relevant to the given phases.
23
+ * Keeps Overview, Technical Requirements, and the matching phase sections from
24
+ * Task Breakdown, while trimming the full User Stories, Implementation Scope,
25
+ * and non-matching phases to reduce context size and avoid output truncation.
26
+ */
27
+ function extractRelevantPrdSections(prdContent, phases) {
28
+ const phaseSet = new Set(phases);
29
+ const lines = prdContent.split('\n');
30
+ const result = [];
31
+ // Sections to always include (key context)
32
+ const alwaysInclude = ['overview', 'goals', 'non-goals', 'technical requirements'];
33
+ // Sections to include condensed
34
+ const condenseSection = ['user stories & acceptance criteria', 'implementation scope', 'success criteria', 'risks & open questions'];
35
+ let currentSection = '';
36
+ let inTaskBreakdown = false;
37
+ let inRelevantPhase = false;
38
+ let skipSection = false;
39
+ for (const line of lines) {
40
+ // Detect heading (## level)
41
+ const h2Match = line.match(/^## (.+)/);
42
+ if (h2Match) {
43
+ const heading = h2Match[1].trim().toLowerCase();
44
+ currentSection = heading;
45
+ inTaskBreakdown = heading === 'task breakdown';
46
+ inRelevantPhase = false;
47
+ skipSection = false;
48
+ if (alwaysInclude.some(s => heading.startsWith(s))) {
49
+ result.push(line);
50
+ continue;
51
+ }
52
+ if (inTaskBreakdown) {
53
+ result.push(line);
54
+ result.push('');
55
+ result.push(`*(Only phases ${phases.join(', ')} shown — other phases omitted for brevity)*`);
56
+ result.push('');
57
+ continue;
58
+ }
59
+ if (condenseSection.some(s => heading.startsWith(s))) {
60
+ // Include the heading but mark as condensed
61
+ result.push(line);
62
+ result.push('');
63
+ result.push('*(Condensed — see full PRD for details)*');
64
+ result.push('');
65
+ skipSection = true;
66
+ continue;
67
+ }
68
+ // # title heading — always include
69
+ result.push(line);
70
+ continue;
71
+ }
72
+ // H1 heading — always include
73
+ if (line.match(/^# /)) {
74
+ result.push(line);
75
+ continue;
76
+ }
77
+ if (skipSection)
78
+ continue;
79
+ if (inTaskBreakdown) {
80
+ // Detect phase headers like "Phase 1 —" or "Phase 2 —"
81
+ const phaseMatch = line.match(/Phase\s+(\d+)/i);
82
+ if (phaseMatch) {
83
+ const phaseNum = parseInt(phaseMatch[1], 10);
84
+ inRelevantPhase = phaseSet.has(phaseNum);
85
+ }
86
+ if (inRelevantPhase) {
87
+ result.push(line);
88
+ }
89
+ continue;
90
+ }
91
+ result.push(line);
92
+ }
93
+ return result.join('\n');
94
+ }
95
+ /**
96
+ * Filter task complexity entries to only those matching the given phases.
97
+ */
98
+ function filterTaskComplexityByPhases(taskComplexity, phases) {
99
+ if (!taskComplexity?.length)
100
+ return taskComplexity;
101
+ const phaseSet = new Set(phases);
102
+ return taskComplexity.filter(tc => phaseSet.has(tc.phase));
103
+ }
21
104
  export function parseComplexityAssessment(jsonText) {
22
105
  try {
23
106
  const parsed = JSON.parse(jsonText.trim());
@@ -266,6 +349,62 @@ function relPath(abs) {
266
349
  function stepLabel(n, total, name) {
267
350
  return c.bold(c.cyan(` [${n}/${total}] ${name}`));
268
351
  }
352
+ async function fixPrd(sharedOpts, prdPath, prdContent, initialErrors, totalSteps, adapterFlag) {
353
+ const MAX_PRD_FIX_RETRIES = 2;
354
+ let fixedPrdContent = prdContent;
355
+ let prdValidationErrors = initialErrors;
356
+ let prdFixed = false;
357
+ for (let attempt = 1; attempt <= MAX_PRD_FIX_RETRIES; attempt++) {
358
+ const label = `Fix PRD attempt ${attempt}/${MAX_PRD_FIX_RETRIES}…`;
359
+ console.log(stepLabel(3, totalSteps, label));
360
+ try {
361
+ await runPromptStep({
362
+ ...sharedOpts,
363
+ template: 'fix-prd',
364
+ goalText: fixedPrdContent,
365
+ contextText: prdValidationErrors,
366
+ outputPath: prdPath,
367
+ });
368
+ }
369
+ catch (err) {
370
+ console.error(`\n ✗ Step 3 (attempt ${attempt}) failed: ${err instanceof Error ? err.message : String(err)}`);
371
+ process.exit(1);
372
+ }
373
+ console.log(c.dim(` Re-validating after fix…`));
374
+ fixedPrdContent = await readFile(prdPath, 'utf8');
375
+ let revalidation;
376
+ try {
377
+ revalidation = await runPromptStep({
378
+ ...sharedOpts,
379
+ template: 'validate-prd',
380
+ goalText: `<!-- validation-pass: ${attempt + 1} -->\n${fixedPrdContent}`,
381
+ });
382
+ }
383
+ catch (err) {
384
+ console.error(`\n ✗ Re-validation failed: ${err instanceof Error ? err.message : String(err)}`);
385
+ process.exit(1);
386
+ }
387
+ if (revalidation.isValid) {
388
+ console.log(c.green(` ✓ PRD fixed and validated\n`));
389
+ prdFixed = true;
390
+ break;
391
+ }
392
+ prdValidationErrors = revalidation.errors ?? revalidation.rawOutput;
393
+ if (attempt < MAX_PRD_FIX_RETRIES) {
394
+ console.log(c.yellow(` ⚠ Still has issues after fix attempt ${attempt} — retrying…\n`));
395
+ console.log(c.dim(prdValidationErrors));
396
+ console.log();
397
+ }
398
+ }
399
+ if (!prdFixed) {
400
+ console.log(c.yellow(`\n ⚠ Could not fully auto-fix the PRD after ${MAX_PRD_FIX_RETRIES} attempts — continuing with best-effort PRD.\n`));
401
+ console.log(c.dim(` Remaining issues:\n`));
402
+ console.log(c.dim(prdValidationErrors));
403
+ console.log(c.dim(`\n PRD saved to ${relPath(prdPath)} with best available fixes.`) +
404
+ c.dim(`\n You can re-validate later with:\n`) +
405
+ ` opencastle start --prd ${relPath(prdPath)}${adapterFlag ? ` --adapter ${adapterFlag}` : ''}\n`);
406
+ }
407
+ }
269
408
  export default async function pipeline({ args, pkgRoot }) {
270
409
  const opts = parseArgs(args);
271
410
  if (opts.help) {
@@ -331,92 +470,16 @@ export default async function pipeline({ args, pkgRoot }) {
331
470
  console.log(c.dim('\n [dry-run] Nothing to preview — PRD already provided via --prd. Remove --dry-run to continue.'));
332
471
  return;
333
472
  }
334
- // ── Step 2: Validate PRD ──────────────────────────────────────────────────
335
- if (!opts.skipValidation) {
336
- console.log(stepLabel(2, totalSteps, 'Validating PRD…'));
337
- const prdContent = await readFile(prdPath, 'utf8');
338
- let result;
339
- try {
340
- result = await runPromptStep({
341
- ...sharedOpts,
342
- template: 'validate-prd',
343
- goalText: `<!-- validation-pass: 1 -->\n${prdContent}`,
344
- });
345
- }
346
- catch (err) {
347
- console.error(`\n ✗ Step 2 failed: ${err instanceof Error ? err.message : String(err)}`);
348
- process.exit(1);
349
- }
350
- if (!result.isValid) {
351
- let prdValidationErrors = result.errors ?? result.rawOutput;
352
- console.log(c.yellow(` ⚠ PRD has validation issues — attempting auto-fix…\n`));
353
- console.log(c.dim(prdValidationErrors));
354
- console.log();
355
- // ── Step 3: Fix PRD (up to 2 retries) ──────────────────────────────────
356
- const MAX_PRD_FIX_RETRIES = 2;
357
- let fixedPrdContent = prdContent;
358
- let prdFixed = false;
359
- for (let attempt = 1; attempt <= MAX_PRD_FIX_RETRIES; attempt++) {
360
- const label = `Fix PRD attempt ${attempt}/${MAX_PRD_FIX_RETRIES}…`;
361
- console.log(stepLabel(3, totalSteps, label));
362
- try {
363
- await runPromptStep({
364
- ...sharedOpts,
365
- template: 'fix-prd',
366
- goalText: fixedPrdContent,
367
- contextText: prdValidationErrors,
368
- outputPath: prdPath, // overwrite in place
369
- });
370
- }
371
- catch (err) {
372
- console.error(`\n ✗ Step 3 (attempt ${attempt}) failed: ${err instanceof Error ? err.message : String(err)}`);
373
- process.exit(1);
374
- }
375
- console.log(c.dim(` Re-validating after fix…`));
376
- fixedPrdContent = await readFile(prdPath, 'utf8');
377
- let revalidation;
378
- try {
379
- revalidation = await runPromptStep({
380
- ...sharedOpts,
381
- template: 'validate-prd',
382
- goalText: `<!-- validation-pass: ${attempt + 1} -->\n${fixedPrdContent}`,
383
- });
384
- }
385
- catch (err) {
386
- console.error(`\n ✗ Re-validation failed: ${err instanceof Error ? err.message : String(err)}`);
387
- process.exit(1);
388
- }
389
- if (revalidation.isValid) {
390
- console.log(c.green(` ✓ PRD fixed and validated\n`));
391
- prdFixed = true;
392
- break;
393
- }
394
- prdValidationErrors = revalidation.errors ?? revalidation.rawOutput;
395
- if (attempt < MAX_PRD_FIX_RETRIES) {
396
- console.log(c.yellow(` ⚠ Still has issues after fix attempt ${attempt} — retrying…\n`));
397
- console.log(c.dim(prdValidationErrors));
398
- console.log();
399
- }
400
- }
401
- if (!prdFixed) {
402
- console.log(c.yellow(`\n ⚠ Could not fully auto-fix the PRD after ${MAX_PRD_FIX_RETRIES} attempts — continuing with best-effort PRD.\n`));
403
- console.log(c.dim(` Remaining issues:\n`));
404
- console.log(c.dim(prdValidationErrors));
405
- console.log(c.dim(`\n PRD saved to ${relPath(prdPath)} with best available fixes.`) +
406
- c.dim(`\n You can re-validate later with:\n`) +
407
- ` opencastle start --prd ${relPath(prdPath)}${opts.adapter ? ` --adapter ${opts.adapter}` : ''}\n`);
408
- }
409
- }
410
- else {
411
- console.log(c.green(` ✓ PRD is valid\n`));
412
- }
413
- }
414
- // ── Complexity-aware strategy decision ────────────────────────────────────
473
+ // ── Steps 2 + 4: Validate PRD & Assess complexity (in parallel) ────────
474
+ // Both only read the PRD — run them concurrently to save one LLM round-trip.
475
+ // If validation fails we still use the complexity result (fix-prd patches
476
+ // issues without changing the overall structure/phases).
415
477
  const complexityStep = opts.skipValidation ? 2 : 4;
416
478
  let complexity = null;
417
479
  const complexityFilePath = opts.complexity
418
480
  ? resolve(process.cwd(), opts.complexity)
419
481
  : deriveComplexityPath(prdPath);
482
+ // Check for cached / provided complexity before launching LLM calls
420
483
  if (opts.complexity) {
421
484
  if (!existsSync(complexityFilePath)) {
422
485
  console.error(` ✗ Complexity file not found: ${opts.complexity}`);
@@ -451,24 +514,103 @@ export default async function pipeline({ args, pkgRoot }) {
451
514
  // ignore — fall through to LLM assessment
452
515
  }
453
516
  }
454
- if (!complexity) {
455
- console.log(stepLabel(complexityStep, totalSteps, 'Assessing complexity…'));
456
- try {
457
- const complexityResult = await runPromptStep({
458
- ...sharedOpts,
459
- template: 'assess-complexity',
460
- filePath: prdPath,
461
- contextText: opts.text ?? undefined,
462
- });
463
- complexity = parseComplexityAssessment(complexityResult.rawOutput);
464
- if (complexity) {
465
- await writeFile(complexityFilePath, JSON.stringify(complexity, null, 2), 'utf8');
466
- console.log(c.green(` ✓ Complexity assessment saved to ${relPath(complexityFilePath)}\n`));
517
+ const needsValidation = !opts.skipValidation;
518
+ const needsComplexity = !complexity;
519
+ if (needsValidation || needsComplexity) {
520
+ const prdContent = await readFile(prdPath, 'utf8');
521
+ // Launch validation and complexity in parallel when both are needed
522
+ if (needsValidation && needsComplexity) {
523
+ console.log(stepLabel(2, totalSteps, 'Validating PRD…'));
524
+ console.log(stepLabel(complexityStep, totalSteps, 'Assessing complexity…'));
525
+ console.log(c.dim(` (running in parallel)\n`));
526
+ const [validationResult, complexityResult] = await Promise.all([
527
+ runPromptStep({
528
+ ...sharedOpts,
529
+ template: 'validate-prd',
530
+ goalText: `<!-- validation-pass: 1 -->\n${prdContent}`,
531
+ }).catch((err) => {
532
+ console.error(`\n ✗ Validation failed: ${err instanceof Error ? err.message : String(err)}`);
533
+ return null;
534
+ }),
535
+ runPromptStep({
536
+ ...sharedOpts,
537
+ template: 'assess-complexity',
538
+ filePath: prdPath,
539
+ contextText: opts.text ?? undefined,
540
+ }).catch((err) => {
541
+ console.warn(c.yellow(` ⚠ Complexity assessment failed: ${err instanceof Error ? err.message : String(err)}`));
542
+ return null;
543
+ }),
544
+ ]);
545
+ // Process complexity result
546
+ if (complexityResult) {
547
+ complexity = parseComplexityAssessment(complexityResult.rawOutput);
548
+ if (complexity) {
549
+ await writeFile(complexityFilePath, JSON.stringify(complexity, null, 2), 'utf8');
550
+ console.log(c.green(` ✓ Complexity assessment saved to ${relPath(complexityFilePath)}`));
551
+ }
552
+ }
553
+ // Process validation result
554
+ if (!validationResult) {
555
+ process.exit(1);
556
+ }
557
+ if (!validationResult.isValid) {
558
+ let prdValidationErrors = validationResult.errors ?? validationResult.rawOutput;
559
+ console.log(c.yellow(` ⚠ PRD has validation issues — attempting auto-fix…\n`));
560
+ console.log(c.dim(prdValidationErrors));
561
+ console.log();
562
+ await fixPrd(sharedOpts, prdPath, prdContent, prdValidationErrors, totalSteps, opts.adapter);
563
+ }
564
+ else {
565
+ console.log(c.green(` ✓ PRD is valid\n`));
467
566
  }
468
567
  }
469
- catch (err) {
470
- console.warn(c.yellow(` ⚠ Complexity assessment failed: ${err instanceof Error ? err.message : String(err)}`));
471
- console.warn(c.dim(` Falling back to single convoy strategy.\n`));
568
+ else if (needsValidation) {
569
+ // Only validation needed (complexity was cached)
570
+ console.log(stepLabel(2, totalSteps, 'Validating PRD…'));
571
+ let result;
572
+ try {
573
+ result = await runPromptStep({
574
+ ...sharedOpts,
575
+ template: 'validate-prd',
576
+ goalText: `<!-- validation-pass: 1 -->\n${prdContent}`,
577
+ });
578
+ }
579
+ catch (err) {
580
+ console.error(`\n ✗ Step 2 failed: ${err instanceof Error ? err.message : String(err)}`);
581
+ process.exit(1);
582
+ }
583
+ if (!result.isValid) {
584
+ let prdValidationErrors = result.errors ?? result.rawOutput;
585
+ console.log(c.yellow(` ⚠ PRD has validation issues — attempting auto-fix…\n`));
586
+ console.log(c.dim(prdValidationErrors));
587
+ console.log();
588
+ await fixPrd(sharedOpts, prdPath, prdContent, prdValidationErrors, totalSteps, opts.adapter);
589
+ }
590
+ else {
591
+ console.log(c.green(` ✓ PRD is valid\n`));
592
+ }
593
+ }
594
+ else {
595
+ // Only complexity needed (validation skipped)
596
+ console.log(stepLabel(complexityStep, totalSteps, 'Assessing complexity…'));
597
+ try {
598
+ const complexityResult = await runPromptStep({
599
+ ...sharedOpts,
600
+ template: 'assess-complexity',
601
+ filePath: prdPath,
602
+ contextText: opts.text ?? undefined,
603
+ });
604
+ complexity = parseComplexityAssessment(complexityResult.rawOutput);
605
+ if (complexity) {
606
+ await writeFile(complexityFilePath, JSON.stringify(complexity, null, 2), 'utf8');
607
+ console.log(c.green(` ✓ Complexity assessment saved to ${relPath(complexityFilePath)}\n`));
608
+ }
609
+ }
610
+ catch (err) {
611
+ console.warn(c.yellow(` ⚠ Complexity assessment failed: ${err instanceof Error ? err.message : String(err)}`));
612
+ console.warn(c.dim(` Falling back to single convoy strategy.\n`));
613
+ }
472
614
  }
473
615
  }
474
616
  if (!complexity) {
@@ -497,9 +639,16 @@ export default async function pipeline({ args, pkgRoot }) {
497
639
  console.log();
498
640
  const convoyDir = resolve(process.cwd(), '.opencastle', 'convoys');
499
641
  await mkdir(convoyDir, { recursive: true });
642
+ // Extract feature name early for convoy naming
643
+ const chainPrdContent = await readFile(prdPath, 'utf8');
644
+ const featureNameMatch = chainPrdContent.match(/^# (.+?)\s*(?:—|-)?\s*PRD/m);
645
+ const featureName = featureNameMatch
646
+ ? featureNameMatch[1].trim().toLowerCase().replace(/[^a-z0-9]+/g, '-')
647
+ : 'feature';
500
648
  const groupSpecPaths = [];
501
649
  for (let i = 0; i < complexity.convoy_groups.length; i++) {
502
650
  const group = complexity.convoy_groups[i];
651
+ console.log(c.cyan(` [${i + 1}/${complexity.convoy_groups.length}] Generating convoy: ${group.name}`) + c.dim(` (phases: ${group.phases.join(', ')})`));
503
652
  const chainGoal = [
504
653
  complexity.original_prompt,
505
654
  '',
@@ -514,8 +663,10 @@ export default async function pipeline({ args, pkgRoot }) {
514
663
  group.depends_on.length ? `- **Depends on groups:** ${group.depends_on.join(', ')}` : '',
515
664
  ].filter(Boolean).join('\n');
516
665
  const prdContent = await readFile(prdPath, 'utf8');
517
- const contextForSpec = appendTaskComplexity(prdContent, complexity?.task_complexity);
518
- const groupSpecPath = resolve(convoyDir, `${group.name}.convoy.yml`);
666
+ const relevantPrd = extractRelevantPrdSections(prdContent, group.phases);
667
+ const relevantComplexity = filterTaskComplexityByPhases(complexity?.task_complexity, group.phases);
668
+ const contextForSpec = appendTaskComplexity(relevantPrd, relevantComplexity);
669
+ const groupSpecPath = resolve(convoyDir, `${featureName}-${group.name}.convoy.yml`);
519
670
  const { specPath: resolvedGroupSpecPath } = await generateAndValidateSpec({
520
671
  sharedOpts,
521
672
  goalText: chainGoal,
@@ -523,16 +674,12 @@ export default async function pipeline({ args, pkgRoot }) {
523
674
  specPath: groupSpecPath,
524
675
  skipValidation: opts.skipValidation,
525
676
  groupName: group.name,
677
+ featureName,
526
678
  enrichment: complexity ? deriveSpecEnrichment(complexity) : undefined,
527
679
  });
528
680
  groupSpecPaths.push(resolvedGroupSpecPath);
529
681
  }
530
682
  // Build master pipeline spec (version 2)
531
- const chainPrdContent = await readFile(prdPath, 'utf8');
532
- const featureNameMatch = chainPrdContent.match(/^# (.+?)\s*(?:—|-)?\s*PRD/m);
533
- const featureName = featureNameMatch
534
- ? featureNameMatch[1].trim().toLowerCase().replace(/[^a-z0-9]+/g, '-')
535
- : 'feature';
536
683
  const branchMatch = chainPrdContent.match(/`feat\/([^`]+)`/);
537
684
  const branch = branchMatch ? `feat/${branchMatch[1]}` : `feat/${featureName}`;
538
685
  const masterSpec = {
@@ -674,6 +821,13 @@ async function generateAndValidateSpec(params) {
674
821
  ? `Generating task plan: ${params.groupName}…`
675
822
  : 'Generating task plan…';
676
823
  console.log(c.cyan(` ${label}`));
824
+ // Temp file for debugging — kept on failure, deleted on success
825
+ const convoyDir = resolve(process.cwd(), '.opencastle', 'convoys');
826
+ await mkdir(convoyDir, { recursive: true });
827
+ const tempJsonName = params.groupName
828
+ ? `${params.featureName ? `${params.featureName}-` : ''}${params.groupName}.task-plan.json`
829
+ : `${params.featureName ?? 'task-plan'}.task-plan.json`;
830
+ const tempJsonPath = resolve(convoyDir, tempJsonName);
677
831
  let taskPlanResult;
678
832
  try {
679
833
  taskPlanResult = await runPromptStep({
@@ -687,12 +841,14 @@ async function generateAndValidateSpec(params) {
687
841
  console.error(`\n ✗ Task plan generation failed: ${err instanceof Error ? err.message : String(err)}`);
688
842
  process.exit(1);
689
843
  }
690
- let taskPlan = parseTaskPlan(taskPlanResult.rawOutput);
691
- if (!taskPlan) {
692
- console.log(c.yellow(` ⚠ Failed to parse task plan JSON — retrying generation…\n`));
693
- if (params.sharedOpts.verbose) {
694
- console.log(c.dim(taskPlanResult.rawOutput.slice(0, 500)));
695
- }
844
+ // Write raw JSON to temp file for debugging
845
+ await writeFile(tempJsonPath, taskPlanResult.rawOutput + '\n', 'utf8');
846
+ let result = parseTaskPlanWithReason(taskPlanResult.rawOutput);
847
+ if (!result.plan) {
848
+ console.log(c.yellow(` ⚠ Failed to parse task plan JSON: ${result.reason}`));
849
+ console.log(c.dim(` Output length: ${taskPlanResult.rawOutput.length} chars`));
850
+ console.log(c.dim(` Temp file: ${relPath(tempJsonPath)}`));
851
+ console.log(c.yellow(` Retrying generation…\n`));
696
852
  let retryResult;
697
853
  try {
698
854
  retryResult = await runPromptStep({
@@ -706,13 +862,20 @@ async function generateAndValidateSpec(params) {
706
862
  console.error(`\n ✗ Retry failed: ${err instanceof Error ? err.message : String(err)}`);
707
863
  process.exit(1);
708
864
  }
709
- taskPlan = parseTaskPlan(retryResult.rawOutput);
710
- if (!taskPlan) {
711
- console.error(' ✗ Failed to parse task plan JSON after retry');
712
- console.error(c.dim(retryResult.rawOutput.slice(0, 500)));
865
+ // Overwrite temp file with retry output
866
+ await writeFile(tempJsonPath, retryResult.rawOutput + '\n', 'utf8');
867
+ result = parseTaskPlanWithReason(retryResult.rawOutput);
868
+ if (!result.plan) {
869
+ console.error(` ✗ Failed to parse task plan JSON after retry: ${result.reason}`);
870
+ console.error(c.dim(` Output length: ${retryResult.rawOutput.length} chars`));
871
+ console.error(c.dim(` Raw JSON saved to ${relPath(tempJsonPath)} for inspection.`));
872
+ console.error(c.dim(`\n${retryResult.rawOutput}`));
713
873
  process.exit(1);
714
874
  }
715
875
  }
876
+ let taskPlan = result.plan;
877
+ // Success — clean up temp file
878
+ await unlink(tempJsonPath).catch(() => { });
716
879
  console.log(c.green(` ✓ Task plan generated (${taskPlan.tasks.length} tasks)`));
717
880
  // Derive spec path from plan name if not provided
718
881
  let resolvedSpecPath = params.specPath;