opencastle 0.32.7 → 0.32.9

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 +5 -0
  2. package/dist/cli/convoy/spec-builder.d.ts.map +1 -1
  3. package/dist/cli/convoy/spec-builder.js +39 -0
  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 +252 -100
  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 +5 -3
  10. package/dist/cli/plan.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/cli/convoy/spec-builder.ts +38 -0
  13. package/src/cli/pipeline.ts +289 -111
  14. package/src/cli/plan.ts +5 -3
  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 +12 -12
  18. package/src/dashboard/dist/data/convoys/demo-data-pipeline.json +3 -3
  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 +10 -10
  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 +12 -12
  26. package/src/dashboard/public/data/convoys/demo-data-pipeline.json +3 -3
  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 +10 -10
  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,6 +1,6 @@
1
1
  import { readFile, writeFile, mkdir } from 'node:fs/promises'
2
2
  import { existsSync } from 'node:fs'
3
- import { resolve, dirname } from 'node:path'
3
+ import { resolve } from 'node:path'
4
4
  import { stringify } from 'yaml'
5
5
  import { c, confirm, closePrompts } from './prompt.js'
6
6
  import { runPromptStep, readProjectMcpServers } from './plan.js'
@@ -8,7 +8,7 @@ import type { PromptStepOptions } from './plan.js'
8
8
  import { cleanupAdapters } from './run/adapters/index.js'
9
9
  import type { CliContext } from './types.js'
10
10
  import { parseYaml, validateSpec } from './run/schema.js'
11
- import { buildConvoyYaml, parseTaskPlan, parsePatches, applyPatches, deriveSpecEnrichment } from './convoy/spec-builder.js'
11
+ import { buildConvoyYaml, parseTaskPlan, parsePatches, applyPatches, deriveSpecEnrichment, detectJsonTruncation } from './convoy/spec-builder.js'
12
12
  import type { TaskPlan, SpecEnrichment } from './convoy/spec-builder.js'
13
13
 
14
14
  export interface ConvoyGroup {
@@ -29,6 +29,104 @@ function appendTaskComplexity(base: string, taskComplexity: ComplexityAssessment
29
29
  return result
30
30
  }
31
31
 
32
+ /**
33
+ * For chain mode, extract only the PRD sections relevant to the given phases.
34
+ * Keeps Overview, Technical Requirements, and the matching phase sections from
35
+ * Task Breakdown, while trimming the full User Stories, Implementation Scope,
36
+ * and non-matching phases to reduce context size and avoid output truncation.
37
+ */
38
+ function extractRelevantPrdSections(prdContent: string, phases: number[]): string {
39
+ const phaseSet = new Set(phases)
40
+ const lines = prdContent.split('\n')
41
+ const result: string[] = []
42
+
43
+ // Sections to always include (key context)
44
+ const alwaysInclude = ['overview', 'goals', 'non-goals', 'technical requirements']
45
+ // Sections to include condensed
46
+ const condenseSection = ['user stories & acceptance criteria', 'implementation scope', 'success criteria', 'risks & open questions']
47
+
48
+ let currentSection = ''
49
+ let inTaskBreakdown = false
50
+ let inRelevantPhase = false
51
+ let skipSection = false
52
+
53
+ for (const line of lines) {
54
+ // Detect heading (## level)
55
+ const h2Match = line.match(/^## (.+)/)
56
+ if (h2Match) {
57
+ const heading = h2Match[1].trim().toLowerCase()
58
+ currentSection = heading
59
+ inTaskBreakdown = heading === 'task breakdown'
60
+ inRelevantPhase = false
61
+ skipSection = false
62
+
63
+ if (alwaysInclude.some(s => heading.startsWith(s))) {
64
+ result.push(line)
65
+ continue
66
+ }
67
+
68
+ if (inTaskBreakdown) {
69
+ result.push(line)
70
+ result.push('')
71
+ result.push(`*(Only phases ${phases.join(', ')} shown — other phases omitted for brevity)*`)
72
+ result.push('')
73
+ continue
74
+ }
75
+
76
+ if (condenseSection.some(s => heading.startsWith(s))) {
77
+ // Include the heading but mark as condensed
78
+ result.push(line)
79
+ result.push('')
80
+ result.push('*(Condensed — see full PRD for details)*')
81
+ result.push('')
82
+ skipSection = true
83
+ continue
84
+ }
85
+
86
+ // # title heading — always include
87
+ result.push(line)
88
+ continue
89
+ }
90
+
91
+ // H1 heading — always include
92
+ if (line.match(/^# /)) {
93
+ result.push(line)
94
+ continue
95
+ }
96
+
97
+ if (skipSection) continue
98
+
99
+ if (inTaskBreakdown) {
100
+ // Detect phase headers like "Phase 1 —" or "Phase 2 —"
101
+ const phaseMatch = line.match(/Phase\s+(\d+)/i)
102
+ if (phaseMatch) {
103
+ const phaseNum = parseInt(phaseMatch[1], 10)
104
+ inRelevantPhase = phaseSet.has(phaseNum)
105
+ }
106
+ if (inRelevantPhase) {
107
+ result.push(line)
108
+ }
109
+ continue
110
+ }
111
+
112
+ result.push(line)
113
+ }
114
+
115
+ return result.join('\n')
116
+ }
117
+
118
+ /**
119
+ * Filter task complexity entries to only those matching the given phases.
120
+ */
121
+ function filterTaskComplexityByPhases(
122
+ taskComplexity: ComplexityAssessment['task_complexity'],
123
+ phases: number[],
124
+ ): ComplexityAssessment['task_complexity'] {
125
+ if (!taskComplexity?.length) return taskComplexity
126
+ const phaseSet = new Set(phases)
127
+ return taskComplexity.filter(tc => phaseSet.has(tc.phase))
128
+ }
129
+
32
130
  export interface ComplexityAssessment {
33
131
  original_prompt: string
34
132
  total_tasks: number
@@ -310,6 +408,79 @@ function stepLabel(n: number, total: number, name: string): string {
310
408
  return c.bold(c.cyan(` [${n}/${total}] ${name}`))
311
409
  }
312
410
 
411
+ async function fixPrd(
412
+ sharedOpts: Omit<PromptStepOptions, 'template' | 'goalText'>,
413
+ prdPath: string,
414
+ prdContent: string,
415
+ initialErrors: string,
416
+ totalSteps: number,
417
+ adapterFlag: string | null,
418
+ ): Promise<void> {
419
+ const MAX_PRD_FIX_RETRIES = 2
420
+ let fixedPrdContent = prdContent
421
+ let prdValidationErrors = initialErrors
422
+ let prdFixed = false
423
+
424
+ for (let attempt = 1; attempt <= MAX_PRD_FIX_RETRIES; attempt++) {
425
+ const label = `Fix PRD attempt ${attempt}/${MAX_PRD_FIX_RETRIES}…`
426
+ console.log(stepLabel(3, totalSteps, label))
427
+
428
+ try {
429
+ await runPromptStep({
430
+ ...sharedOpts,
431
+ template: 'fix-prd',
432
+ goalText: fixedPrdContent,
433
+ contextText: prdValidationErrors,
434
+ outputPath: prdPath,
435
+ })
436
+ } catch (err) {
437
+ console.error(`\n ✗ Step 3 (attempt ${attempt}) failed: ${err instanceof Error ? err.message : String(err)}`)
438
+ process.exit(1)
439
+ }
440
+
441
+ console.log(c.dim(` Re-validating after fix…`))
442
+
443
+ fixedPrdContent = await readFile(prdPath, 'utf8')
444
+
445
+ let revalidation
446
+ try {
447
+ revalidation = await runPromptStep({
448
+ ...sharedOpts,
449
+ template: 'validate-prd',
450
+ goalText: `<!-- validation-pass: ${attempt + 1} -->\n${fixedPrdContent}`,
451
+ })
452
+ } catch (err) {
453
+ console.error(`\n ✗ Re-validation failed: ${err instanceof Error ? err.message : String(err)}`)
454
+ process.exit(1)
455
+ }
456
+
457
+ if (revalidation.isValid) {
458
+ console.log(c.green(` ✓ PRD fixed and validated\n`))
459
+ prdFixed = true
460
+ break
461
+ }
462
+
463
+ prdValidationErrors = revalidation.errors ?? revalidation.rawOutput
464
+
465
+ if (attempt < MAX_PRD_FIX_RETRIES) {
466
+ console.log(c.yellow(` ⚠ Still has issues after fix attempt ${attempt} — retrying…\n`))
467
+ console.log(c.dim(prdValidationErrors))
468
+ console.log()
469
+ }
470
+ }
471
+
472
+ if (!prdFixed) {
473
+ console.log(c.yellow(`\n ⚠ Could not fully auto-fix the PRD after ${MAX_PRD_FIX_RETRIES} attempts — continuing with best-effort PRD.\n`))
474
+ console.log(c.dim(` Remaining issues:\n`))
475
+ console.log(c.dim(prdValidationErrors))
476
+ console.log(
477
+ c.dim(`\n PRD saved to ${relPath(prdPath)} with best available fixes.`) +
478
+ c.dim(`\n You can re-validate later with:\n`) +
479
+ ` opencastle start --prd ${relPath(prdPath)}${adapterFlag ? ` --adapter ${adapterFlag}` : ''}\n`
480
+ )
481
+ }
482
+ }
483
+
313
484
  export default async function pipeline({ args, pkgRoot }: CliContext): Promise<void> {
314
485
  const opts = parseArgs(args)
315
486
 
@@ -386,98 +557,11 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
386
557
  return
387
558
  }
388
559
 
389
- // ── Step 2: Validate PRD ──────────────────────────────────────────────────
390
- if (!opts.skipValidation) {
391
- console.log(stepLabel(2, totalSteps, 'Validating PRD…'))
392
-
393
- const prdContent = await readFile(prdPath, 'utf8')
394
- let result
395
- try {
396
- result = await runPromptStep({
397
- ...sharedOpts,
398
- template: 'validate-prd',
399
- goalText: `<!-- validation-pass: 1 -->\n${prdContent}`,
400
- })
401
- } catch (err) {
402
- console.error(`\n ✗ Step 2 failed: ${err instanceof Error ? err.message : String(err)}`)
403
- process.exit(1)
404
- }
405
-
406
- if (!result.isValid) {
407
- let prdValidationErrors = result.errors ?? result.rawOutput
408
- console.log(c.yellow(` ⚠ PRD has validation issues — attempting auto-fix…\n`))
409
- console.log(c.dim(prdValidationErrors))
410
- console.log()
411
-
412
- // ── Step 3: Fix PRD (up to 2 retries) ──────────────────────────────────
413
- const MAX_PRD_FIX_RETRIES = 2
414
- let fixedPrdContent = prdContent
415
- let prdFixed = false
416
-
417
- for (let attempt = 1; attempt <= MAX_PRD_FIX_RETRIES; attempt++) {
418
- const label = `Fix PRD attempt ${attempt}/${MAX_PRD_FIX_RETRIES}…`
419
- console.log(stepLabel(3, totalSteps, label))
420
-
421
- try {
422
- await runPromptStep({
423
- ...sharedOpts,
424
- template: 'fix-prd',
425
- goalText: fixedPrdContent,
426
- contextText: prdValidationErrors,
427
- outputPath: prdPath, // overwrite in place
428
- })
429
- } catch (err) {
430
- console.error(`\n ✗ Step 3 (attempt ${attempt}) failed: ${err instanceof Error ? err.message : String(err)}`)
431
- process.exit(1)
432
- }
433
-
434
- console.log(c.dim(` Re-validating after fix…`))
435
-
436
- fixedPrdContent = await readFile(prdPath, 'utf8')
437
-
438
- let revalidation
439
- try {
440
- revalidation = await runPromptStep({
441
- ...sharedOpts,
442
- template: 'validate-prd',
443
- goalText: `<!-- validation-pass: ${attempt + 1} -->\n${fixedPrdContent}`,
444
- })
445
- } catch (err) {
446
- console.error(`\n ✗ Re-validation failed: ${err instanceof Error ? err.message : String(err)}`)
447
- process.exit(1)
448
- }
449
-
450
- if (revalidation.isValid) {
451
- console.log(c.green(` ✓ PRD fixed and validated\n`))
452
- prdFixed = true
453
- break
454
- }
455
-
456
- prdValidationErrors = revalidation.errors ?? revalidation.rawOutput
457
-
458
- if (attempt < MAX_PRD_FIX_RETRIES) {
459
- console.log(c.yellow(` ⚠ Still has issues after fix attempt ${attempt} — retrying…\n`))
460
- console.log(c.dim(prdValidationErrors))
461
- console.log()
462
- }
463
- }
464
-
465
- if (!prdFixed) {
466
- console.log(c.yellow(`\n ⚠ Could not fully auto-fix the PRD after ${MAX_PRD_FIX_RETRIES} attempts — continuing with best-effort PRD.\n`))
467
- console.log(c.dim(` Remaining issues:\n`))
468
- console.log(c.dim(prdValidationErrors))
469
- console.log(
470
- c.dim(`\n PRD saved to ${relPath(prdPath)} with best available fixes.`) +
471
- c.dim(`\n You can re-validate later with:\n`) +
472
- ` opencastle start --prd ${relPath(prdPath)}${opts.adapter ? ` --adapter ${opts.adapter}` : ''}\n`
473
- )
474
- }
475
- } else {
476
- console.log(c.green(` ✓ PRD is valid\n`))
477
- }
478
- }
560
+ // ── Steps 2 + 4: Validate PRD & Assess complexity (in parallel) ────────
561
+ // Both only read the PRD — run them concurrently to save one LLM round-trip.
562
+ // If validation fails we still use the complexity result (fix-prd patches
563
+ // issues without changing the overall structure/phases).
479
564
 
480
- // ── Complexity-aware strategy decision ────────────────────────────────────
481
565
  const complexityStep = opts.skipValidation ? 2 : 4
482
566
 
483
567
  let complexity: ComplexityAssessment | null = null
@@ -485,6 +569,7 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
485
569
  ? resolve(process.cwd(), opts.complexity)
486
570
  : deriveComplexityPath(prdPath)
487
571
 
572
+ // Check for cached / provided complexity before launching LLM calls
488
573
  if (opts.complexity) {
489
574
  if (!existsSync(complexityFilePath)) {
490
575
  console.error(` ✗ Complexity file not found: ${opts.complexity}`)
@@ -516,23 +601,107 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
516
601
  }
517
602
  }
518
603
 
519
- if (!complexity) {
520
- console.log(stepLabel(complexityStep, totalSteps, 'Assessing complexity…'))
521
- try {
522
- const complexityResult = await runPromptStep({
523
- ...sharedOpts,
524
- template: 'assess-complexity',
525
- filePath: prdPath,
526
- contextText: opts.text ?? undefined,
527
- })
528
- complexity = parseComplexityAssessment(complexityResult.rawOutput)
529
- if (complexity) {
530
- await writeFile(complexityFilePath, JSON.stringify(complexity, null, 2), 'utf8')
531
- console.log(c.green(` ✓ Complexity assessment saved to ${relPath(complexityFilePath)}\n`))
604
+ const needsValidation = !opts.skipValidation
605
+ const needsComplexity = !complexity
606
+
607
+ if (needsValidation || needsComplexity) {
608
+ const prdContent = await readFile(prdPath, 'utf8')
609
+
610
+ // Launch validation and complexity in parallel when both are needed
611
+ if (needsValidation && needsComplexity) {
612
+ console.log(stepLabel(2, totalSteps, 'Validating PRD…'))
613
+ console.log(stepLabel(complexityStep, totalSteps, 'Assessing complexity…'))
614
+ console.log(c.dim(` (running in parallel)\n`))
615
+
616
+ const [validationResult, complexityResult] = await Promise.all([
617
+ runPromptStep({
618
+ ...sharedOpts,
619
+ template: 'validate-prd',
620
+ goalText: `<!-- validation-pass: 1 -->\n${prdContent}`,
621
+ }).catch((err) => {
622
+ console.error(`\n ✗ Validation failed: ${err instanceof Error ? err.message : String(err)}`)
623
+ return null
624
+ }),
625
+ runPromptStep({
626
+ ...sharedOpts,
627
+ template: 'assess-complexity',
628
+ filePath: prdPath,
629
+ contextText: opts.text ?? undefined,
630
+ }).catch((err) => {
631
+ console.warn(c.yellow(` ⚠ Complexity assessment failed: ${err instanceof Error ? err.message : String(err)}`))
632
+ return null
633
+ }),
634
+ ])
635
+
636
+ // Process complexity result
637
+ if (complexityResult) {
638
+ complexity = parseComplexityAssessment(complexityResult.rawOutput)
639
+ if (complexity) {
640
+ await writeFile(complexityFilePath, JSON.stringify(complexity, null, 2), 'utf8')
641
+ console.log(c.green(` ✓ Complexity assessment saved to ${relPath(complexityFilePath)}`))
642
+ }
643
+ }
644
+
645
+ // Process validation result
646
+ if (!validationResult) {
647
+ process.exit(1)
648
+ }
649
+
650
+ if (!validationResult.isValid) {
651
+ let prdValidationErrors = validationResult.errors ?? validationResult.rawOutput
652
+ console.log(c.yellow(` ⚠ PRD has validation issues — attempting auto-fix…\n`))
653
+ console.log(c.dim(prdValidationErrors))
654
+ console.log()
655
+
656
+ await fixPrd(sharedOpts, prdPath, prdContent, prdValidationErrors, totalSteps, opts.adapter)
657
+ } else {
658
+ console.log(c.green(` ✓ PRD is valid\n`))
659
+ }
660
+ } else if (needsValidation) {
661
+ // Only validation needed (complexity was cached)
662
+ console.log(stepLabel(2, totalSteps, 'Validating PRD…'))
663
+
664
+ let result
665
+ try {
666
+ result = await runPromptStep({
667
+ ...sharedOpts,
668
+ template: 'validate-prd',
669
+ goalText: `<!-- validation-pass: 1 -->\n${prdContent}`,
670
+ })
671
+ } catch (err) {
672
+ console.error(`\n ✗ Step 2 failed: ${err instanceof Error ? err.message : String(err)}`)
673
+ process.exit(1)
674
+ }
675
+
676
+ if (!result.isValid) {
677
+ let prdValidationErrors = result.errors ?? result.rawOutput
678
+ console.log(c.yellow(` ⚠ PRD has validation issues — attempting auto-fix…\n`))
679
+ console.log(c.dim(prdValidationErrors))
680
+ console.log()
681
+
682
+ await fixPrd(sharedOpts, prdPath, prdContent, prdValidationErrors, totalSteps, opts.adapter)
683
+ } else {
684
+ console.log(c.green(` ✓ PRD is valid\n`))
685
+ }
686
+ } else {
687
+ // Only complexity needed (validation skipped)
688
+ console.log(stepLabel(complexityStep, totalSteps, 'Assessing complexity…'))
689
+ try {
690
+ const complexityResult = await runPromptStep({
691
+ ...sharedOpts,
692
+ template: 'assess-complexity',
693
+ filePath: prdPath,
694
+ contextText: opts.text ?? undefined,
695
+ })
696
+ complexity = parseComplexityAssessment(complexityResult.rawOutput)
697
+ if (complexity) {
698
+ await writeFile(complexityFilePath, JSON.stringify(complexity, null, 2), 'utf8')
699
+ console.log(c.green(` ✓ Complexity assessment saved to ${relPath(complexityFilePath)}\n`))
700
+ }
701
+ } catch (err) {
702
+ console.warn(c.yellow(` ⚠ Complexity assessment failed: ${err instanceof Error ? err.message : String(err)}`))
703
+ console.warn(c.dim(` Falling back to single convoy strategy.\n`))
532
704
  }
533
- } catch (err) {
534
- console.warn(c.yellow(` ⚠ Complexity assessment failed: ${err instanceof Error ? err.message : String(err)}`))
535
- console.warn(c.dim(` Falling back to single convoy strategy.\n`))
536
705
  }
537
706
  }
538
707
 
@@ -589,7 +758,9 @@ export default async function pipeline({ args, pkgRoot }: CliContext): Promise<v
589
758
  ].filter(Boolean).join('\n')
590
759
 
591
760
  const prdContent = await readFile(prdPath, 'utf8')
592
- const contextForSpec = appendTaskComplexity(prdContent, complexity?.task_complexity)
761
+ const relevantPrd = extractRelevantPrdSections(prdContent, group.phases)
762
+ const relevantComplexity = filterTaskComplexityByPhases(complexity?.task_complexity, group.phases)
763
+ const contextForSpec = appendTaskComplexity(relevantPrd, relevantComplexity)
593
764
  const groupSpecPath = resolve(convoyDir, `${group.name}.convoy.yml`)
594
765
 
595
766
  const { specPath: resolvedGroupSpecPath } = await generateAndValidateSpec({
@@ -819,7 +990,14 @@ async function generateAndValidateSpec(params: {
819
990
 
820
991
  taskPlan = parseTaskPlan(retryResult.rawOutput)
821
992
  if (!taskPlan) {
822
- console.error(' ✗ Failed to parse task plan JSON after retry')
993
+ const truncation = detectJsonTruncation(retryResult.rawOutput)
994
+ if (truncation) {
995
+ console.error(` ✗ Task plan JSON was truncated: ${truncation}`)
996
+ console.error(c.dim(`\n The AI model ran out of output tokens before finishing the JSON.`))
997
+ console.error(c.dim(` Try reducing the PRD size or re-running with a model that supports longer output.`))
998
+ } else {
999
+ console.error(' ✗ Failed to parse task plan JSON after retry')
1000
+ }
823
1001
  console.error(c.dim(retryResult.rawOutput.slice(0, 500)))
824
1002
  process.exit(1)
825
1003
  }
package/src/cli/plan.ts CHANGED
@@ -152,8 +152,8 @@ function parseFrontmatter(text: string): Record<string, string> {
152
152
 
153
153
  /** Extract YAML content from a fenced ```yaml ... ``` block. */
154
154
  function extractYamlBlock(text: string): string | null {
155
- // 1. Prefer explicit yaml/yml fence
156
- const yamlFence = text.match(/```ya?ml\s*\n([\s\S]*?)```/)
155
+ // 1. Prefer explicit yaml/yml fence — greedy to skip backticks inside content
156
+ const yamlFence = text.match(/```ya?ml\s*\n([\s\S]*)```/)
157
157
  if (yamlFence) return yamlFence[1].trim()
158
158
 
159
159
  // 2. Fallback: any code fence whose content looks like a convoy spec
@@ -382,7 +382,9 @@ export async function runPromptStep(opts: PromptStepOptions): Promise<PromptStep
382
382
 
383
383
  if (outputType === 'json') {
384
384
  // Extract JSON from a ```json fenced block — fail fast if missing
385
- const jsonFenceMatch = rawOutput.match(/```json\s*\n([\s\S]*?)```/)
385
+ // Use greedy match (.*) to capture up to the LAST closing fence,
386
+ // since task prompts may contain triple backticks in code examples
387
+ const jsonFenceMatch = rawOutput.match(/```json\s*\n([\s\S]*)```/)
386
388
  if (!jsonFenceMatch) {
387
389
  const preview = rawOutput.slice(0, 300)
388
390
  throw new Error(
@@ -51,21 +51,21 @@
51
51
  "name": "docs/api-v2-contract.json",
52
52
  "type": "json",
53
53
  "task_id": "api-t1",
54
- "created_at": "2026-03-18T01:18:30.528Z"
54
+ "created_at": "2026-03-18T15:52:51.142Z"
55
55
  },
56
56
  {
57
57
  "id": "artifact-demo-api-v2-reports-security-gate-failure-md",
58
58
  "name": "reports/security-gate-failure.md",
59
59
  "type": "summary",
60
60
  "task_id": "api-t3",
61
- "created_at": "2026-03-18T01:18:30.528Z"
61
+ "created_at": "2026-03-18T15:52:51.142Z"
62
62
  },
63
63
  {
64
64
  "id": "artifact-demo-api-v2-src-api-rate-limiter-ts",
65
65
  "name": "src/api/rate-limiter.ts",
66
66
  "type": "file",
67
67
  "task_id": "api-t2",
68
- "created_at": "2026-03-18T01:18:30.528Z"
68
+ "created_at": "2026-03-18T15:52:51.142Z"
69
69
  }
70
70
  ],
71
71
  "has_more_events": false,
@@ -42,28 +42,28 @@
42
42
  "name": "libs/auth/src/jwt-middleware.ts",
43
43
  "type": "file",
44
44
  "task_id": "auth-t2",
45
- "created_at": "2026-03-18T01:18:30.527Z"
45
+ "created_at": "2026-03-18T15:52:51.141Z"
46
46
  },
47
47
  {
48
48
  "id": "artifact-demo-auth-revamp-libs-auth-src-rls-policies-sql",
49
49
  "name": "libs/auth/src/rls-policies.sql",
50
50
  "type": "file",
51
51
  "task_id": "auth-t3",
52
- "created_at": "2026-03-18T01:18:30.527Z"
52
+ "created_at": "2026-03-18T15:52:51.141Z"
53
53
  },
54
54
  {
55
55
  "id": "artifact-demo-auth-revamp-reports-auth-review-summary-md",
56
56
  "name": "reports/auth-review-summary.md",
57
57
  "type": "summary",
58
58
  "task_id": "auth-t5",
59
- "created_at": "2026-03-18T01:18:30.528Z"
59
+ "created_at": "2026-03-18T15:52:51.141Z"
60
60
  },
61
61
  {
62
62
  "id": "artifact-demo-auth-revamp-tests-auth-integration-test-ts",
63
63
  "name": "tests/auth/integration.test.ts",
64
64
  "type": "file",
65
65
  "task_id": "auth-t4",
66
- "created_at": "2026-03-18T01:18:30.528Z"
66
+ "created_at": "2026-03-18T15:52:51.141Z"
67
67
  }
68
68
  ],
69
69
  "has_more_events": false,
@@ -46,47 +46,47 @@
46
46
  ],
47
47
  "artifact_count": 6,
48
48
  "artifacts": [
49
+ {
50
+ "id": "artifact-demo-dashboard-ui-src-components-design-tokens-ts",
51
+ "name": "src/components/design-tokens.ts",
52
+ "type": "file",
53
+ "task_id": "ui-t1",
54
+ "created_at": "2026-03-18T15:52:51.141Z"
55
+ },
49
56
  {
50
57
  "id": "artifact-demo-dashboard-ui-reports-panel-review-dashboard-md",
51
58
  "name": "reports/panel-review-dashboard.md",
52
59
  "type": "summary",
53
60
  "task_id": "ui-t7",
54
- "created_at": "2026-03-18T01:18:30.528Z"
61
+ "created_at": "2026-03-18T15:52:51.142Z"
55
62
  },
56
63
  {
57
64
  "id": "artifact-demo-dashboard-ui-reports-visual-regression-json",
58
65
  "name": "reports/visual-regression.json",
59
66
  "type": "json",
60
67
  "task_id": "ui-t6",
61
- "created_at": "2026-03-18T01:18:30.528Z"
68
+ "created_at": "2026-03-18T15:52:51.142Z"
62
69
  },
63
70
  {
64
71
  "id": "artifact-demo-dashboard-ui-src-components-DonutChart-tsx",
65
72
  "name": "src/components/DonutChart.tsx",
66
73
  "type": "file",
67
74
  "task_id": "ui-t3",
68
- "created_at": "2026-03-18T01:18:30.528Z"
75
+ "created_at": "2026-03-18T15:52:51.142Z"
69
76
  },
70
77
  {
71
78
  "id": "artifact-demo-dashboard-ui-src-components-KpiCard-tsx",
72
79
  "name": "src/components/KpiCard.tsx",
73
80
  "type": "file",
74
81
  "task_id": "ui-t2",
75
- "created_at": "2026-03-18T01:18:30.528Z"
76
- },
77
- {
78
- "id": "artifact-demo-dashboard-ui-src-components-design-tokens-ts",
79
- "name": "src/components/design-tokens.ts",
80
- "type": "file",
81
- "task_id": "ui-t1",
82
- "created_at": "2026-03-18T01:18:30.528Z"
82
+ "created_at": "2026-03-18T15:52:51.142Z"
83
83
  },
84
84
  {
85
85
  "id": "artifact-demo-dashboard-ui-src-styles-animations-css",
86
86
  "name": "src/styles/animations.css",
87
87
  "type": "file",
88
88
  "task_id": "ui-t4",
89
- "created_at": "2026-03-18T01:18:30.528Z"
89
+ "created_at": "2026-03-18T15:52:51.142Z"
90
90
  }
91
91
  ],
92
92
  "has_more_events": false,
@@ -42,21 +42,21 @@
42
42
  "name": "src/etl/pipeline.ts",
43
43
  "type": "file",
44
44
  "task_id": "etl-t2",
45
- "created_at": "2026-03-18T01:18:30.529Z"
45
+ "created_at": "2026-03-18T15:52:51.143Z"
46
46
  },
47
47
  {
48
48
  "id": "artifact-demo-data-pipeline-src-etl-schema-ts",
49
49
  "name": "src/etl/schema.ts",
50
50
  "type": "file",
51
51
  "task_id": "etl-t1",
52
- "created_at": "2026-03-18T01:18:30.529Z"
52
+ "created_at": "2026-03-18T15:52:51.143Z"
53
53
  },
54
54
  {
55
55
  "id": "artifact-demo-data-pipeline-tests-etl-pipeline-test-ts",
56
56
  "name": "tests/etl/pipeline.test.ts",
57
57
  "type": "file",
58
58
  "task_id": "etl-t3",
59
- "created_at": "2026-03-18T01:18:30.529Z"
59
+ "created_at": "2026-03-18T15:52:51.143Z"
60
60
  }
61
61
  ],
62
62
  "has_more_events": false,
@@ -51,7 +51,7 @@
51
51
  "name": ".github/workflows/ci.yml",
52
52
  "type": "file",
53
53
  "task_id": "ci-t1",
54
- "created_at": "2026-03-18T01:18:30.529Z"
54
+ "created_at": "2026-03-18T15:52:51.143Z"
55
55
  }
56
56
  ],
57
57
  "has_more_events": false,
@@ -42,21 +42,21 @@
42
42
  "name": "docs/ARCHITECTURE.md",
43
43
  "type": "file",
44
44
  "task_id": "docs-t1",
45
- "created_at": "2026-03-18T01:18:30.529Z"
45
+ "created_at": "2026-03-18T15:52:51.143Z"
46
46
  },
47
47
  {
48
48
  "id": "artifact-demo-docs-update-docs-README-md",
49
49
  "name": "docs/README.md",
50
50
  "type": "file",
51
51
  "task_id": "docs-t1",
52
- "created_at": "2026-03-18T01:18:30.529Z"
52
+ "created_at": "2026-03-18T15:52:51.143Z"
53
53
  },
54
54
  {
55
55
  "id": "artifact-demo-docs-update-docs-api-reference-json",
56
56
  "name": "docs/api-reference.json",
57
57
  "type": "json",
58
58
  "task_id": "docs-t2",
59
- "created_at": "2026-03-18T01:18:30.529Z"
59
+ "created_at": "2026-03-18T15:52:51.143Z"
60
60
  }
61
61
  ],
62
62
  "has_more_events": false,