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
@@ -0,0 +1,624 @@
1
+ "use strict";
2
+ /**
3
+ * GSWD Render Module — Terminal UI rendering functions
4
+ *
5
+ * All functions return strings (not print to stdout).
6
+ * Zero external dependencies. Uses only string operations.
7
+ *
8
+ * Schema: GSWD_SPEC.md Section 7.1-7.3
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.SPECIFY_AGENT_BRIEFINGS = exports.IMAGINE_AGENT_BRIEFINGS = exports.STAGE_CONTEXT_TEMPLATES = exports.COLORS = exports.SYMBOLS = void 0;
45
+ exports.colorize = colorize;
46
+ exports.renderLogo = renderLogo;
47
+ exports.renderFirstRunGuide = renderFirstRunGuide;
48
+ exports.padRight = padRight;
49
+ exports.renderBanner = renderBanner;
50
+ exports.renderCheckpoint = renderCheckpoint;
51
+ exports.renderNextUp = renderNextUp;
52
+ exports.renderStatusLine = renderStatusLine;
53
+ exports.renderProgressBar = renderProgressBar;
54
+ exports.renderResearchSummary = renderResearchSummary;
55
+ exports.renderDirectionCard = renderDirectionCard;
56
+ exports.renderStageSummary = renderStageSummary;
57
+ exports.extractProductContext = extractProductContext;
58
+ exports.renderContextBanner = renderContextBanner;
59
+ exports.renderAgentBriefing = renderAgentBriefing;
60
+ exports.renderAgentCompletion = renderAgentCompletion;
61
+ exports.renderAgentProgress = renderAgentProgress;
62
+ exports.renderFileInventory = renderFileInventory;
63
+ exports.extractHeadline = extractHeadline;
64
+ const fs = __importStar(require("node:fs"));
65
+ const path = __importStar(require("node:path"));
66
+ // ─── Status Symbols ──────────────────────────────────────────────────────────
67
+ exports.SYMBOLS = {
68
+ complete: '\u2713', // ✓
69
+ failed: '\u2717', // ✗
70
+ inProgress: '\u25C6', // ◆
71
+ pending: '\u25CB', // ○
72
+ autoApproved: '\u26A1', // ⚡
73
+ warning: '\u26A0', // ⚠
74
+ };
75
+ // ─── Colors ──────────────────────────────────────────────────────────────────
76
+ /**
77
+ * ANSI color codes for GSWD branding.
78
+ * Electric orange/amber is the brand color (per CONTEXT.md locked decision).
79
+ * 256-color code \x1b[38;5;208m; dim uses \x1b[2m.
80
+ */
81
+ exports.COLORS = {
82
+ orange: '\x1b[38;5;208m',
83
+ dim: '\x1b[2m',
84
+ reset: '\x1b[0m',
85
+ };
86
+ /**
87
+ * Wrap text in ANSI color. No-op when stdout is not a TTY (CI/Docker/pipe safety).
88
+ * Respects FORCE_COLOR env var to override TTY detection.
89
+ */
90
+ function colorize(text, color) {
91
+ if (!process.stdout.isTTY && !process.env.FORCE_COLOR) {
92
+ return text;
93
+ }
94
+ return `${exports.COLORS[color]}${text}${exports.COLORS.reset}`;
95
+ }
96
+ // ─── Logo ────────────────────────────────────────────────────────────────────
97
+ /**
98
+ * Render the GSWD ASCII logo with catchphrase.
99
+ * Logo shape matches GSD's block-letter style from install.js.
100
+ * Static string — no figlet (STATE.md architectural decision).
101
+ *
102
+ * @param version - Optional version string (e.g., "1.0.1")
103
+ */
104
+ function renderLogo(version) {
105
+ const lines = [
106
+ ' \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ',
107
+ ' \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557',
108
+ ' \u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551',
109
+ ' \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551',
110
+ ' \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D',
111
+ ' \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D ',
112
+ ].join('\n');
113
+ const coloredLogo = colorize(lines, 'orange');
114
+ const tagline = ' Spec clarity before code.';
115
+ const versionStr = version ? colorize(` v${version}`, 'dim') : '';
116
+ return [coloredLogo, '', tagline, versionStr].filter(Boolean).join('\n');
117
+ }
118
+ // ─── First Run Guide ─────────────────────────────────────────────────────────
119
+ /**
120
+ * Render the condensed first-run command guide.
121
+ * Per CONTEXT.md: primary commands only, no descriptions.
122
+ * Followed by automatic /gswd:start proposal for zero-friction onboarding.
123
+ */
124
+ function renderFirstRunGuide() {
125
+ const commands = [
126
+ '/gswd:start',
127
+ '/gswd:imagine',
128
+ '/gswd:specify',
129
+ '/gswd:compile',
130
+ ];
131
+ const lines = [
132
+ 'Commands:',
133
+ '',
134
+ ...commands.map(cmd => ` ${colorize(cmd, 'orange')}`),
135
+ '',
136
+ 'Run /gswd:start to begin.',
137
+ ];
138
+ return lines.join('\n');
139
+ }
140
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
141
+ /**
142
+ * Pad or truncate a string to exact width.
143
+ * If str exceeds width, truncate and append '...' (total = width).
144
+ */
145
+ function padRight(str, width) {
146
+ if (str.length > width) {
147
+ return str.slice(0, width - 3) + '...';
148
+ }
149
+ return str + ' '.repeat(width - str.length);
150
+ }
151
+ // ─── Banner ──────────────────────────────────────────────────────────────────
152
+ /**
153
+ * Render a stage banner matching GSWD_SPEC Section 7.1.
154
+ *
155
+ * ```
156
+ * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
157
+ * GSWD ► {STAGE NAME}
158
+ * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
159
+ * ```
160
+ *
161
+ * Banner width = 55 characters. Stage name is uppercased.
162
+ */
163
+ function renderBanner(stageName) {
164
+ const BANNER_WIDTH = 55;
165
+ const border = colorize('\u2501'.repeat(BANNER_WIDTH), 'orange');
166
+ const title = ` GSWD \u25B6 ${stageName.toUpperCase()}`;
167
+ return `${border}\n${title}\n${border}`;
168
+ }
169
+ // ─── Checkpoint Box ──────────────────────────────────────────────────────────
170
+ /**
171
+ * Render a checkpoint box matching GSWD_SPEC Section 7.2.
172
+ *
173
+ * Box total width: 56 characters (54 inner + 2 border characters).
174
+ * Uses single-line box-drawing characters.
175
+ *
176
+ * @param type - Header text (e.g., "Decision Required")
177
+ * @param context - Context paragraph
178
+ * @param options - Numbered options list
179
+ * @param actionPrompt - Action prompt (appears after → )
180
+ */
181
+ function renderCheckpoint(type, context, options, actionPrompt) {
182
+ const BOX_WIDTH = 56;
183
+ const INNER_WIDTH = BOX_WIDTH - 4; // 52 (accounting for "│ " and " │")
184
+ const topBorder = colorize(`\u250C${'\u2500'.repeat(BOX_WIDTH - 2)}\u2510`, 'orange');
185
+ const midBorder = colorize(`\u251C${'\u2500'.repeat(BOX_WIDTH - 2)}\u2524`, 'orange');
186
+ const bottomBorder = colorize(`\u2514${'\u2500'.repeat(BOX_WIDTH - 2)}\u2518`, 'orange');
187
+ const line = (text) => {
188
+ const padded = padRight(text, INNER_WIDTH);
189
+ return `${colorize('\u2502', 'orange')} ${padded} ${colorize('\u2502', 'orange')}`;
190
+ };
191
+ const blankLine = line('');
192
+ const lines = [];
193
+ lines.push(topBorder);
194
+ lines.push(line(type));
195
+ lines.push(midBorder);
196
+ lines.push(line(context));
197
+ lines.push(blankLine);
198
+ for (let i = 0; i < options.length; i++) {
199
+ lines.push(line(`${i + 1}) ${options[i]}`));
200
+ }
201
+ lines.push(blankLine);
202
+ lines.push(line(`\u2192 ${actionPrompt}`));
203
+ lines.push(bottomBorder);
204
+ return lines.join('\n');
205
+ }
206
+ // ─── Next Up ─────────────────────────────────────────────────────────────────
207
+ /**
208
+ * Render a Next Up block matching GSWD_SPEC Section 7.3.
209
+ *
210
+ * ```
211
+ * ✓ Wrote: file1, file2, ...
212
+ *
213
+ * Next up:
214
+ * {nextCommand}
215
+ *
216
+ * Tip:
217
+ * If context is getting crowded, run /clear.
218
+ * ```
219
+ */
220
+ function renderNextUp(files, nextCommand, alsoAvailable) {
221
+ const parts = [];
222
+ parts.push(`${exports.SYMBOLS.complete} Wrote: ${files.join(', ')}`);
223
+ parts.push('');
224
+ parts.push('Next up:');
225
+ parts.push(` ${nextCommand}`);
226
+ parts.push('');
227
+ parts.push('Tip:');
228
+ parts.push(' If context is getting crowded, run /clear.');
229
+ if (alsoAvailable && alsoAvailable.length > 0) {
230
+ parts.push('');
231
+ parts.push('Also available:');
232
+ for (const item of alsoAvailable) {
233
+ parts.push(` - ${item}`);
234
+ }
235
+ }
236
+ return parts.join('\n');
237
+ }
238
+ // ─── Status Line ─────────────────────────────────────────────────────────────
239
+ /**
240
+ * Render a single status line with the appropriate symbol.
241
+ *
242
+ * Maps: done/pass -> ✓, fail -> ✗, in_progress -> ◆, not_started -> ○
243
+ */
244
+ function renderStatusLine(stage, status) {
245
+ let symbol;
246
+ switch (status) {
247
+ case 'done':
248
+ case 'pass':
249
+ symbol = exports.SYMBOLS.complete;
250
+ break;
251
+ case 'fail':
252
+ symbol = exports.SYMBOLS.failed;
253
+ break;
254
+ case 'in_progress':
255
+ symbol = exports.SYMBOLS.inProgress;
256
+ break;
257
+ case 'not_started':
258
+ symbol = exports.SYMBOLS.pending;
259
+ break;
260
+ default:
261
+ symbol = exports.SYMBOLS.pending;
262
+ }
263
+ return `${symbol} ${stage}: ${status}`;
264
+ }
265
+ // ─── Progress Bar ────────────────────────────────────────────────────────────
266
+ /**
267
+ * Render an ASCII progress bar.
268
+ *
269
+ * ```
270
+ * Progress: ████████░░ 80%
271
+ * ```
272
+ *
273
+ * @param current - Current value
274
+ * @param total - Total value
275
+ * @param width - Bar width in characters (default 10)
276
+ */
277
+ function renderProgressBar(current, total, width = 10) {
278
+ const ratio = total === 0 ? 0 : Math.min(current / total, 1);
279
+ const filled = Math.round(ratio * width);
280
+ const empty = width - filled;
281
+ const percent = Math.round(ratio * 100);
282
+ const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(empty);
283
+ return `Progress: ${bar} ${percent}%`;
284
+ }
285
+ /**
286
+ * Render structured research summary after agent completion.
287
+ * Product-contextualized: section intros reference user's product.
288
+ * Per CONTEXT.md: 3-5 takeaway bullets per section, bridging paragraph per section.
289
+ *
290
+ * @param sections - Research summary sections from buildResearchSummary
291
+ * @param productContext - Brief description of the product (from brief.vision)
292
+ * @param targetUser - Who the product is for (from brief.target_user)
293
+ */
294
+ function renderResearchSummary(sections, productContext, targetUser) {
295
+ const parts = [];
296
+ parts.push('## Research Summary');
297
+ parts.push('');
298
+ for (const section of sections) {
299
+ // Product-contextualized intro
300
+ parts.push(`### ${section.displayName}`);
301
+ parts.push(`*For a product helping ${targetUser}:*`);
302
+ parts.push('');
303
+ // Takeaway bullets (3-5)
304
+ for (const takeaway of section.takeaways) {
305
+ parts.push(`- ${takeaway}`);
306
+ }
307
+ parts.push('');
308
+ // Bridging paragraph
309
+ parts.push(section.bridge);
310
+ parts.push('');
311
+ }
312
+ return parts.join('\n');
313
+ }
314
+ /**
315
+ * Render a direction card with "Why this makes sense" rationale section.
316
+ * Per CONTEXT.md: rationale section comes FIRST, before existing fields.
317
+ * Recommended direction gets a visual marker.
318
+ *
319
+ * @param direction - Direction data
320
+ * @param index - 0-based index for display numbering
321
+ * @param isRecommended - Whether this is the proposed/recommended direction
322
+ */
323
+ function renderDirectionCard(direction, index, isRecommended) {
324
+ const marker = isRecommended ? ' **(Recommended)**' : '';
325
+ const parts = [];
326
+ parts.push(`### ${direction.label}${marker}`);
327
+ parts.push('');
328
+ // Rationale section FIRST per CONTEXT.md locked decision
329
+ if (direction.rationale.length > 0) {
330
+ parts.push('**Why this makes sense:**');
331
+ for (const r of direction.rationale) {
332
+ parts.push(`- ${r}`);
333
+ }
334
+ parts.push('');
335
+ }
336
+ // Existing fields
337
+ parts.push(`**ICP:** ${direction.icp_summary}`);
338
+ parts.push(`**Problem:** ${direction.problem_framing}`);
339
+ parts.push(`**Wedge:** ${direction.wedge}`);
340
+ parts.push(`**Differentiator:** ${direction.differentiator}`);
341
+ parts.push(`**Risks:** ${direction.risks.join('; ')}`);
342
+ return parts.join('\n');
343
+ }
344
+ // ─── Stage Summary ───────────────────────────────────────────────────────────
345
+ function renderStageSummary(input) {
346
+ const parts = [];
347
+ // Headline
348
+ parts.push(`## ${input.stageName} Complete`);
349
+ parts.push('');
350
+ parts.push(input.headline);
351
+ parts.push('');
352
+ // Key outputs (section summaries)
353
+ if (input.sections.length > 0) {
354
+ parts.push('**Key outputs:**');
355
+ for (const section of input.sections) {
356
+ parts.push(`- **${section.name}:** ${section.summary}`);
357
+ }
358
+ parts.push('');
359
+ }
360
+ // Files created
361
+ if (input.filesCreated.length > 0) {
362
+ parts.push(`**Created:** ${input.filesCreated.join(', ')}`);
363
+ }
364
+ return parts.join('\n');
365
+ }
366
+ /**
367
+ * Extract product context from DECISIONS.md and INTAKE.json.
368
+ * DECISIONS.md is preferred (more precise, available after Imagine).
369
+ * INTAKE.json is fallback (available from pipeline start).
370
+ * Returns safe defaults if neither file exists.
371
+ *
372
+ * @param planningDir - Path to .planning/ directory
373
+ * @param isRerun - Whether this is a re-run
374
+ */
375
+ function extractProductContext(planningDir, isRerun = false) {
376
+ let domain = 'your product';
377
+ let targetUser = 'your users';
378
+ // Try DECISIONS.md first (more precise, available after Imagine)
379
+ try {
380
+ const decisionsPath = path.join(planningDir, 'DECISIONS.md');
381
+ const decisions = fs.readFileSync(decisionsPath, 'utf-8');
382
+ const targetMatch = decisions.match(/\*\*Target User:\*\*\s*(?:Auto-chosen:\s*)?(.+)/);
383
+ if (targetMatch)
384
+ targetUser = targetMatch[1].trim();
385
+ const directionMatch = decisions.match(/\*\*Product Direction:\*\*\s*(?:Auto-chosen:\s*)?(.+)/);
386
+ if (directionMatch)
387
+ domain = directionMatch[1].trim();
388
+ }
389
+ catch {
390
+ // Fall through to INTAKE.json
391
+ }
392
+ // Fallback to INTAKE.json (available from pipeline start)
393
+ if (domain === 'your product') {
394
+ try {
395
+ const gswdDir = path.join(planningDir, 'gswd');
396
+ const intakePath = path.join(gswdDir, 'INTAKE.json');
397
+ const intake = JSON.parse(fs.readFileSync(intakePath, 'utf-8'));
398
+ if (intake.product_description) {
399
+ // Use first sentence as domain
400
+ const firstSentence = intake.product_description.split('.')[0];
401
+ if (firstSentence && firstSentence.trim().length > 0) {
402
+ domain = firstSentence.trim();
403
+ }
404
+ }
405
+ }
406
+ catch {
407
+ // Use defaults
408
+ }
409
+ }
410
+ return { domain, targetUser, stageContext: '', isRerun, rerunContext: undefined };
411
+ }
412
+ // ─── Stage Context Templates ────────────────────────────────────────────────
413
+ /**
414
+ * Stage-specific context line generators for contextual banners.
415
+ * Each returns a product-contextualized one-liner for the banner.
416
+ * Compile is deliberately excluded (per CONTEXT.md: deterministic, no banner needed).
417
+ */
418
+ exports.STAGE_CONTEXT_TEMPLATES = {
419
+ imagine: (ctx) => ctx.isRerun
420
+ ? `Refining directions based on your feedback${ctx.rerunContext ? ` about ${ctx.rerunContext}` : ''}`
421
+ : `Validating product direction for ${ctx.domain} targeting ${ctx.targetUser}`,
422
+ specify: (ctx) => `Building the execution-grade spec: journeys, requirements, and architecture for ${ctx.domain}`,
423
+ audit: (ctx) => `Verifying spec completeness \u2014 every journey, requirement, and edge case for ${ctx.domain} must trace`,
424
+ };
425
+ // ─── Agent Briefing Templates ───────────────────────────────────────────────
426
+ /**
427
+ * Product-contextualized one-liner descriptions for Imagine research agents.
428
+ * Each function receives ProductContext and returns a purpose string.
429
+ */
430
+ exports.IMAGINE_AGENT_BRIEFINGS = {
431
+ 'market-researcher': (ctx) => `mapping the ${ctx.domain} landscape to find gaps`,
432
+ 'icp-persona': (ctx) => `profiling ${ctx.targetUser} who need ${ctx.domain}`,
433
+ 'positioning': (ctx) => `finding the angle that makes ${ctx.domain} obviously necessary`,
434
+ 'brainstorm-alternatives': (ctx) => `exploring 3 distinct product directions for ${ctx.domain}`,
435
+ 'devils-advocate': (ctx) => `stress-testing assumptions before you commit to a direction`,
436
+ };
437
+ /**
438
+ * Product-contextualized one-liner descriptions for Specify agents.
439
+ */
440
+ exports.SPECIFY_AGENT_BRIEFINGS = {
441
+ 'journey-mapper': (ctx) => `mapping user journeys for ${ctx.targetUser} through ${ctx.domain}`,
442
+ 'architecture-drafter': (ctx) => `designing the system architecture for ${ctx.domain}`,
443
+ 'integrations-checker': (ctx) => `evaluating external services ${ctx.domain} needs to connect with`,
444
+ };
445
+ // ─── Contextual Banner ──────────────────────────────────────────────────────
446
+ /**
447
+ * Render a stage banner with a product-contextualized line below the title.
448
+ * Extension of renderBanner() for consultant-style messaging (Phase 16).
449
+ *
450
+ * ```
451
+ * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
452
+ * GSWD ► IMAGINE
453
+ * Validating product direction for a spec-writing tool targeting founders
454
+ * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
455
+ * ```
456
+ *
457
+ * @param stageName - Stage name (will be uppercased)
458
+ * @param contextLine - Product-contextualized one-liner
459
+ */
460
+ function renderContextBanner(stageName, contextLine) {
461
+ const BANNER_WIDTH = 55;
462
+ const border = colorize('\u2501'.repeat(BANNER_WIDTH), 'orange');
463
+ const title = ` GSWD \u25B6 ${stageName.toUpperCase()}`;
464
+ const context = ` ${contextLine}`;
465
+ return `${border}\n${title}\n${context}\n${border}`;
466
+ }
467
+ /**
468
+ * Render agent pre-briefing block before spawning.
469
+ * Technical transparency: name each agent explicitly with product-specific purpose.
470
+ *
471
+ * ```
472
+ * ◆ Spawning 5 research agents:
473
+ * → market-researcher: mapping the spec-tooling landscape to find gaps
474
+ * → icp-persona: profiling founders who struggle with product specification
475
+ * ```
476
+ *
477
+ * @param agents - Array of agent names with product-contextualized purposes
478
+ * @param stageContext - Optional stage context description
479
+ */
480
+ function renderAgentBriefing(agents, stageContext) {
481
+ const parts = [];
482
+ parts.push(`${exports.SYMBOLS.inProgress} Spawning ${agents.length} research agents:`);
483
+ for (const agent of agents) {
484
+ parts.push(` \u2192 ${agent.name}: ${agent.productPurpose}`);
485
+ }
486
+ if (stageContext) {
487
+ parts.push('');
488
+ parts.push(stageContext);
489
+ }
490
+ return parts.join('\n');
491
+ }
492
+ // ─── Agent Completion ───────────────────────────────────────────────────────
493
+ /**
494
+ * Render a single agent completion line with its key finding headline.
495
+ *
496
+ * ✓ market-researcher: 3 direct competitors in the spec-tooling space
497
+ * ✗ icp-persona: Agent failed — timeout
498
+ *
499
+ * @param agentName - Agent name
500
+ * @param headline - Key finding or error message
501
+ * @param status - Agent completion status
502
+ */
503
+ function renderAgentCompletion(agentName, headline, status) {
504
+ const symbol = status === 'complete' ? exports.SYMBOLS.complete : exports.SYMBOLS.failed;
505
+ return `${symbol} ${agentName}: ${headline}`;
506
+ }
507
+ // ─── Agent Progress ─────────────────────────────────────────────────────────
508
+ /**
509
+ * Render full agent progress block showing pending and completed agents.
510
+ * Pending agents show ○, completed show ✓ with headline, failed show ✗.
511
+ *
512
+ * ✓ market-researcher: 3 direct competitors found
513
+ * ✓ icp-persona: primary persona is technical founder
514
+ * ○ positioning (pending)
515
+ * ○ brainstorm-alternatives (pending)
516
+ * ○ devils-advocate (pending)
517
+ */
518
+ function renderAgentProgress(agents) {
519
+ const lines = [];
520
+ for (const agent of agents) {
521
+ switch (agent.status) {
522
+ case 'complete':
523
+ lines.push(`${exports.SYMBOLS.complete} ${agent.name}: ${agent.headline || 'complete'}`);
524
+ break;
525
+ case 'failed':
526
+ lines.push(`${exports.SYMBOLS.failed} ${agent.name}: ${agent.headline || 'failed'}`);
527
+ break;
528
+ case 'pending':
529
+ lines.push(`${exports.SYMBOLS.pending} ${agent.name} (pending)`);
530
+ break;
531
+ }
532
+ }
533
+ return lines.join('\n');
534
+ }
535
+ // ─── File Inventory ──────────────────────────────────────────────────────────
536
+ /**
537
+ * Render a file inventory table listing all generated files with descriptions.
538
+ * Used at pipeline end to give users a deliverable checklist.
539
+ *
540
+ * @param files - Array of { path, description } objects
541
+ * @returns Markdown table string
542
+ */
543
+ function renderFileInventory(files) {
544
+ if (files.length === 0)
545
+ return '';
546
+ const lines = [];
547
+ lines.push('| File | Description |');
548
+ lines.push('|------|-------------|');
549
+ for (const file of files) {
550
+ lines.push(`| \`${file.path}\` | ${file.description} |`);
551
+ }
552
+ return lines.join('\n');
553
+ }
554
+ // ─── Headline Extraction ────────────────────────────────────────────────────
555
+ /**
556
+ * Extract a one-line headline from agent output for completion display.
557
+ * Multi-strategy extraction with robust fallback.
558
+ *
559
+ * Strategy priority:
560
+ * 1. First sentence after "## Summary" heading
561
+ * 2. Text after "Key finding:" prefix
562
+ * 3. First non-heading, non-empty line
563
+ * 4. Fallback: "{agentName} analysis complete"
564
+ *
565
+ * Truncates to 80 chars. Never returns empty string.
566
+ *
567
+ * @param agentOutput - Raw agent output content
568
+ * @param agentName - Agent name (for fallback)
569
+ */
570
+ function extractHeadline(agentOutput, agentName) {
571
+ const MAX_LENGTH = 80;
572
+ if (!agentOutput || agentOutput.trim().length === 0) {
573
+ return `${agentName} analysis complete`;
574
+ }
575
+ const lines = agentOutput.split('\n');
576
+ // Strategy 1: First sentence after "## Summary" heading
577
+ for (let i = 0; i < lines.length; i++) {
578
+ if (/^##\s+Summary\s*$/i.test(lines[i].trim())) {
579
+ // Look at next non-empty lines for the first sentence
580
+ for (let j = i + 1; j < lines.length; j++) {
581
+ const line = lines[j].trim();
582
+ if (line.length === 0)
583
+ continue;
584
+ if (line.startsWith('#'))
585
+ break; // next heading, stop
586
+ // Take up to first period
587
+ const periodIdx = line.indexOf('.');
588
+ const sentence = periodIdx > 0 ? line.slice(0, periodIdx + 1) : line;
589
+ return truncateHeadline(sentence, MAX_LENGTH);
590
+ }
591
+ }
592
+ }
593
+ // Strategy 2: "Key finding:" prefix
594
+ for (const line of lines) {
595
+ const match = line.match(/Key finding:\s*(.+)/i);
596
+ if (match) {
597
+ return truncateHeadline(match[1].trim(), MAX_LENGTH);
598
+ }
599
+ }
600
+ // Strategy 3: First non-heading, non-empty line
601
+ for (const line of lines) {
602
+ const trimmed = line.trim();
603
+ if (trimmed.length === 0)
604
+ continue;
605
+ if (trimmed.startsWith('#'))
606
+ continue;
607
+ if (trimmed.startsWith('---'))
608
+ continue;
609
+ if (trimmed.startsWith('```'))
610
+ continue;
611
+ return truncateHeadline(trimmed, MAX_LENGTH);
612
+ }
613
+ // Strategy 4: Fallback
614
+ return `${agentName} analysis complete`;
615
+ }
616
+ /**
617
+ * Truncate a string to maxLength, adding "..." if truncated.
618
+ */
619
+ function truncateHeadline(text, maxLength) {
620
+ const cleaned = text.trim();
621
+ if (cleaned.length <= maxLength)
622
+ return cleaned;
623
+ return cleaned.slice(0, maxLength - 3) + '...';
624
+ }