gswd 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/bin/gswd-tools.cjs +228 -0
  2. package/commands/gswd/imagine.md +7 -1
  3. package/commands/gswd/start.md +507 -32
  4. package/dist/lib/audit.d.ts +205 -0
  5. package/dist/lib/audit.js +805 -0
  6. package/dist/lib/bootstrap.d.ts +103 -0
  7. package/dist/lib/bootstrap.js +563 -0
  8. package/dist/lib/compile.d.ts +239 -0
  9. package/dist/lib/compile.js +1152 -0
  10. package/dist/lib/config.d.ts +49 -0
  11. package/dist/lib/config.js +150 -0
  12. package/dist/lib/imagine-agents.d.ts +54 -0
  13. package/dist/lib/imagine-agents.js +185 -0
  14. package/dist/lib/imagine-gate.d.ts +47 -0
  15. package/dist/lib/imagine-gate.js +131 -0
  16. package/dist/lib/imagine-input.d.ts +46 -0
  17. package/dist/lib/imagine-input.js +233 -0
  18. package/dist/lib/imagine-synthesis.d.ts +90 -0
  19. package/dist/lib/imagine-synthesis.js +453 -0
  20. package/dist/lib/imagine.d.ts +56 -0
  21. package/dist/lib/imagine.js +413 -0
  22. package/dist/lib/intake.d.ts +27 -0
  23. package/dist/lib/intake.js +82 -0
  24. package/dist/lib/parse.d.ts +59 -0
  25. package/dist/lib/parse.js +171 -0
  26. package/dist/lib/render.d.ts +309 -0
  27. package/dist/lib/render.js +624 -0
  28. package/dist/lib/specify-agents.d.ts +120 -0
  29. package/dist/lib/specify-agents.js +269 -0
  30. package/dist/lib/specify-journeys.d.ts +124 -0
  31. package/dist/lib/specify-journeys.js +279 -0
  32. package/dist/lib/specify-nfr.d.ts +45 -0
  33. package/dist/lib/specify-nfr.js +159 -0
  34. package/dist/lib/specify-roles.d.ts +46 -0
  35. package/dist/lib/specify-roles.js +88 -0
  36. package/dist/lib/specify.d.ts +70 -0
  37. package/dist/lib/specify.js +676 -0
  38. package/dist/lib/state.d.ts +140 -0
  39. package/dist/lib/state.js +340 -0
  40. package/dist/tests/audit.test.d.ts +4 -0
  41. package/dist/tests/audit.test.js +1579 -0
  42. package/dist/tests/bootstrap.test.d.ts +5 -0
  43. package/dist/tests/bootstrap.test.js +611 -0
  44. package/dist/tests/compile.test.d.ts +4 -0
  45. package/dist/tests/compile.test.js +862 -0
  46. package/dist/tests/config.test.d.ts +4 -0
  47. package/dist/tests/config.test.js +191 -0
  48. package/dist/tests/imagine-agents.test.d.ts +6 -0
  49. package/dist/tests/imagine-agents.test.js +179 -0
  50. package/dist/tests/imagine-gate.test.d.ts +6 -0
  51. package/dist/tests/imagine-gate.test.js +264 -0
  52. package/dist/tests/imagine-input.test.d.ts +6 -0
  53. package/dist/tests/imagine-input.test.js +283 -0
  54. package/dist/tests/imagine-synthesis.test.d.ts +7 -0
  55. package/dist/tests/imagine-synthesis.test.js +380 -0
  56. package/dist/tests/imagine.test.d.ts +8 -0
  57. package/dist/tests/imagine.test.js +406 -0
  58. package/dist/tests/parse.test.d.ts +4 -0
  59. package/dist/tests/parse.test.js +285 -0
  60. package/dist/tests/render.test.d.ts +4 -0
  61. package/dist/tests/render.test.js +236 -0
  62. package/dist/tests/specify-agents.test.d.ts +4 -0
  63. package/dist/tests/specify-agents.test.js +352 -0
  64. package/dist/tests/specify-journeys.test.d.ts +5 -0
  65. package/dist/tests/specify-journeys.test.js +440 -0
  66. package/dist/tests/specify-nfr.test.d.ts +4 -0
  67. package/dist/tests/specify-nfr.test.js +205 -0
  68. package/dist/tests/specify-roles.test.d.ts +4 -0
  69. package/dist/tests/specify-roles.test.js +136 -0
  70. package/dist/tests/specify.test.d.ts +9 -0
  71. package/dist/tests/specify.test.js +544 -0
  72. package/dist/tests/state.test.d.ts +4 -0
  73. package/dist/tests/state.test.js +316 -0
  74. package/lib/bootstrap.ts +37 -11
  75. package/lib/compile.ts +426 -4
  76. package/lib/imagine-agents.ts +53 -7
  77. package/lib/imagine-synthesis.ts +170 -6
  78. package/lib/imagine.ts +59 -5
  79. package/lib/intake.ts +60 -0
  80. package/lib/parse.ts +2 -1
  81. package/lib/render.ts +566 -5
  82. package/lib/specify-agents.ts +25 -3
  83. package/lib/state.ts +115 -0
  84. package/package.json +3 -2
  85. package/templates/gswd/DECISIONS.template.md +3 -0
package/lib/compile.ts CHANGED
@@ -28,6 +28,7 @@ export interface ParsedFR {
28
28
  scope: 'v1' | 'v2' | 'out';
29
29
  priority: 'P0' | 'P1' | 'P2';
30
30
  sourceJourneys: string[];
31
+ acceptanceCriteria: string[];
31
32
  }
32
33
 
33
34
  export interface ParsedNFR {
@@ -69,6 +70,11 @@ export interface SpecBundle {
69
70
  journeys: ParsedJourney[];
70
71
  integrations: ParsedIntegration[];
71
72
  projectSlug: string;
73
+ architectureSummary: string;
74
+ icpHighlights: string;
75
+ competitionLandscape: string;
76
+ gtmPositioning: string;
77
+ journeyInsights: string;
72
78
  }
73
79
 
74
80
  // ─── Constants (all exported) ─────────────────────────────────────────────────
@@ -151,12 +157,26 @@ export function parseFRsFromSpec(
151
157
 
152
158
  const sourceJourneys = Array.from(frToJourneys.get(pos.id) ?? new Set<string>()).sort();
153
159
 
160
+ // Extract acceptance criteria: lines after "**Acceptance Criteria:**" heading
161
+ const acceptanceCriteria: string[] = [];
162
+ const acMatch = section.match(/\*{0,2}Acceptance Criteria:?\*{0,2}:?\s*\n([\s\S]*?)(?=\*\*|###|$)/i);
163
+ if (acMatch) {
164
+ const acLines = acMatch[1].split('\n');
165
+ for (const acLine of acLines) {
166
+ const bulletMatch = acLine.match(/^[-*]\s+(.+)/);
167
+ if (bulletMatch) {
168
+ acceptanceCriteria.push(bulletMatch[1].trim());
169
+ }
170
+ }
171
+ }
172
+
154
173
  return {
155
174
  id: pos.id,
156
175
  description,
157
176
  scope: (scopeMatch?.[1]?.toLowerCase() as 'v1' | 'v2' | 'out') ?? 'v1',
158
177
  priority: (priorityMatch?.[1] as 'P0' | 'P1' | 'P2') ?? 'P1',
159
178
  sourceJourneys,
179
+ acceptanceCriteria,
160
180
  };
161
181
  });
162
182
 
@@ -346,6 +366,12 @@ export function parseSpecBundle(
346
366
  nfr: string;
347
367
  journeys: string;
348
368
  integrations: string;
369
+ architecture?: string;
370
+ icp?: string;
371
+ competition?: string;
372
+ gtm?: string;
373
+ researchSummary?: string;
374
+ features?: string;
349
375
  },
350
376
  statePath?: string
351
377
  ): SpecBundle {
@@ -412,6 +438,52 @@ export function parseSpecBundle(
412
438
  }
413
439
  }
414
440
 
441
+ // Populate new enrichment fields from research files
442
+ const architectureContent = files.architecture ?? '';
443
+ const icpContent = files.icp ?? '';
444
+ const competitionContent = files.competition ?? '';
445
+ const gtmContent = files.gtm ?? '';
446
+ const featuresContent = files.features ?? '';
447
+
448
+ const architectureSummary =
449
+ extractHeadingContent(architectureContent, '## Standard Architecture') ??
450
+ extractHeadingContent(architectureContent, '## Executive Summary') ??
451
+ '';
452
+
453
+ const icpHighlights =
454
+ extractHeadingContent(icpContent, '## ICP Highlights') ??
455
+ extractHeadingContent(icpContent, '## Target Users') ??
456
+ extractHeadingContent(featuresContent, '### Expected Features') ??
457
+ extractHeadingContent(featuresContent, '## Expected Features') ??
458
+ '';
459
+
460
+ const competitionLandscape =
461
+ extractHeadingContent(competitionContent, '## Competitor Analysis') ??
462
+ extractHeadingContent(competitionContent, '## Landscape') ??
463
+ extractHeadingContent(featuresContent, '## Competitor Feature Analysis') ??
464
+ '';
465
+
466
+ const gtmPositioning =
467
+ extractHeadingContent(gtmContent, '## GTM Positioning') ??
468
+ extractHeadingContent(gtmContent, '## Positioning') ??
469
+ extractHeadingContent(gtmContent, '## Executive Summary') ??
470
+ '';
471
+
472
+ // Synthesize journeyInsights from journeys array
473
+ const journeyTypeCounts = new Map<string, number>();
474
+ for (const journey of journeys) {
475
+ const count = journeyTypeCounts.get(journey.type) ?? 0;
476
+ journeyTypeCounts.set(journey.type, count + 1);
477
+ }
478
+ const journeyInsightParts: string[] = [];
479
+ for (const journey of journeys) {
480
+ const frCount = journey.linkedFRs.length;
481
+ journeyInsightParts.push(`${journey.id} (${journey.name}, type: ${journey.type}, FRs: ${frCount})`);
482
+ }
483
+ const journeyInsights = journeyInsightParts.length > 0
484
+ ? journeyInsightParts.join('\n')
485
+ : '';
486
+
415
487
  return {
416
488
  vision,
417
489
  targetUser,
@@ -429,6 +501,11 @@ export function parseSpecBundle(
429
501
  journeys,
430
502
  integrations,
431
503
  projectSlug,
504
+ architectureSummary,
505
+ icpHighlights,
506
+ competitionLandscape,
507
+ gtmPositioning,
508
+ journeyInsights,
432
509
  };
433
510
  }
434
511
 
@@ -476,6 +553,96 @@ export function generateProjectDoc(bundle: SpecBundle): string {
476
553
  }
477
554
  lines.push('');
478
555
 
556
+ // ── ## Context section ──────────────────────────────────────────────────────
557
+ lines.push('## Context');
558
+ lines.push('');
559
+ lines.push('Product context synthesized from GSWD research pipeline. Each subsection summarizes key findings and references the source artifact.');
560
+ lines.push('');
561
+
562
+ // Architecture Summary
563
+ lines.push('### Architecture Summary');
564
+ lines.push('');
565
+ if (bundle.architectureSummary) {
566
+ lines.push(bundle.architectureSummary);
567
+ } else {
568
+ lines.push('*(Architecture research not available — run /gswd:imagine to generate)*');
569
+ }
570
+ lines.push('');
571
+ lines.push('> See: .planning/research/ARCHITECTURE.md for full details');
572
+ lines.push('');
573
+
574
+ // ICP Highlights
575
+ lines.push('### ICP Highlights');
576
+ lines.push('');
577
+ if (bundle.icpHighlights) {
578
+ lines.push(bundle.icpHighlights);
579
+ } else {
580
+ lines.push('*(ICP research not available)*');
581
+ }
582
+ lines.push('');
583
+ lines.push('> See: .planning/research/ICP.md for full details');
584
+ lines.push('');
585
+
586
+ // Competition Landscape
587
+ lines.push('### Competition Landscape');
588
+ lines.push('');
589
+ if (bundle.competitionLandscape) {
590
+ lines.push(bundle.competitionLandscape);
591
+ } else {
592
+ lines.push('*(Competition research not available)*');
593
+ }
594
+ lines.push('');
595
+ lines.push('> See: .planning/research/COMPETITION.md for full details');
596
+ lines.push('');
597
+
598
+ // GTM Positioning
599
+ lines.push('### GTM Positioning');
600
+ lines.push('');
601
+ if (bundle.gtmPositioning) {
602
+ lines.push(bundle.gtmPositioning);
603
+ } else {
604
+ lines.push('*(GTM research not available)*');
605
+ }
606
+ lines.push('');
607
+ lines.push('> See: .planning/research/GTM.md for full details');
608
+ lines.push('');
609
+
610
+ // Journey Insights
611
+ lines.push('### Journey Insights');
612
+ lines.push('');
613
+ if (bundle.journeyInsights) {
614
+ lines.push(bundle.journeyInsights);
615
+ } else if (bundle.journeys.length > 0) {
616
+ // Synthesize from journeys array
617
+ for (const j of bundle.journeys) {
618
+ lines.push(`${j.id} (${j.name}, type: ${j.type}, FRs: ${j.linkedFRs.length})`);
619
+ }
620
+ } else {
621
+ lines.push('*(No journeys defined)*');
622
+ }
623
+ lines.push('');
624
+ lines.push('> See: .planning/research/FEATURES.md for full details');
625
+ lines.push('');
626
+
627
+ // GSWD Artifacts table
628
+ lines.push('### GSWD Artifacts');
629
+ lines.push('');
630
+ lines.push('| File | Description |');
631
+ lines.push('|------|-------------|');
632
+ lines.push('| .planning/IMAGINE.md | Product vision, target user, and direction |');
633
+ lines.push('| .planning/DECISIONS.md | Frozen decisions, success metrics, risks |');
634
+ lines.push('| .planning/SPEC.md | Functional requirements with IDs |');
635
+ lines.push('| .planning/NFR.md | Non-functional requirements |');
636
+ lines.push('| .planning/JOURNEYS.md | User journeys with FR linkages |');
637
+ lines.push('| .planning/INTEGRATIONS.md | External integrations and fallbacks |');
638
+ lines.push('| .planning/research/ARCHITECTURE.md | Architecture research |');
639
+ lines.push('| .planning/research/FEATURES.md | Feature landscape research |');
640
+ lines.push('| .planning/research/PITFALLS.md | Common pitfalls research |');
641
+ lines.push('| .planning/research/STACK.md | Technology stack research |');
642
+ lines.push('| .planning/research/SUMMARY.md | Research executive summary |');
643
+ lines.push('| .planning/research/gswd/SUMMARY.md | GSD-format research bridge |');
644
+ lines.push('');
645
+
479
646
  return lines.join('\n');
480
647
  }
481
648
 
@@ -506,11 +673,65 @@ export function generateRequirementsDoc(bundle: SpecBundle): string {
506
673
  const v1FRs = sortedFRs.filter((fr) => fr.scope === 'v1');
507
674
  const v2FRs = sortedFRs.filter((fr) => fr.scope === 'v2');
508
675
 
676
+ // Build a journey map for quick lookup
677
+ const journeyMap = new Map<string, ParsedJourney>();
678
+ for (const journey of bundle.journeys) {
679
+ journeyMap.set(journey.id, journey);
680
+ }
681
+
509
682
  lines.push('### v1 (In Scope)');
510
683
  lines.push('');
511
684
  for (const fr of v1FRs) {
512
685
  lines.push(`#### ${fr.id}: ${fr.description}`);
513
686
  lines.push(`**Scope:** ${fr.scope} **Priority:** ${fr.priority}`);
687
+
688
+ // Journey origin
689
+ if (fr.sourceJourneys.length > 0) {
690
+ const journeyRefs = fr.sourceJourneys
691
+ .sort()
692
+ .map((jId) => {
693
+ const journey = journeyMap.get(jId);
694
+ return journey ? `${jId}: ${journey.name}` : jId;
695
+ })
696
+ .join(', ');
697
+ lines.push(`**Origin:** From ${journeyRefs}`);
698
+ }
699
+
700
+ // Acceptance criteria
701
+ if (fr.acceptanceCriteria && fr.acceptanceCriteria.length > 0) {
702
+ lines.push('**Acceptance Criteria:**');
703
+ for (const criterion of fr.acceptanceCriteria) {
704
+ lines.push(`- ${criterion}`);
705
+ }
706
+ }
707
+
708
+ // NFR constraints — apply heuristic based on NFR category
709
+ const applicableNFRs: string[] = [];
710
+ for (const nfr of bundle.nfrs) {
711
+ const cat = nfr.category.toLowerCase();
712
+ // Security and performance apply to all v1 FRs
713
+ if (cat === 'security' || cat === 'performance') {
714
+ applicableNFRs.push(`${nfr.id} (${nfr.category.charAt(0).toUpperCase() + nfr.category.slice(1)})`);
715
+ continue;
716
+ }
717
+ // Observability applies to all v1 FRs
718
+ if (cat === 'observability') {
719
+ applicableNFRs.push(`${nfr.id} (${nfr.category.charAt(0).toUpperCase() + nfr.category.slice(1)})`);
720
+ continue;
721
+ }
722
+ // Privacy applies if FR touches user/data/personal/profile/account keywords
723
+ if (cat === 'privacy') {
724
+ const desc = fr.description.toLowerCase();
725
+ if (desc.includes('user') || desc.includes('data') || desc.includes('personal') ||
726
+ desc.includes('profile') || desc.includes('account')) {
727
+ applicableNFRs.push(`${nfr.id} (${nfr.category.charAt(0).toUpperCase() + nfr.category.slice(1)})`);
728
+ }
729
+ }
730
+ }
731
+ if (applicableNFRs.length > 0) {
732
+ lines.push(`**Applicable NFR Constraints:** ${applicableNFRs.join(', ')}`);
733
+ }
734
+
514
735
  lines.push('');
515
736
  }
516
737
  if (v1FRs.length === 0) {
@@ -614,6 +835,12 @@ export function generateRoadmapDoc(bundle: SpecBundle): string {
614
835
  if (journeys.length === 0) {
615
836
  lines.push('*(no journeys assigned)*');
616
837
  } else {
838
+ // Emit a Goal line with journey IDs
839
+ const goalJourneys = journeys
840
+ .map((j) => `${j.id} (${j.name})`)
841
+ .join(', ');
842
+ lines.push(`**Goal:** Implement ${goalJourneys}`);
843
+ lines.push('');
617
844
  for (const j of journeys) {
618
845
  lines.push(`- **${j.id}**: ${j.name}`);
619
846
  const frRefs = [...j.linkedFRs].sort().join(', ');
@@ -631,6 +858,10 @@ export function generateRoadmapDoc(bundle: SpecBundle): string {
631
858
  if (obsSecNFRs.length === 0) {
632
859
  lines.push('*(no NFRs assigned)*');
633
860
  } else {
861
+ // Emit a Goal line for Phase 4
862
+ const nfrGoal = obsSecNFRs.map((nfr) => nfr.id).join(', ');
863
+ lines.push(`**Goal:** Enforce ${nfrGoal}`);
864
+ lines.push('');
634
865
  for (const nfr of obsSecNFRs) {
635
866
  lines.push(`- **${nfr.id}**: ${nfr.description}`);
636
867
  }
@@ -643,6 +874,160 @@ export function generateRoadmapDoc(bundle: SpecBundle): string {
643
874
  return lines.join('\n');
644
875
  }
645
876
 
877
+ /**
878
+ * Generate a GSD-format research SUMMARY.md from SpecBundle.
879
+ *
880
+ * Maps GSWD spec content to GSD research template headings.
881
+ * Pure function — no Date calls, no I/O.
882
+ * Versioned with <!-- gswd-schema-version: 1 --> header.
883
+ */
884
+ export function generateResearchSummary(bundle: SpecBundle): string {
885
+ const lines: string[] = [];
886
+
887
+ lines.push('<!-- gswd-schema-version: 1 -->');
888
+ lines.push('# Project Research Summary');
889
+ lines.push('');
890
+ lines.push(`**Project:** ${bundle.projectSlug}`);
891
+ lines.push('**Domain:** Product specification');
892
+ lines.push('**Confidence:** HIGH');
893
+ lines.push('');
894
+
895
+ // ## Executive Summary
896
+ lines.push('## Executive Summary');
897
+ lines.push('');
898
+ if (bundle.vision) {
899
+ lines.push(bundle.vision);
900
+ lines.push('');
901
+ }
902
+ if (bundle.productDirection) {
903
+ lines.push(bundle.productDirection);
904
+ lines.push('');
905
+ }
906
+
907
+ // ## Key Findings
908
+ lines.push('## Key Findings');
909
+ lines.push('');
910
+
911
+ // ### Recommended Stack
912
+ lines.push('### Recommended Stack');
913
+ lines.push('');
914
+ if (bundle.architectureSummary) {
915
+ lines.push(bundle.architectureSummary);
916
+ } else {
917
+ lines.push('*(Stack research not available from GSWD — will be determined during GSD planning)*');
918
+ }
919
+ lines.push('');
920
+
921
+ // ### Expected Features
922
+ lines.push('### Expected Features');
923
+ lines.push('');
924
+ if (bundle.icpHighlights) {
925
+ lines.push(bundle.icpHighlights);
926
+ } else {
927
+ // Synthesize from FRs
928
+ const p0FRs = bundle.frs.filter((f) => f.scope === 'v1' && f.priority === 'P0');
929
+ const p1FRs = bundle.frs.filter((f) => f.scope === 'v1' && f.priority === 'P1');
930
+
931
+ if (p0FRs.length > 0) {
932
+ lines.push('**Must have (table stakes):**');
933
+ for (const fr of p0FRs) {
934
+ lines.push(`- ${fr.description}`);
935
+ }
936
+ lines.push('');
937
+ }
938
+ if (p1FRs.length > 0) {
939
+ lines.push('**Should have:**');
940
+ for (const fr of p1FRs) {
941
+ lines.push(`- ${fr.description}`);
942
+ }
943
+ }
944
+ }
945
+ lines.push('');
946
+
947
+ // ### Architecture Approach
948
+ lines.push('### Architecture Approach');
949
+ lines.push('');
950
+ if (bundle.architectureSummary) {
951
+ lines.push(bundle.architectureSummary);
952
+ } else {
953
+ lines.push('*(Architecture to be determined during GSD planning)*');
954
+ }
955
+ lines.push('');
956
+
957
+ // ### Critical Pitfalls
958
+ lines.push('### Critical Pitfalls');
959
+ lines.push('');
960
+ if (bundle.risks.length > 0) {
961
+ for (const risk of bundle.risks) {
962
+ lines.push(`- ${risk}`);
963
+ }
964
+ } else {
965
+ lines.push('*(No risks identified)*');
966
+ }
967
+ lines.push('');
968
+
969
+ // ## Implications for Roadmap
970
+ lines.push('## Implications for Roadmap');
971
+ lines.push('');
972
+ // Group journeys by phase and describe implications
973
+ const phaseJourneyMap = new Map<number, ParsedJourney[]>();
974
+ for (let i = 1; i <= 4; i++) {
975
+ phaseJourneyMap.set(i, []);
976
+ }
977
+ for (const journey of bundle.journeys) {
978
+ const phase = JOURNEY_TYPE_TO_PHASE[journey.type] ?? 3;
979
+ phaseJourneyMap.get(phase)!.push(journey);
980
+ }
981
+ for (let phaseNum = 1; phaseNum <= 3; phaseNum++) {
982
+ const phaseJ = phaseJourneyMap.get(phaseNum)!;
983
+ if (phaseJ.length > 0) {
984
+ const journeyList = phaseJ.map((j) => `${j.id} (${j.name})`).join(', ');
985
+ lines.push(`- **Phase ${phaseNum}**: ${PHASE_NAMES[phaseNum]} — ${journeyList}`);
986
+ }
987
+ }
988
+ const obsSecNFRs = bundle.nfrs.filter(
989
+ (nfr) => nfr.category === 'observability' || nfr.category === 'security'
990
+ );
991
+ if (obsSecNFRs.length > 0) {
992
+ const nfrList = obsSecNFRs.map((nfr) => nfr.id).join(', ');
993
+ lines.push(`- **Phase 4**: ${PHASE_NAMES[4]} — enforce ${nfrList}`);
994
+ }
995
+ lines.push('');
996
+
997
+ // ## Confidence Assessment
998
+ lines.push('## Confidence Assessment');
999
+ lines.push('');
1000
+ lines.push('| Area | Confidence | Notes |');
1001
+ lines.push('|------|------------|-------|');
1002
+ lines.push(
1003
+ `| Stack | ${bundle.architectureSummary ? 'HIGH' : 'LOW'} | ${
1004
+ bundle.architectureSummary ? 'From GSWD research' : 'Not researched in GSWD'
1005
+ } |`
1006
+ );
1007
+ lines.push(`| Features | HIGH | ${bundle.frs.length} FRs defined with traceability |`);
1008
+ lines.push(
1009
+ `| Architecture | ${bundle.architectureSummary ? 'HIGH' : 'MEDIUM'} | ${
1010
+ bundle.architectureSummary ? 'From GSWD research' : 'Inferred from requirements'
1011
+ } |`
1012
+ );
1013
+ lines.push(`| Pitfalls | MEDIUM | ${bundle.risks.length} risks identified |`);
1014
+ lines.push('');
1015
+
1016
+ // ## Sources
1017
+ lines.push('## Sources');
1018
+ lines.push('');
1019
+ lines.push('### Primary (HIGH confidence)');
1020
+ lines.push('- GSWD specification pipeline — full imagine/specify/audit/compile cycle');
1021
+ lines.push('');
1022
+
1023
+ lines.push('---');
1024
+ lines.push('*Research completed: via GSWD compile*');
1025
+ lines.push('*Ready for roadmap: yes*');
1026
+ lines.push('');
1027
+
1028
+ return lines.join('\n');
1029
+ }
1030
+
646
1031
  // ─── Validator Types ──────────────────────────────────────────────────────────
647
1032
 
648
1033
  export interface ValidatorFinding {
@@ -809,6 +1194,7 @@ export function generateStateDoc(bundle: SpecBundle): string {
809
1194
 
810
1195
  /**
811
1196
  * Read all 6 spec artifact files from a planning directory.
1197
+ * Also reads research files from .planning/research/ directory.
812
1198
  * Returns a content map keyed by artifact type.
813
1199
  * Missing files return empty string (does not throw).
814
1200
  */
@@ -831,6 +1217,25 @@ export function readSpecArtifacts(planningDir: string): Record<string, string> {
831
1217
  }
832
1218
  }
833
1219
 
1220
+ // Read research files from .planning/research/ subdirectory
1221
+ const researchDir = path.join(planningDir, 'research');
1222
+ const researchFileMap: Array<[string, string]> = [
1223
+ ['ARCHITECTURE.md', 'architecture'],
1224
+ ['ICP.md', 'icp'],
1225
+ ['COMPETITION.md', 'competition'],
1226
+ ['GTM.md', 'gtm'],
1227
+ ['SUMMARY.md', 'researchSummary'],
1228
+ ['FEATURES.md', 'features'],
1229
+ ];
1230
+
1231
+ for (const [filename, key] of researchFileMap) {
1232
+ try {
1233
+ result[key] = fs.readFileSync(path.join(researchDir, filename), 'utf-8');
1234
+ } catch {
1235
+ result[key] = '';
1236
+ }
1237
+ }
1238
+
834
1239
  return result;
835
1240
  }
836
1241
 
@@ -841,7 +1246,7 @@ export interface CompileWorkflowResult {
841
1246
  error?: string;
842
1247
  validatorResult?: ValidatorResult;
843
1248
  filesWritten: string[];
844
- generated?: { project: string; requirements: string; roadmap: string; state: string };
1249
+ generated?: { project: string; requirements: string; roadmap: string; state: string; researchSummary: string };
845
1250
  }
846
1251
 
847
1252
  // ─── Workflow Orchestrator ────────────────────────────────────────────────────
@@ -879,7 +1284,7 @@ export function runCompileWorkflow(options: {
879
1284
  // Step 3: Read spec artifacts
880
1285
  const files = readSpecArtifacts(planningDir);
881
1286
 
882
- // Step 4: Parse into SpecBundle
1287
+ // Step 4: Parse into SpecBundle (includes research file enrichment)
883
1288
  const bundle = parseSpecBundle(
884
1289
  {
885
1290
  imagine: files['imagine'] ?? '',
@@ -888,16 +1293,23 @@ export function runCompileWorkflow(options: {
888
1293
  nfr: files['nfr'] ?? '',
889
1294
  journeys: files['journeys'] ?? '',
890
1295
  integrations: files['integrations'] ?? '',
1296
+ architecture: files['architecture'] ?? '',
1297
+ icp: files['icp'] ?? '',
1298
+ competition: files['competition'] ?? '',
1299
+ gtm: files['gtm'] ?? '',
1300
+ researchSummary: files['researchSummary'] ?? '',
1301
+ features: files['features'] ?? '',
891
1302
  },
892
1303
  statePath
893
1304
  );
894
1305
 
895
- // Step 5: Generate all 4 docs (pure functions)
1306
+ // Step 5: Generate all 4 contract docs + GSD research bridge (pure functions)
896
1307
  const generated = {
897
1308
  project: generateProjectDoc(bundle),
898
1309
  requirements: generateRequirementsDoc(bundle),
899
1310
  roadmap: generateRoadmapDoc(bundle),
900
1311
  state: generateStateDoc(bundle),
1312
+ researchSummary: generateResearchSummary(bundle),
901
1313
  };
902
1314
 
903
1315
  // Step 6: Run validator
@@ -909,7 +1321,7 @@ export function runCompileWorkflow(options: {
909
1321
  return { passed: false, validatorResult, filesWritten: [] };
910
1322
  }
911
1323
 
912
- // Step 8: Write all 4 docs atomically
1324
+ // Step 8: Write all 4 contract docs atomically
913
1325
  const fileOutputMap: Array<[string, string]> = [
914
1326
  ['PROJECT.md', generated.project],
915
1327
  ['REQUIREMENTS.md', generated.requirements],
@@ -924,6 +1336,16 @@ export function runCompileWorkflow(options: {
924
1336
  filesWritten.push(filePath);
925
1337
  }
926
1338
 
1339
+ // Step 8b: Write GSD research bridge SUMMARY.md to .planning/research/gswd/SUMMARY.md
1340
+ // planningDir resolves to .planning/ in production; research/gswd/ is a subdirectory
1341
+ const gsdResearchDir = path.join(planningDir, 'research', 'gswd');
1342
+ if (!fs.existsSync(gsdResearchDir)) {
1343
+ fs.mkdirSync(gsdResearchDir, { recursive: true });
1344
+ }
1345
+ const summaryPath = path.join(gsdResearchDir, 'SUMMARY.md');
1346
+ safeWriteFile(summaryPath, generated.researchSummary);
1347
+ filesWritten.push(summaryPath);
1348
+
927
1349
  // Step 9: Update state: stage_status.compile = 'done', stage = 'compile'
928
1350
  updateStageStatus(statePath, 'compile', 'done');
929
1351
  const state = readState(statePath);
@@ -10,6 +10,7 @@
10
10
 
11
11
  import * as fs from 'node:fs';
12
12
  import type { StarterBrief } from './imagine-input.js';
13
+ import { extractHeadline } from './render.js';
13
14
 
14
15
  // ─── Types ───────────────────────────────────────────────────────────────────
15
16
 
@@ -30,6 +31,12 @@ export interface AgentResult {
30
31
 
31
32
  export type SpawnFn = (prompt: string) => Promise<string>;
32
33
 
34
+ export type OnAgentComplete = (result: {
35
+ agent: string;
36
+ headline: string;
37
+ status: 'complete' | 'failed';
38
+ }) => void;
39
+
33
40
  // ─── Agent Definitions ───────────────────────────────────────────────────────
34
41
 
35
42
  /**
@@ -76,7 +83,12 @@ export const IMAGINE_AGENTS: AgentDefinition[] = [
76
83
  *
77
84
  * Reads agent definition file and injects StarterBrief as context.
78
85
  */
79
- export function buildAgentPrompt(agent: AgentDefinition, brief: StarterBrief): string {
86
+ export function buildAgentPrompt(
87
+ agent: AgentDefinition,
88
+ brief: StarterBrief,
89
+ previousOutput?: string,
90
+ userFeedback?: string,
91
+ ): string {
80
92
  let definition: string;
81
93
  try {
82
94
  definition = fs.readFileSync(agent.definitionPath, 'utf-8');
@@ -86,13 +98,27 @@ export function buildAgentPrompt(agent: AgentDefinition, brief: StarterBrief): s
86
98
 
87
99
  const briefJson = JSON.stringify(brief, null, 2);
88
100
 
89
- return `${definition}
101
+ let prompt = `${definition}
90
102
 
91
103
  <starter_brief>
92
104
  ${briefJson}
93
- </starter_brief>
105
+ </starter_brief>`;
106
+
107
+ // Re-run augmentation (IMAGINE-03): include previous output + user feedback
108
+ if (previousOutput || userFeedback) {
109
+ prompt += '\n\n<rerun_context>';
110
+ if (previousOutput) {
111
+ prompt += `\n\n## Previous Output\nYour previous research produced the following findings. Build on these — do not start from scratch unless the user feedback explicitly asks for a different direction.\n\n${previousOutput}`;
112
+ }
113
+ if (userFeedback) {
114
+ prompt += `\n\n## User Feedback\nThe user was not satisfied with the previous directions and provided this feedback. Use it to refine your research:\n\n${userFeedback}`;
115
+ }
116
+ prompt += '\n</rerun_context>';
117
+ }
118
+
119
+ prompt += `\n\nProduce your output now. Include all required headings: ${agent.requiredHeadings.join(', ')}.`;
94
120
 
95
- Produce your output now. Include all required headings: ${agent.requiredHeadings.join(', ')}.`;
121
+ return prompt;
96
122
  }
97
123
 
98
124
  // ─── Orchestration ───────────────────────────────────────────────────────────
@@ -115,6 +141,9 @@ export async function orchestrateAgents(
115
141
  maxParallel: number,
116
142
  brief: StarterBrief,
117
143
  spawnFn: SpawnFn,
144
+ previousOutputs?: Record<string, string>,
145
+ userFeedback?: string,
146
+ onComplete?: OnAgentComplete,
118
147
  ): Promise<AgentResult[]> {
119
148
  const results: AgentResult[] = [];
120
149
 
@@ -126,23 +155,40 @@ export async function orchestrateAgents(
126
155
  const batchPromises = batch.map(async (agent): Promise<AgentResult> => {
127
156
  const start = Date.now();
128
157
  try {
129
- const prompt = buildAgentPrompt(agent, brief);
158
+ const previousOutput = previousOutputs?.[agent.name];
159
+ const prompt = buildAgentPrompt(agent, brief, previousOutput, userFeedback);
130
160
  const content = await spawnFn(prompt);
131
- return {
161
+ const result: AgentResult = {
132
162
  agent: agent.name,
133
163
  content,
134
164
  status: 'complete',
135
165
  duration_ms: Date.now() - start,
136
166
  };
167
+ if (onComplete) {
168
+ onComplete({
169
+ agent: agent.name,
170
+ headline: extractHeadline(result.content, agent.name),
171
+ status: result.status,
172
+ });
173
+ }
174
+ return result;
137
175
  } catch (err: unknown) {
138
176
  const message = err instanceof Error ? err.message : String(err);
139
- return {
177
+ const result: AgentResult = {
140
178
  agent: agent.name,
141
179
  content: '',
142
180
  status: 'failed',
143
181
  error: message,
144
182
  duration_ms: Date.now() - start,
145
183
  };
184
+ if (onComplete) {
185
+ onComplete({
186
+ agent: agent.name,
187
+ headline: message || 'Agent failed',
188
+ status: 'failed',
189
+ });
190
+ }
191
+ return result;
146
192
  }
147
193
  });
148
194