gswd 1.0.0 → 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 (86) hide show
  1. package/bin/gswd-tools.cjs +228 -0
  2. package/bin/install.js +8 -0
  3. package/commands/gswd/imagine.md +7 -1
  4. package/commands/gswd/start.md +507 -32
  5. package/dist/lib/audit.d.ts +205 -0
  6. package/dist/lib/audit.js +805 -0
  7. package/dist/lib/bootstrap.d.ts +103 -0
  8. package/dist/lib/bootstrap.js +563 -0
  9. package/dist/lib/compile.d.ts +239 -0
  10. package/dist/lib/compile.js +1152 -0
  11. package/dist/lib/config.d.ts +49 -0
  12. package/dist/lib/config.js +150 -0
  13. package/dist/lib/imagine-agents.d.ts +54 -0
  14. package/dist/lib/imagine-agents.js +185 -0
  15. package/dist/lib/imagine-gate.d.ts +47 -0
  16. package/dist/lib/imagine-gate.js +131 -0
  17. package/dist/lib/imagine-input.d.ts +46 -0
  18. package/dist/lib/imagine-input.js +233 -0
  19. package/dist/lib/imagine-synthesis.d.ts +90 -0
  20. package/dist/lib/imagine-synthesis.js +453 -0
  21. package/dist/lib/imagine.d.ts +56 -0
  22. package/dist/lib/imagine.js +413 -0
  23. package/dist/lib/intake.d.ts +27 -0
  24. package/dist/lib/intake.js +82 -0
  25. package/dist/lib/parse.d.ts +59 -0
  26. package/dist/lib/parse.js +171 -0
  27. package/dist/lib/render.d.ts +309 -0
  28. package/dist/lib/render.js +624 -0
  29. package/dist/lib/specify-agents.d.ts +120 -0
  30. package/dist/lib/specify-agents.js +269 -0
  31. package/dist/lib/specify-journeys.d.ts +124 -0
  32. package/dist/lib/specify-journeys.js +279 -0
  33. package/dist/lib/specify-nfr.d.ts +45 -0
  34. package/dist/lib/specify-nfr.js +159 -0
  35. package/dist/lib/specify-roles.d.ts +46 -0
  36. package/dist/lib/specify-roles.js +88 -0
  37. package/dist/lib/specify.d.ts +70 -0
  38. package/dist/lib/specify.js +676 -0
  39. package/dist/lib/state.d.ts +140 -0
  40. package/dist/lib/state.js +340 -0
  41. package/dist/tests/audit.test.d.ts +4 -0
  42. package/dist/tests/audit.test.js +1579 -0
  43. package/dist/tests/bootstrap.test.d.ts +5 -0
  44. package/dist/tests/bootstrap.test.js +611 -0
  45. package/dist/tests/compile.test.d.ts +4 -0
  46. package/dist/tests/compile.test.js +862 -0
  47. package/dist/tests/config.test.d.ts +4 -0
  48. package/dist/tests/config.test.js +191 -0
  49. package/dist/tests/imagine-agents.test.d.ts +6 -0
  50. package/dist/tests/imagine-agents.test.js +179 -0
  51. package/dist/tests/imagine-gate.test.d.ts +6 -0
  52. package/dist/tests/imagine-gate.test.js +264 -0
  53. package/dist/tests/imagine-input.test.d.ts +6 -0
  54. package/dist/tests/imagine-input.test.js +283 -0
  55. package/dist/tests/imagine-synthesis.test.d.ts +7 -0
  56. package/dist/tests/imagine-synthesis.test.js +380 -0
  57. package/dist/tests/imagine.test.d.ts +8 -0
  58. package/dist/tests/imagine.test.js +406 -0
  59. package/dist/tests/parse.test.d.ts +4 -0
  60. package/dist/tests/parse.test.js +285 -0
  61. package/dist/tests/render.test.d.ts +4 -0
  62. package/dist/tests/render.test.js +236 -0
  63. package/dist/tests/specify-agents.test.d.ts +4 -0
  64. package/dist/tests/specify-agents.test.js +352 -0
  65. package/dist/tests/specify-journeys.test.d.ts +5 -0
  66. package/dist/tests/specify-journeys.test.js +440 -0
  67. package/dist/tests/specify-nfr.test.d.ts +4 -0
  68. package/dist/tests/specify-nfr.test.js +205 -0
  69. package/dist/tests/specify-roles.test.d.ts +4 -0
  70. package/dist/tests/specify-roles.test.js +136 -0
  71. package/dist/tests/specify.test.d.ts +9 -0
  72. package/dist/tests/specify.test.js +544 -0
  73. package/dist/tests/state.test.d.ts +4 -0
  74. package/dist/tests/state.test.js +316 -0
  75. package/lib/bootstrap.ts +37 -11
  76. package/lib/compile.ts +426 -4
  77. package/lib/imagine-agents.ts +53 -7
  78. package/lib/imagine-synthesis.ts +170 -6
  79. package/lib/imagine.ts +59 -5
  80. package/lib/intake.ts +60 -0
  81. package/lib/parse.ts +2 -1
  82. package/lib/render.ts +566 -5
  83. package/lib/specify-agents.ts +25 -3
  84. package/lib/state.ts +115 -0
  85. package/package.json +4 -2
  86. package/templates/gswd/DECISIONS.template.md +3 -0
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import type { AgentResult } from './imagine-agents.js';
12
+ import type { StarterBrief } from './imagine-input.js';
12
13
 
13
14
  // ─── Types ───────────────────────────────────────────────────────────────────
14
15
 
@@ -19,8 +20,31 @@ export interface Direction {
19
20
  wedge: string; // MVP boundary / entry point
20
21
  differentiator: string; // What makes this unique
21
22
  risks: string[]; // Top 2-3 risks for this direction
23
+ rationale?: string[]; // Evidence-based rationale bullets (optional for backward compat)
22
24
  }
23
25
 
26
+ /**
27
+ * Research summary section for post-agent display.
28
+ * Produced by buildResearchSummary(), consumed by renderResearchSummary().
29
+ */
30
+ export interface ResearchSummarySection {
31
+ agentName: string; // Internal agent name (e.g., 'market-researcher')
32
+ displayName: string; // User-friendly name (e.g., 'Market Landscape')
33
+ takeaways: string[]; // 3-5 key bullet takeaways
34
+ bridge: string; // Bridging paragraph: "These findings shaped..."
35
+ }
36
+
37
+ /**
38
+ * User-friendly display names for imagine agents.
39
+ */
40
+ export const AGENT_DISPLAY_NAMES: Record<string, string> = {
41
+ 'market-researcher': 'Market Landscape',
42
+ 'icp-persona': 'Ideal Customer',
43
+ 'positioning': 'Positioning and GTM',
44
+ 'brainstorm-alternatives': 'Product Directions',
45
+ 'devils-advocate': 'Risks and Challenges',
46
+ };
47
+
24
48
  export interface AutoDecision {
25
49
  type: string; // 'icp' | 'wedge' | 'direction' | 'metric'
26
50
  chosen: string; // What was selected
@@ -135,6 +159,71 @@ function extractField(body: string, fieldName: string): string {
135
159
  return match ? match[1].trim() : '';
136
160
  }
137
161
 
162
+ /**
163
+ * Build evidence-based rationale bullets by cross-referencing agent content.
164
+ * Returns 2-4 bullets traceable to specific agent findings.
165
+ */
166
+ function buildRationale(
167
+ directionBody: string,
168
+ marketContent: string,
169
+ icpContent: string,
170
+ positioningContent: string,
171
+ ): string[] {
172
+ const rationale: string[] = [];
173
+
174
+ // Extract market evidence
175
+ if (marketContent) {
176
+ const marketMatch = marketContent.match(/##\s*Market (?:Overview|Gaps?)\s*\n+([\s\S]*?)(?=\n##|$)/);
177
+ if (marketMatch) {
178
+ const firstBullet = marketMatch[1].trim().split('\n')
179
+ .filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
180
+ .map(l => l.replace(/^[-*]\s*/, '').trim())
181
+ .find(l => l.length > 10);
182
+ if (firstBullet) {
183
+ rationale.push(`Market evidence: ${firstBullet}`);
184
+ }
185
+ }
186
+ }
187
+
188
+ // Extract ICP evidence
189
+ if (icpContent) {
190
+ const painMatch = icpContent.match(/##\s*Pain Points?\s*\n+([\s\S]*?)(?=\n##|$)/);
191
+ if (painMatch) {
192
+ const firstPain = painMatch[1].trim().split('\n')
193
+ .filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
194
+ .map(l => l.replace(/^[-*]\s*/, '').trim())
195
+ .find(l => l.length > 10);
196
+ if (firstPain) {
197
+ rationale.push(`ICP insight: ${firstPain}`);
198
+ }
199
+ }
200
+ }
201
+
202
+ // Extract positioning evidence
203
+ if (positioningContent) {
204
+ const vpMatch = positioningContent.match(/##\s*Value Proposition\s*\n+([\s\S]*?)(?=\n##|$)/);
205
+ if (vpMatch) {
206
+ const firstLine = vpMatch[1].trim().split('\n')[0];
207
+ if (firstLine && firstLine.length > 10) {
208
+ rationale.push(`Competitive advantage: ${firstLine}`);
209
+ }
210
+ }
211
+ }
212
+
213
+ // Extract direction-specific evidence from the body itself
214
+ const diffField = extractField(directionBody, 'Differentiator');
215
+ if (diffField && diffField.length > 10 && rationale.length < 4) {
216
+ rationale.push(`Direction differentiator: ${diffField}`);
217
+ }
218
+
219
+ // Ensure at least one rationale bullet
220
+ if (rationale.length === 0) {
221
+ rationale.push('Rationale unavailable — agent data incomplete');
222
+ }
223
+
224
+ return rationale;
225
+ }
226
+
138
227
  /**
139
228
  * Build a Direction from parsed section data, enriched with ICP and risk data.
140
229
  */
@@ -143,6 +232,8 @@ function buildDirection(
143
232
  body: string,
144
233
  icpContent: string,
145
234
  risksContent: string,
235
+ marketContent?: string,
236
+ positioningContent?: string,
146
237
  ): Direction {
147
238
  const icp = extractField(body, 'ICP') || extractIcpSummary(icpContent);
148
239
  const problem = extractField(body, 'Problem') || 'See agent research outputs';
@@ -150,8 +241,9 @@ function buildDirection(
150
241
  const differentiator = extractField(body, 'Differentiator') || 'To be refined';
151
242
  const riskField = extractField(body, 'Risk');
152
243
  const risks = riskField ? [riskField] : extractRiskList(risksContent);
244
+ const rationale = buildRationale(body, marketContent || '', icpContent, positioningContent || '');
153
245
 
154
- return { label, icp_summary: icp, problem_framing: problem, wedge, differentiator, risks };
246
+ return { label, icp_summary: icp, problem_framing: problem, wedge, differentiator, risks, rationale };
155
247
  }
156
248
 
157
249
  /**
@@ -220,15 +312,15 @@ export function synthesizeDirections(agentResults: AgentResult[]): SynthesisResu
220
312
  const sections = parseDirectionSections(brainstormContent);
221
313
 
222
314
  if (sections.length >= 3) {
223
- proposed = buildDirection(sections[0].label, sections[0].body, icpContent, risksContent);
315
+ proposed = buildDirection(sections[0].label, sections[0].body, icpContent, risksContent, marketContent, positioningContent);
224
316
  alternatives = [
225
- buildDirection(sections[1].label, sections[1].body, icpContent, risksContent),
226
- buildDirection(sections[2].label, sections[2].body, icpContent, risksContent),
317
+ buildDirection(sections[1].label, sections[1].body, icpContent, risksContent, marketContent, positioningContent),
318
+ buildDirection(sections[2].label, sections[2].body, icpContent, risksContent, marketContent, positioningContent),
227
319
  ];
228
320
  } else if (sections.length >= 1) {
229
321
  // Partial parse — use what we have
230
- proposed = buildDirection(sections[0].label, sections[0].body, icpContent, risksContent);
231
- alternatives = sections.slice(1).map(s => buildDirection(s.label, s.body, icpContent, risksContent));
322
+ proposed = buildDirection(sections[0].label, sections[0].body, icpContent, risksContent, marketContent, positioningContent);
323
+ alternatives = sections.slice(1).map(s => buildDirection(s.label, s.body, icpContent, risksContent, marketContent, positioningContent));
232
324
  // Fill missing alternatives with degraded entries
233
325
  while (alternatives.length < 2) {
234
326
  alternatives.push(buildDegradedDirection(alternatives.length + 2, icpContent, positioningContent, risksContent));
@@ -289,6 +381,7 @@ function buildDegradedDirection(
289
381
  wedge: 'To be defined',
290
382
  differentiator,
291
383
  risks,
384
+ rationale: ['Rationale unavailable — insufficient agent data'],
292
385
  };
293
386
  }
294
387
 
@@ -350,6 +443,77 @@ export function scoreIcpOptions(directions: Direction[]): ScoreResult {
350
443
  return { index: index >= 0 ? index : 0, scores };
351
444
  }
352
445
 
446
+ // ─── Research Summary ────────────────────────────────────────────────────────
447
+
448
+ /**
449
+ * Extract takeaway bullets from agent content.
450
+ * Looks for bullet items under headings, returns up to maxItems.
451
+ */
452
+ function extractTakeaways(content: string, maxItems: number = 5): string[] {
453
+ if (!content) return [];
454
+ const bullets: string[] = [];
455
+ const lines = content.split('\n');
456
+ for (const line of lines) {
457
+ const trimmed = line.trim();
458
+ if (/^[-*]\s+/.test(trimmed) && trimmed.length > 10) {
459
+ bullets.push(trimmed.replace(/^[-*]\s+/, ''));
460
+ if (bullets.length >= maxItems) break;
461
+ }
462
+ }
463
+ return bullets;
464
+ }
465
+
466
+ /**
467
+ * Build structured research summary from agent results.
468
+ * Each section: agent display name, 3-5 takeaway bullets, bridging paragraph.
469
+ * Product-contextualized using brief.vision and brief.target_user.
470
+ */
471
+ export function buildResearchSummary(
472
+ agentResults: AgentResult[],
473
+ brief: StarterBrief,
474
+ ): ResearchSummarySection[] {
475
+ const sections: ResearchSummarySection[] = [];
476
+
477
+ for (const result of agentResults) {
478
+ const displayName = AGENT_DISPLAY_NAMES[result.agent] || result.agent;
479
+
480
+ if (result.status === 'failed' || !result.content) {
481
+ sections.push({
482
+ agentName: result.agent,
483
+ displayName,
484
+ takeaways: ['Agent failed — no findings available'],
485
+ bridge: `These findings could not shape the directions below due to agent failure.`,
486
+ });
487
+ continue;
488
+ }
489
+
490
+ const takeaways = extractTakeaways(result.content);
491
+ // Ensure at least 3 takeaways; pad with content excerpts if needed
492
+ if (takeaways.length < 3) {
493
+ const lines = result.content.split('\n')
494
+ .filter(l => l.trim().length > 20 && !l.trim().startsWith('#'))
495
+ .slice(0, 3 - takeaways.length);
496
+ for (const line of lines) {
497
+ takeaways.push(line.trim().slice(0, 150));
498
+ }
499
+ }
500
+
501
+ // Build product-contextualized bridge paragraph
502
+ const userCtx = brief.target_user || 'target users';
503
+ const visionCtx = brief.vision || 'the product';
504
+ const bridge = `These findings shaped the directions below by revealing how ${visionCtx} can best serve ${userCtx} in this domain.`;
505
+
506
+ sections.push({
507
+ agentName: result.agent,
508
+ displayName,
509
+ takeaways: takeaways.slice(0, 5),
510
+ bridge,
511
+ });
512
+ }
513
+
514
+ return sections;
515
+ }
516
+
353
517
  // ─── Auto Selection ─────────────────────────────────────────────────────────
354
518
 
355
519
  /**
package/lib/imagine.ts CHANGED
@@ -20,8 +20,8 @@ import { parseIdeaFile, buildFromIntake, validateBrief } from './imagine-input.j
20
20
  import type { StarterBrief, IntakeAnswers } from './imagine-input.js';
21
21
  import { IMAGINE_AGENTS, orchestrateAgents } from './imagine-agents.js';
22
22
  import type { AgentResult, SpawnFn } from './imagine-agents.js';
23
- import { synthesizeDirections, autoSelectDirection, scoreIcpOptions } from './imagine-synthesis.js';
24
- import type { Direction, AutoDecision, SynthesisResult } from './imagine-synthesis.js';
23
+ import { synthesizeDirections, autoSelectDirection, scoreIcpOptions, buildResearchSummary } from './imagine-synthesis.js';
24
+ import type { Direction, AutoDecision, SynthesisResult, ResearchSummarySection } from './imagine-synthesis.js';
25
25
  import { validateDecisionGate } from './imagine-gate.js';
26
26
  import type { GateResult } from './imagine-gate.js';
27
27
 
@@ -36,6 +36,9 @@ export interface ImagineOptions {
36
36
  configPath?: string; // Override config.json path (for testing)
37
37
  spawnFn?: SpawnFn; // Task() wrapper (injectable for testing)
38
38
  selectedDirectionIndex?: number; // Manual direction selection (0-based)
39
+ previousAgentOutputs?: Record<string, string>; // For re-run augmentation (IMAGINE-03)
40
+ userFeedback?: string; // For re-run augmentation (IMAGINE-03)
41
+ visionStatement?: string; // Pre-approved vision (from Accept/Edit flow)
39
42
  }
40
43
 
41
44
  export interface ImagineResult {
@@ -44,6 +47,9 @@ export interface ImagineResult {
44
47
  gate_result?: GateResult;
45
48
  auto_decisions?: AutoDecision[];
46
49
  selected_direction?: Direction;
50
+ research_summary?: ResearchSummarySection[]; // Structured per-agent summary (IMAGINE-01)
51
+ vision_statement?: string; // Frozen vision statement (IMAGINE-05)
52
+ agent_headlines?: Array<{ agent: string; headline: string; status: 'complete' | 'failed' }>; // Per-agent completion headlines (COMM-03)
47
53
  error?: string;
48
54
  }
49
55
 
@@ -56,6 +62,7 @@ function buildImagineContent(
56
62
  brief: StarterBrief,
57
63
  selected: Direction,
58
64
  synthesis: SynthesisResult,
65
+ visionStatement: string,
59
66
  autoDecisions?: AutoDecision[],
60
67
  ): string {
61
68
  const alternatives = synthesis.alternatives
@@ -69,7 +76,7 @@ function buildImagineContent(
69
76
  return `# Imagine
70
77
 
71
78
  ## Vision
72
- ${brief.vision}
79
+ ${visionStatement}
73
80
 
74
81
  ## Target User
75
82
  ${brief.target_user}
@@ -116,6 +123,7 @@ function buildDecisionsContent(
116
123
  selected: Direction,
117
124
  synthesis: SynthesisResult,
118
125
  brief: StarterBrief,
126
+ visionStatement: string,
119
127
  autoDecisions?: AutoDecision[],
120
128
  ): string {
121
129
  // Build frozen decisions (need >= 8)
@@ -156,6 +164,9 @@ function buildDecisionsContent(
156
164
 
157
165
  return `# Decisions
158
166
 
167
+ ## Vision
168
+ ${visionStatement}
169
+
159
170
  ## Frozen Decisions
160
171
  ${frozenDecisions.join('\n')}
161
172
 
@@ -223,6 +234,23 @@ function buildAgentArtifact(agentName: string, rawOutputs: Record<string, string
223
234
  return content || fallback;
224
235
  }
225
236
 
237
+ // ─── Vision Synthesis ───────────────────────────────────────────────────────
238
+
239
+ /**
240
+ * Synthesize an aspirational vision statement from brief and selected direction.
241
+ * Vision = future state the product enables, NOT product description.
242
+ * Example: "A world where solo founders never build the wrong thing because
243
+ * their specification process is as rigorous as their code."
244
+ *
245
+ * This is a template-based synthesis; in manual mode, the LLM in start.md
246
+ * will generate a richer vision and pass it via options.visionStatement.
247
+ */
248
+ function synthesizeVisionStatement(brief: StarterBrief, selected: Direction): string {
249
+ const userContext = brief.target_user || 'users';
250
+ const problemContext = selected.problem_framing || brief.vision;
251
+ return `A world where ${userContext} achieve ${problemContext} effortlessly — because the right tools make clarity the default, not the exception.`;
252
+ }
253
+
226
254
  // ─── Main Workflow ──────────────────────────────────────────────────────────
227
255
 
228
256
  /**
@@ -282,6 +310,7 @@ export async function runImagine(options: ImagineOptions): Promise<ImagineResult
282
310
 
283
311
  // ── Step 3: Spawn agents ──────────────────────────────────────────
284
312
  let agentResults: AgentResult[];
313
+ const agentHeadlines: Array<{ agent: string; headline: string; status: 'complete' | 'failed' }> = [];
285
314
 
286
315
  if (options.skipResearch) {
287
316
  // Create minimal results from brief content
@@ -312,9 +341,15 @@ export async function runImagine(options: ImagineOptions): Promise<ImagineResult
312
341
  config.max_parallel_agents,
313
342
  brief,
314
343
  spawnFn,
344
+ options.previousAgentOutputs,
345
+ options.userFeedback,
346
+ (result) => { agentHeadlines.push(result); },
315
347
  );
316
348
  }
317
349
 
350
+ // Build research summary (IMAGINE-01)
351
+ const researchSummary = buildResearchSummary(agentResults, brief);
352
+
318
353
  // Mid-stage checkpoint: agents complete (RESM-02)
319
354
  writeCheckpoint(statePath, 'gswd/imagine', 'agents-complete');
320
355
 
@@ -341,9 +376,19 @@ export async function runImagine(options: ImagineOptions): Promise<ImagineResult
341
376
  // Mid-stage checkpoint: direction selected (RESM-02)
342
377
  writeCheckpoint(statePath, 'gswd/imagine', 'direction-selected');
343
378
 
379
+ // ── Step 5.5: Synthesize vision statement (IMAGINE-05) ───────────
380
+ let visionStatement: string;
381
+ if (options.visionStatement) {
382
+ // User already approved/edited a vision statement (manual mode)
383
+ visionStatement = options.visionStatement;
384
+ } else {
385
+ // Auto-generate from brief + selected direction
386
+ visionStatement = synthesizeVisionStatement(brief, selected);
387
+ }
388
+
344
389
  // ── Step 6: Build artifacts ──────────────────────────────────────
345
- const imagineContent = buildImagineContent(brief, selected, synthesis, autoDecisions);
346
- const decisionsContent = buildDecisionsContent(selected, synthesis, brief, autoDecisions);
390
+ const imagineContent = buildImagineContent(brief, selected, synthesis, visionStatement, autoDecisions);
391
+ const decisionsContent = buildDecisionsContent(selected, synthesis, brief, visionStatement, autoDecisions);
347
392
 
348
393
  const icpContent = buildAgentArtifact(
349
394
  'icp-persona',
@@ -378,6 +423,9 @@ export async function runImagine(options: ImagineOptions): Promise<ImagineResult
378
423
  }
379
424
 
380
425
  // ── Step 8: Write artifacts ──────────────────────────────────────
426
+ const directionsContent = synthesis.raw_agent_outputs['brainstorm-alternatives'] || '';
427
+ const risksRawContent = synthesis.raw_agent_outputs['devils-advocate'] || '';
428
+
381
429
  const artifacts: { name: string; content: string }[] = [
382
430
  { name: 'IMAGINE.md', content: imagineContent },
383
431
  { name: 'ICP.md', content: icpContent },
@@ -386,6 +434,9 @@ export async function runImagine(options: ImagineOptions): Promise<ImagineResult
386
434
  { name: 'DECISIONS.md', content: decisionsContent },
387
435
  ];
388
436
 
437
+ if (directionsContent) artifacts.push({ name: 'DIRECTIONS.md', content: directionsContent });
438
+ if (risksRawContent) artifacts.push({ name: 'RISKS.md', content: risksRawContent });
439
+
389
440
  const artifactsWritten: string[] = [];
390
441
 
391
442
  for (const artifact of artifacts) {
@@ -419,6 +470,9 @@ export async function runImagine(options: ImagineOptions): Promise<ImagineResult
419
470
  gate_result: gateResult,
420
471
  auto_decisions: autoDecisions,
421
472
  selected_direction: selected,
473
+ research_summary: researchSummary,
474
+ vision_statement: visionStatement,
475
+ agent_headlines: agentHeadlines.length > 0 ? agentHeadlines : undefined,
422
476
  };
423
477
  } catch (err: unknown) {
424
478
  const message = err instanceof Error ? err.message : String(err);
package/lib/intake.ts ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * GSWD Intake Module — INTAKE.json persistence for idempotent resume
3
+ *
4
+ * Writes/reads the user's confirmed product description to .planning/gswd/INTAKE.json.
5
+ * Uses atomic write pattern from state.ts (safeWriteJson).
6
+ *
7
+ * INTAKE.json is separate from intake.md:
8
+ * - intake.md: text file passed to bootstrap CLI (imagine agents)
9
+ * - INTAKE.json: machine-readable state for resume
10
+ */
11
+
12
+ import * as path from 'node:path';
13
+ import * as fs from 'node:fs';
14
+ import { safeWriteJson } from './state.js';
15
+
16
+ // ─── Types ───────────────────────────────────────────────────────────────────
17
+
18
+ export interface IntakeData {
19
+ version: 1;
20
+ recorded_at: string;
21
+ product_description: string;
22
+ source: 'interactive' | 'file';
23
+ idea_file?: string;
24
+ }
25
+
26
+ // ─── CRUD ────────────────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Write INTAKE.json atomically to the given GSWD directory.
30
+ * Persistence timing: called once after user confirms their product description
31
+ * (the "Did I get that right?" moment in start.md).
32
+ */
33
+ export function writeIntake(
34
+ gswdDir: string,
35
+ data: Omit<IntakeData, 'version' | 'recorded_at'>
36
+ ): IntakeData {
37
+ const intake: IntakeData = {
38
+ version: 1,
39
+ recorded_at: new Date().toISOString(),
40
+ ...data,
41
+ };
42
+ const intakePath = path.join(gswdDir, 'INTAKE.json');
43
+ safeWriteJson(intakePath, intake);
44
+ return intake;
45
+ }
46
+
47
+ /**
48
+ * Read INTAKE.json. Returns null if missing or corrupt.
49
+ */
50
+ export function readIntake(gswdDir: string): IntakeData | null {
51
+ const intakePath = path.join(gswdDir, 'INTAKE.json');
52
+ try {
53
+ const content = fs.readFileSync(intakePath, 'utf-8');
54
+ const parsed = JSON.parse(content);
55
+ if (typeof parsed.product_description !== 'string') return null;
56
+ return parsed as IntakeData;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
package/lib/parse.ts CHANGED
@@ -20,6 +20,7 @@ export const REQUIRED_HEADINGS: Record<string, string[]> = {
20
20
  'INTEGRATIONS.md': ['## Integrations'],
21
21
  'ARCHITECTURE.md': ['## Architecture', '### Components', '### Data Model'],
22
22
  'DECISIONS.md': [
23
+ '## Vision',
23
24
  '## Frozen Decisions',
24
25
  '## Success Metrics',
25
26
  '## Out of Scope',
@@ -27,7 +28,7 @@ export const REQUIRED_HEADINGS: Record<string, string[]> = {
27
28
  '## Open Questions',
28
29
  ],
29
30
  'AUDIT.md': ['## Coverage Matrix', '## Check Results'],
30
- 'PROJECT.md': ['## What This Is', '## Target User', '## Problem Statement', '## Wedge / MVP Boundary', '## Success Metrics', '## Out of Scope'],
31
+ 'PROJECT.md': ['## What This Is', '## Target User', '## Problem Statement', '## Wedge / MVP Boundary', '## Success Metrics', '## Out of Scope', '## Context'],
31
32
  'REQUIREMENTS.md': ['## Functional Requirements', '## Non-Functional Requirements', '## Traceability'],
32
33
  'ROADMAP.md': ['## Overview', '## Phases'],
33
34
  'STATE.md': ['## Frozen Decisions', '## Approvals', '## Open Questions', '## Risks'],