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.
- package/bin/gswd-tools.cjs +228 -0
- package/commands/gswd/imagine.md +7 -1
- package/commands/gswd/start.md +507 -32
- package/dist/lib/audit.d.ts +205 -0
- package/dist/lib/audit.js +805 -0
- package/dist/lib/bootstrap.d.ts +103 -0
- package/dist/lib/bootstrap.js +563 -0
- package/dist/lib/compile.d.ts +239 -0
- package/dist/lib/compile.js +1152 -0
- package/dist/lib/config.d.ts +49 -0
- package/dist/lib/config.js +150 -0
- package/dist/lib/imagine-agents.d.ts +54 -0
- package/dist/lib/imagine-agents.js +185 -0
- package/dist/lib/imagine-gate.d.ts +47 -0
- package/dist/lib/imagine-gate.js +131 -0
- package/dist/lib/imagine-input.d.ts +46 -0
- package/dist/lib/imagine-input.js +233 -0
- package/dist/lib/imagine-synthesis.d.ts +90 -0
- package/dist/lib/imagine-synthesis.js +453 -0
- package/dist/lib/imagine.d.ts +56 -0
- package/dist/lib/imagine.js +413 -0
- package/dist/lib/intake.d.ts +27 -0
- package/dist/lib/intake.js +82 -0
- package/dist/lib/parse.d.ts +59 -0
- package/dist/lib/parse.js +171 -0
- package/dist/lib/render.d.ts +309 -0
- package/dist/lib/render.js +624 -0
- package/dist/lib/specify-agents.d.ts +120 -0
- package/dist/lib/specify-agents.js +269 -0
- package/dist/lib/specify-journeys.d.ts +124 -0
- package/dist/lib/specify-journeys.js +279 -0
- package/dist/lib/specify-nfr.d.ts +45 -0
- package/dist/lib/specify-nfr.js +159 -0
- package/dist/lib/specify-roles.d.ts +46 -0
- package/dist/lib/specify-roles.js +88 -0
- package/dist/lib/specify.d.ts +70 -0
- package/dist/lib/specify.js +676 -0
- package/dist/lib/state.d.ts +140 -0
- package/dist/lib/state.js +340 -0
- package/dist/tests/audit.test.d.ts +4 -0
- package/dist/tests/audit.test.js +1579 -0
- package/dist/tests/bootstrap.test.d.ts +5 -0
- package/dist/tests/bootstrap.test.js +611 -0
- package/dist/tests/compile.test.d.ts +4 -0
- package/dist/tests/compile.test.js +862 -0
- package/dist/tests/config.test.d.ts +4 -0
- package/dist/tests/config.test.js +191 -0
- package/dist/tests/imagine-agents.test.d.ts +6 -0
- package/dist/tests/imagine-agents.test.js +179 -0
- package/dist/tests/imagine-gate.test.d.ts +6 -0
- package/dist/tests/imagine-gate.test.js +264 -0
- package/dist/tests/imagine-input.test.d.ts +6 -0
- package/dist/tests/imagine-input.test.js +283 -0
- package/dist/tests/imagine-synthesis.test.d.ts +7 -0
- package/dist/tests/imagine-synthesis.test.js +380 -0
- package/dist/tests/imagine.test.d.ts +8 -0
- package/dist/tests/imagine.test.js +406 -0
- package/dist/tests/parse.test.d.ts +4 -0
- package/dist/tests/parse.test.js +285 -0
- package/dist/tests/render.test.d.ts +4 -0
- package/dist/tests/render.test.js +236 -0
- package/dist/tests/specify-agents.test.d.ts +4 -0
- package/dist/tests/specify-agents.test.js +352 -0
- package/dist/tests/specify-journeys.test.d.ts +5 -0
- package/dist/tests/specify-journeys.test.js +440 -0
- package/dist/tests/specify-nfr.test.d.ts +4 -0
- package/dist/tests/specify-nfr.test.js +205 -0
- package/dist/tests/specify-roles.test.d.ts +4 -0
- package/dist/tests/specify-roles.test.js +136 -0
- package/dist/tests/specify.test.d.ts +9 -0
- package/dist/tests/specify.test.js +544 -0
- package/dist/tests/state.test.d.ts +4 -0
- package/dist/tests/state.test.js +316 -0
- package/lib/bootstrap.ts +37 -11
- package/lib/compile.ts +426 -4
- package/lib/imagine-agents.ts +53 -7
- package/lib/imagine-synthesis.ts +170 -6
- package/lib/imagine.ts +59 -5
- package/lib/intake.ts +60 -0
- package/lib/parse.ts +2 -1
- package/lib/render.ts +566 -5
- package/lib/specify-agents.ts +25 -3
- package/lib/state.ts +115 -0
- package/package.json +3 -2
- package/templates/gswd/DECISIONS.template.md +3 -0
package/lib/render.ts
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* Schema: GSWD_SPEC.md Section 7.1-7.3
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
|
|
10
13
|
// ─── Status Symbols ──────────────────────────────────────────────────────────
|
|
11
14
|
|
|
12
15
|
export const SYMBOLS = {
|
|
@@ -18,6 +21,82 @@ export const SYMBOLS = {
|
|
|
18
21
|
warning: '\u26A0', // ⚠
|
|
19
22
|
} as const;
|
|
20
23
|
|
|
24
|
+
// ─── Colors ──────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* ANSI color codes for GSWD branding.
|
|
28
|
+
* Electric orange/amber is the brand color (per CONTEXT.md locked decision).
|
|
29
|
+
* 256-color code \x1b[38;5;208m; dim uses \x1b[2m.
|
|
30
|
+
*/
|
|
31
|
+
export const COLORS = {
|
|
32
|
+
orange: '\x1b[38;5;208m',
|
|
33
|
+
dim: '\x1b[2m',
|
|
34
|
+
reset: '\x1b[0m',
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Wrap text in ANSI color. No-op when stdout is not a TTY (CI/Docker/pipe safety).
|
|
39
|
+
* Respects FORCE_COLOR env var to override TTY detection.
|
|
40
|
+
*/
|
|
41
|
+
export function colorize(text: string, color: keyof typeof COLORS): string {
|
|
42
|
+
if (!process.stdout.isTTY && !process.env.FORCE_COLOR) {
|
|
43
|
+
return text;
|
|
44
|
+
}
|
|
45
|
+
return `${COLORS[color]}${text}${COLORS.reset}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Logo ────────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Render the GSWD ASCII logo with catchphrase.
|
|
52
|
+
* Logo shape matches GSD's block-letter style from install.js.
|
|
53
|
+
* Static string — no figlet (STATE.md architectural decision).
|
|
54
|
+
*
|
|
55
|
+
* @param version - Optional version string (e.g., "1.0.1")
|
|
56
|
+
*/
|
|
57
|
+
export function renderLogo(version?: string): string {
|
|
58
|
+
const lines = [
|
|
59
|
+
' \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 ',
|
|
60
|
+
' \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',
|
|
61
|
+
' \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',
|
|
62
|
+
' \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',
|
|
63
|
+
' \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',
|
|
64
|
+
' \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 ',
|
|
65
|
+
].join('\n');
|
|
66
|
+
|
|
67
|
+
const coloredLogo = colorize(lines, 'orange');
|
|
68
|
+
const tagline = ' Spec clarity before code.';
|
|
69
|
+
const versionStr = version ? colorize(` v${version}`, 'dim') : '';
|
|
70
|
+
|
|
71
|
+
return [coloredLogo, '', tagline, versionStr].filter(Boolean).join('\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── First Run Guide ─────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Render the condensed first-run command guide.
|
|
78
|
+
* Per CONTEXT.md: primary commands only, no descriptions.
|
|
79
|
+
* Followed by automatic /gswd:start proposal for zero-friction onboarding.
|
|
80
|
+
*/
|
|
81
|
+
export function renderFirstRunGuide(): string {
|
|
82
|
+
const commands = [
|
|
83
|
+
'/gswd:start',
|
|
84
|
+
'/gswd:imagine',
|
|
85
|
+
'/gswd:specify',
|
|
86
|
+
'/gswd:compile',
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const lines = [
|
|
90
|
+
'Commands:',
|
|
91
|
+
'',
|
|
92
|
+
...commands.map(cmd => ` ${colorize(cmd, 'orange')}`),
|
|
93
|
+
'',
|
|
94
|
+
'Run /gswd:start to begin.',
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
return lines.join('\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
21
100
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
22
101
|
|
|
23
102
|
/**
|
|
@@ -46,7 +125,7 @@ export function padRight(str: string, width: number): string {
|
|
|
46
125
|
*/
|
|
47
126
|
export function renderBanner(stageName: string): string {
|
|
48
127
|
const BANNER_WIDTH = 55;
|
|
49
|
-
const border = '\u2501'.repeat(BANNER_WIDTH);
|
|
128
|
+
const border = colorize('\u2501'.repeat(BANNER_WIDTH), 'orange');
|
|
50
129
|
const title = ` GSWD \u25B6 ${stageName.toUpperCase()}`;
|
|
51
130
|
return `${border}\n${title}\n${border}`;
|
|
52
131
|
}
|
|
@@ -73,13 +152,13 @@ export function renderCheckpoint(
|
|
|
73
152
|
const BOX_WIDTH = 56;
|
|
74
153
|
const INNER_WIDTH = BOX_WIDTH - 4; // 52 (accounting for "│ " and " │")
|
|
75
154
|
|
|
76
|
-
const topBorder = `\u250C${'\u2500'.repeat(BOX_WIDTH - 2)}\u2510
|
|
77
|
-
const midBorder = `\u251C${'\u2500'.repeat(BOX_WIDTH - 2)}\u2524
|
|
78
|
-
const bottomBorder = `\u2514${'\u2500'.repeat(BOX_WIDTH - 2)}\u2518
|
|
155
|
+
const topBorder = colorize(`\u250C${'\u2500'.repeat(BOX_WIDTH - 2)}\u2510`, 'orange');
|
|
156
|
+
const midBorder = colorize(`\u251C${'\u2500'.repeat(BOX_WIDTH - 2)}\u2524`, 'orange');
|
|
157
|
+
const bottomBorder = colorize(`\u2514${'\u2500'.repeat(BOX_WIDTH - 2)}\u2518`, 'orange');
|
|
79
158
|
|
|
80
159
|
const line = (text: string): string => {
|
|
81
160
|
const padded = padRight(text, INNER_WIDTH);
|
|
82
|
-
return
|
|
161
|
+
return `${colorize('\u2502', 'orange')} ${padded} ${colorize('\u2502', 'orange')}`;
|
|
83
162
|
};
|
|
84
163
|
|
|
85
164
|
const blankLine = line('');
|
|
@@ -198,3 +277,485 @@ export function renderProgressBar(
|
|
|
198
277
|
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(empty);
|
|
199
278
|
return `Progress: ${bar} ${percent}%`;
|
|
200
279
|
}
|
|
280
|
+
|
|
281
|
+
// ─── Stage Summary ────────────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Input for stage summary rendering.
|
|
285
|
+
* Used at manual-mode checkpoints to show what a stage produced.
|
|
286
|
+
*/
|
|
287
|
+
export interface StageSummarySection {
|
|
288
|
+
name: string;
|
|
289
|
+
summary: string;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export interface StageSummaryInput {
|
|
293
|
+
stageName: string;
|
|
294
|
+
headline: string;
|
|
295
|
+
sections: StageSummarySection[];
|
|
296
|
+
filesCreated: string[];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Render a structured stage summary for manual-mode checkpoints.
|
|
301
|
+
* Consistent template per CONTEXT.md locked decision:
|
|
302
|
+
* Headline -> section paragraphs -> file list
|
|
303
|
+
*
|
|
304
|
+
* ```
|
|
305
|
+
* ## Imagine Complete
|
|
306
|
+
*
|
|
307
|
+
* Imagine produced 3 product directions with ICP, GTM, and competition analysis.
|
|
308
|
+
*
|
|
309
|
+
* **Key outputs:**
|
|
310
|
+
* - ICP Analysis: Identified 2 primary personas with distinct pain points...
|
|
311
|
+
* - GTM Strategy: Recommended freemium launch with enterprise upgrade path...
|
|
312
|
+
*
|
|
313
|
+
* **Created:** IMAGINE.md, ICP.md, GTM.md, COMPETITION.md, DECISIONS.md
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
316
|
+
// ─── Research Summary ────────────────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Research summary section for post-agent display.
|
|
320
|
+
* Mirrors ResearchSummarySection from imagine-synthesis.ts.
|
|
321
|
+
*/
|
|
322
|
+
export interface RenderResearchSection {
|
|
323
|
+
displayName: string;
|
|
324
|
+
takeaways: string[];
|
|
325
|
+
bridge: string;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Render structured research summary after agent completion.
|
|
330
|
+
* Product-contextualized: section intros reference user's product.
|
|
331
|
+
* Per CONTEXT.md: 3-5 takeaway bullets per section, bridging paragraph per section.
|
|
332
|
+
*
|
|
333
|
+
* @param sections - Research summary sections from buildResearchSummary
|
|
334
|
+
* @param productContext - Brief description of the product (from brief.vision)
|
|
335
|
+
* @param targetUser - Who the product is for (from brief.target_user)
|
|
336
|
+
*/
|
|
337
|
+
export function renderResearchSummary(
|
|
338
|
+
sections: RenderResearchSection[],
|
|
339
|
+
productContext: string,
|
|
340
|
+
targetUser: string,
|
|
341
|
+
): string {
|
|
342
|
+
const parts: string[] = [];
|
|
343
|
+
parts.push('## Research Summary');
|
|
344
|
+
parts.push('');
|
|
345
|
+
|
|
346
|
+
for (const section of sections) {
|
|
347
|
+
// Product-contextualized intro
|
|
348
|
+
parts.push(`### ${section.displayName}`);
|
|
349
|
+
parts.push(`*For a product helping ${targetUser}:*`);
|
|
350
|
+
parts.push('');
|
|
351
|
+
|
|
352
|
+
// Takeaway bullets (3-5)
|
|
353
|
+
for (const takeaway of section.takeaways) {
|
|
354
|
+
parts.push(`- ${takeaway}`);
|
|
355
|
+
}
|
|
356
|
+
parts.push('');
|
|
357
|
+
|
|
358
|
+
// Bridging paragraph
|
|
359
|
+
parts.push(section.bridge);
|
|
360
|
+
parts.push('');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return parts.join('\n');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ─── Direction Cards ─────────────────────────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Direction card input for rendering.
|
|
370
|
+
*/
|
|
371
|
+
export interface RenderDirectionInput {
|
|
372
|
+
label: string;
|
|
373
|
+
rationale: string[];
|
|
374
|
+
icp_summary: string;
|
|
375
|
+
problem_framing: string;
|
|
376
|
+
wedge: string;
|
|
377
|
+
differentiator: string;
|
|
378
|
+
risks: string[];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Render a direction card with "Why this makes sense" rationale section.
|
|
383
|
+
* Per CONTEXT.md: rationale section comes FIRST, before existing fields.
|
|
384
|
+
* Recommended direction gets a visual marker.
|
|
385
|
+
*
|
|
386
|
+
* @param direction - Direction data
|
|
387
|
+
* @param index - 0-based index for display numbering
|
|
388
|
+
* @param isRecommended - Whether this is the proposed/recommended direction
|
|
389
|
+
*/
|
|
390
|
+
export function renderDirectionCard(
|
|
391
|
+
direction: RenderDirectionInput,
|
|
392
|
+
index: number,
|
|
393
|
+
isRecommended: boolean,
|
|
394
|
+
): string {
|
|
395
|
+
const marker = isRecommended ? ' **(Recommended)**' : '';
|
|
396
|
+
const parts: string[] = [];
|
|
397
|
+
|
|
398
|
+
parts.push(`### ${direction.label}${marker}`);
|
|
399
|
+
parts.push('');
|
|
400
|
+
|
|
401
|
+
// Rationale section FIRST per CONTEXT.md locked decision
|
|
402
|
+
if (direction.rationale.length > 0) {
|
|
403
|
+
parts.push('**Why this makes sense:**');
|
|
404
|
+
for (const r of direction.rationale) {
|
|
405
|
+
parts.push(`- ${r}`);
|
|
406
|
+
}
|
|
407
|
+
parts.push('');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Existing fields
|
|
411
|
+
parts.push(`**ICP:** ${direction.icp_summary}`);
|
|
412
|
+
parts.push(`**Problem:** ${direction.problem_framing}`);
|
|
413
|
+
parts.push(`**Wedge:** ${direction.wedge}`);
|
|
414
|
+
parts.push(`**Differentiator:** ${direction.differentiator}`);
|
|
415
|
+
parts.push(`**Risks:** ${direction.risks.join('; ')}`);
|
|
416
|
+
|
|
417
|
+
return parts.join('\n');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ─── Stage Summary ───────────────────────────────────────────────────────────
|
|
421
|
+
|
|
422
|
+
export function renderStageSummary(input: StageSummaryInput): string {
|
|
423
|
+
const parts: string[] = [];
|
|
424
|
+
|
|
425
|
+
// Headline
|
|
426
|
+
parts.push(`## ${input.stageName} Complete`);
|
|
427
|
+
parts.push('');
|
|
428
|
+
parts.push(input.headline);
|
|
429
|
+
parts.push('');
|
|
430
|
+
|
|
431
|
+
// Key outputs (section summaries)
|
|
432
|
+
if (input.sections.length > 0) {
|
|
433
|
+
parts.push('**Key outputs:**');
|
|
434
|
+
for (const section of input.sections) {
|
|
435
|
+
parts.push(`- **${section.name}:** ${section.summary}`);
|
|
436
|
+
}
|
|
437
|
+
parts.push('');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Files created
|
|
441
|
+
if (input.filesCreated.length > 0) {
|
|
442
|
+
parts.push(`**Created:** ${input.filesCreated.join(', ')}`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return parts.join('\n');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ─── Product Context ────────────────────────────────────────────────────────
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Minimal product context for contextual messaging throughout the pipeline.
|
|
452
|
+
* Extracted from INTAKE.json and DECISIONS.md with graceful fallbacks.
|
|
453
|
+
*/
|
|
454
|
+
export interface ProductContext {
|
|
455
|
+
/** Product domain (e.g., "spec-writing tool") */
|
|
456
|
+
domain: string;
|
|
457
|
+
/** Target user (e.g., "solo founders") */
|
|
458
|
+
targetUser: string;
|
|
459
|
+
/** Stage-specific context string */
|
|
460
|
+
stageContext: string;
|
|
461
|
+
/** Whether this is a re-run (changes banner messaging) */
|
|
462
|
+
isRerun: boolean;
|
|
463
|
+
/** Re-run feedback context (if re-run) */
|
|
464
|
+
rerunContext?: string;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Extract product context from DECISIONS.md and INTAKE.json.
|
|
469
|
+
* DECISIONS.md is preferred (more precise, available after Imagine).
|
|
470
|
+
* INTAKE.json is fallback (available from pipeline start).
|
|
471
|
+
* Returns safe defaults if neither file exists.
|
|
472
|
+
*
|
|
473
|
+
* @param planningDir - Path to .planning/ directory
|
|
474
|
+
* @param isRerun - Whether this is a re-run
|
|
475
|
+
*/
|
|
476
|
+
export function extractProductContext(planningDir: string, isRerun: boolean = false): ProductContext {
|
|
477
|
+
let domain = 'your product';
|
|
478
|
+
let targetUser = 'your users';
|
|
479
|
+
|
|
480
|
+
// Try DECISIONS.md first (more precise, available after Imagine)
|
|
481
|
+
try {
|
|
482
|
+
const decisionsPath = path.join(planningDir, 'DECISIONS.md');
|
|
483
|
+
const decisions = fs.readFileSync(decisionsPath, 'utf-8');
|
|
484
|
+
const targetMatch = decisions.match(/\*\*Target User:\*\*\s*(?:Auto-chosen:\s*)?(.+)/);
|
|
485
|
+
if (targetMatch) targetUser = targetMatch[1].trim();
|
|
486
|
+
const directionMatch = decisions.match(/\*\*Product Direction:\*\*\s*(?:Auto-chosen:\s*)?(.+)/);
|
|
487
|
+
if (directionMatch) domain = directionMatch[1].trim();
|
|
488
|
+
} catch {
|
|
489
|
+
// Fall through to INTAKE.json
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Fallback to INTAKE.json (available from pipeline start)
|
|
493
|
+
if (domain === 'your product') {
|
|
494
|
+
try {
|
|
495
|
+
const gswdDir = path.join(planningDir, 'gswd');
|
|
496
|
+
const intakePath = path.join(gswdDir, 'INTAKE.json');
|
|
497
|
+
const intake = JSON.parse(fs.readFileSync(intakePath, 'utf-8'));
|
|
498
|
+
if (intake.product_description) {
|
|
499
|
+
// Use first sentence as domain
|
|
500
|
+
const firstSentence = intake.product_description.split('.')[0];
|
|
501
|
+
if (firstSentence && firstSentence.trim().length > 0) {
|
|
502
|
+
domain = firstSentence.trim();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
} catch {
|
|
506
|
+
// Use defaults
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return { domain, targetUser, stageContext: '', isRerun, rerunContext: undefined };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ─── Stage Context Templates ────────────────────────────────────────────────
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Stage-specific context line generators for contextual banners.
|
|
517
|
+
* Each returns a product-contextualized one-liner for the banner.
|
|
518
|
+
* Compile is deliberately excluded (per CONTEXT.md: deterministic, no banner needed).
|
|
519
|
+
*/
|
|
520
|
+
export const STAGE_CONTEXT_TEMPLATES: Record<string, (ctx: ProductContext) => string> = {
|
|
521
|
+
imagine: (ctx) => ctx.isRerun
|
|
522
|
+
? `Refining directions based on your feedback${ctx.rerunContext ? ` about ${ctx.rerunContext}` : ''}`
|
|
523
|
+
: `Validating product direction for ${ctx.domain} targeting ${ctx.targetUser}`,
|
|
524
|
+
specify: (ctx) => `Building the execution-grade spec: journeys, requirements, and architecture for ${ctx.domain}`,
|
|
525
|
+
audit: (ctx) => `Verifying spec completeness \u2014 every journey, requirement, and edge case for ${ctx.domain} must trace`,
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
// ─── Agent Briefing Templates ───────────────────────────────────────────────
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Product-contextualized one-liner descriptions for Imagine research agents.
|
|
532
|
+
* Each function receives ProductContext and returns a purpose string.
|
|
533
|
+
*/
|
|
534
|
+
export const IMAGINE_AGENT_BRIEFINGS: Record<string, (ctx: ProductContext) => string> = {
|
|
535
|
+
'market-researcher': (ctx) => `mapping the ${ctx.domain} landscape to find gaps`,
|
|
536
|
+
'icp-persona': (ctx) => `profiling ${ctx.targetUser} who need ${ctx.domain}`,
|
|
537
|
+
'positioning': (ctx) => `finding the angle that makes ${ctx.domain} obviously necessary`,
|
|
538
|
+
'brainstorm-alternatives': (ctx) => `exploring 3 distinct product directions for ${ctx.domain}`,
|
|
539
|
+
'devils-advocate': (ctx) => `stress-testing assumptions before you commit to a direction`,
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Product-contextualized one-liner descriptions for Specify agents.
|
|
544
|
+
*/
|
|
545
|
+
export const SPECIFY_AGENT_BRIEFINGS: Record<string, (ctx: ProductContext) => string> = {
|
|
546
|
+
'journey-mapper': (ctx) => `mapping user journeys for ${ctx.targetUser} through ${ctx.domain}`,
|
|
547
|
+
'architecture-drafter': (ctx) => `designing the system architecture for ${ctx.domain}`,
|
|
548
|
+
'integrations-checker': (ctx) => `evaluating external services ${ctx.domain} needs to connect with`,
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
// ─── Contextual Banner ──────────────────────────────────────────────────────
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Render a stage banner with a product-contextualized line below the title.
|
|
555
|
+
* Extension of renderBanner() for consultant-style messaging (Phase 16).
|
|
556
|
+
*
|
|
557
|
+
* ```
|
|
558
|
+
* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
559
|
+
* GSWD ► IMAGINE
|
|
560
|
+
* Validating product direction for a spec-writing tool targeting founders
|
|
561
|
+
* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
562
|
+
* ```
|
|
563
|
+
*
|
|
564
|
+
* @param stageName - Stage name (will be uppercased)
|
|
565
|
+
* @param contextLine - Product-contextualized one-liner
|
|
566
|
+
*/
|
|
567
|
+
export function renderContextBanner(stageName: string, contextLine: string): string {
|
|
568
|
+
const BANNER_WIDTH = 55;
|
|
569
|
+
const border = colorize('\u2501'.repeat(BANNER_WIDTH), 'orange');
|
|
570
|
+
const title = ` GSWD \u25B6 ${stageName.toUpperCase()}`;
|
|
571
|
+
const context = ` ${contextLine}`;
|
|
572
|
+
return `${border}\n${title}\n${context}\n${border}`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ─── Agent Briefing ─────────────────────────────────────────────────────────
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Input for agent pre-briefing display.
|
|
579
|
+
*/
|
|
580
|
+
export interface AgentBriefingEntry {
|
|
581
|
+
name: string;
|
|
582
|
+
productPurpose: string;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Render agent pre-briefing block before spawning.
|
|
587
|
+
* Technical transparency: name each agent explicitly with product-specific purpose.
|
|
588
|
+
*
|
|
589
|
+
* ```
|
|
590
|
+
* ◆ Spawning 5 research agents:
|
|
591
|
+
* → market-researcher: mapping the spec-tooling landscape to find gaps
|
|
592
|
+
* → icp-persona: profiling founders who struggle with product specification
|
|
593
|
+
* ```
|
|
594
|
+
*
|
|
595
|
+
* @param agents - Array of agent names with product-contextualized purposes
|
|
596
|
+
* @param stageContext - Optional stage context description
|
|
597
|
+
*/
|
|
598
|
+
export function renderAgentBriefing(agents: AgentBriefingEntry[], stageContext?: string): string {
|
|
599
|
+
const parts: string[] = [];
|
|
600
|
+
parts.push(`${SYMBOLS.inProgress} Spawning ${agents.length} research agents:`);
|
|
601
|
+
|
|
602
|
+
for (const agent of agents) {
|
|
603
|
+
parts.push(` \u2192 ${agent.name}: ${agent.productPurpose}`);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (stageContext) {
|
|
607
|
+
parts.push('');
|
|
608
|
+
parts.push(stageContext);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return parts.join('\n');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ─── Agent Completion ───────────────────────────────────────────────────────
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Render a single agent completion line with its key finding headline.
|
|
618
|
+
*
|
|
619
|
+
* ✓ market-researcher: 3 direct competitors in the spec-tooling space
|
|
620
|
+
* ✗ icp-persona: Agent failed — timeout
|
|
621
|
+
*
|
|
622
|
+
* @param agentName - Agent name
|
|
623
|
+
* @param headline - Key finding or error message
|
|
624
|
+
* @param status - Agent completion status
|
|
625
|
+
*/
|
|
626
|
+
export function renderAgentCompletion(
|
|
627
|
+
agentName: string,
|
|
628
|
+
headline: string,
|
|
629
|
+
status: 'complete' | 'failed',
|
|
630
|
+
): string {
|
|
631
|
+
const symbol = status === 'complete' ? SYMBOLS.complete : SYMBOLS.failed;
|
|
632
|
+
return `${symbol} ${agentName}: ${headline}`;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ─── Agent Progress ─────────────────────────────────────────────────────────
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Render full agent progress block showing pending and completed agents.
|
|
639
|
+
* Pending agents show ○, completed show ✓ with headline, failed show ✗.
|
|
640
|
+
*
|
|
641
|
+
* ✓ market-researcher: 3 direct competitors found
|
|
642
|
+
* ✓ icp-persona: primary persona is technical founder
|
|
643
|
+
* ○ positioning (pending)
|
|
644
|
+
* ○ brainstorm-alternatives (pending)
|
|
645
|
+
* ○ devils-advocate (pending)
|
|
646
|
+
*/
|
|
647
|
+
export function renderAgentProgress(
|
|
648
|
+
agents: Array<{ name: string; status: 'pending' | 'complete' | 'failed'; headline?: string }>,
|
|
649
|
+
): string {
|
|
650
|
+
const lines: string[] = [];
|
|
651
|
+
|
|
652
|
+
for (const agent of agents) {
|
|
653
|
+
switch (agent.status) {
|
|
654
|
+
case 'complete':
|
|
655
|
+
lines.push(`${SYMBOLS.complete} ${agent.name}: ${agent.headline || 'complete'}`);
|
|
656
|
+
break;
|
|
657
|
+
case 'failed':
|
|
658
|
+
lines.push(`${SYMBOLS.failed} ${agent.name}: ${agent.headline || 'failed'}`);
|
|
659
|
+
break;
|
|
660
|
+
case 'pending':
|
|
661
|
+
lines.push(`${SYMBOLS.pending} ${agent.name} (pending)`);
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return lines.join('\n');
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// ─── File Inventory ──────────────────────────────────────────────────────────
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Render a file inventory table listing all generated files with descriptions.
|
|
673
|
+
* Used at pipeline end to give users a deliverable checklist.
|
|
674
|
+
*
|
|
675
|
+
* @param files - Array of { path, description } objects
|
|
676
|
+
* @returns Markdown table string
|
|
677
|
+
*/
|
|
678
|
+
export function renderFileInventory(files: Array<{ path: string; description: string }>): string {
|
|
679
|
+
if (files.length === 0) return '';
|
|
680
|
+
|
|
681
|
+
const lines: string[] = [];
|
|
682
|
+
lines.push('| File | Description |');
|
|
683
|
+
lines.push('|------|-------------|');
|
|
684
|
+
for (const file of files) {
|
|
685
|
+
lines.push(`| \`${file.path}\` | ${file.description} |`);
|
|
686
|
+
}
|
|
687
|
+
return lines.join('\n');
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ─── Headline Extraction ────────────────────────────────────────────────────
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Extract a one-line headline from agent output for completion display.
|
|
694
|
+
* Multi-strategy extraction with robust fallback.
|
|
695
|
+
*
|
|
696
|
+
* Strategy priority:
|
|
697
|
+
* 1. First sentence after "## Summary" heading
|
|
698
|
+
* 2. Text after "Key finding:" prefix
|
|
699
|
+
* 3. First non-heading, non-empty line
|
|
700
|
+
* 4. Fallback: "{agentName} analysis complete"
|
|
701
|
+
*
|
|
702
|
+
* Truncates to 80 chars. Never returns empty string.
|
|
703
|
+
*
|
|
704
|
+
* @param agentOutput - Raw agent output content
|
|
705
|
+
* @param agentName - Agent name (for fallback)
|
|
706
|
+
*/
|
|
707
|
+
export function extractHeadline(agentOutput: string, agentName: string): string {
|
|
708
|
+
const MAX_LENGTH = 80;
|
|
709
|
+
|
|
710
|
+
if (!agentOutput || agentOutput.trim().length === 0) {
|
|
711
|
+
return `${agentName} analysis complete`;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const lines = agentOutput.split('\n');
|
|
715
|
+
|
|
716
|
+
// Strategy 1: First sentence after "## Summary" heading
|
|
717
|
+
for (let i = 0; i < lines.length; i++) {
|
|
718
|
+
if (/^##\s+Summary\s*$/i.test(lines[i].trim())) {
|
|
719
|
+
// Look at next non-empty lines for the first sentence
|
|
720
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
721
|
+
const line = lines[j].trim();
|
|
722
|
+
if (line.length === 0) continue;
|
|
723
|
+
if (line.startsWith('#')) break; // next heading, stop
|
|
724
|
+
// Take up to first period
|
|
725
|
+
const periodIdx = line.indexOf('.');
|
|
726
|
+
const sentence = periodIdx > 0 ? line.slice(0, periodIdx + 1) : line;
|
|
727
|
+
return truncateHeadline(sentence, MAX_LENGTH);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Strategy 2: "Key finding:" prefix
|
|
733
|
+
for (const line of lines) {
|
|
734
|
+
const match = line.match(/Key finding:\s*(.+)/i);
|
|
735
|
+
if (match) {
|
|
736
|
+
return truncateHeadline(match[1].trim(), MAX_LENGTH);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Strategy 3: First non-heading, non-empty line
|
|
741
|
+
for (const line of lines) {
|
|
742
|
+
const trimmed = line.trim();
|
|
743
|
+
if (trimmed.length === 0) continue;
|
|
744
|
+
if (trimmed.startsWith('#')) continue;
|
|
745
|
+
if (trimmed.startsWith('---')) continue;
|
|
746
|
+
if (trimmed.startsWith('```')) continue;
|
|
747
|
+
return truncateHeadline(trimmed, MAX_LENGTH);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Strategy 4: Fallback
|
|
751
|
+
return `${agentName} analysis complete`;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Truncate a string to maxLength, adding "..." if truncated.
|
|
756
|
+
*/
|
|
757
|
+
function truncateHeadline(text: string, maxLength: number): string {
|
|
758
|
+
const cleaned = text.trim();
|
|
759
|
+
if (cleaned.length <= maxLength) return cleaned;
|
|
760
|
+
return cleaned.slice(0, maxLength - 3) + '...';
|
|
761
|
+
}
|
package/lib/specify-agents.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
import * as fs from 'node:fs';
|
|
13
13
|
import type { Journey, FunctionalRequirement } from './specify-journeys.js';
|
|
14
|
+
import { extractHeadline } from './render.js';
|
|
15
|
+
import type { OnAgentComplete } from './imagine-agents.js';
|
|
14
16
|
|
|
15
17
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
16
18
|
|
|
@@ -204,6 +206,7 @@ export async function orchestrateSpecifyAgents(
|
|
|
204
206
|
agents: SpecifyAgentDefinition[],
|
|
205
207
|
context: SpecifyAgentContext,
|
|
206
208
|
spawnFn: SpawnFn,
|
|
209
|
+
onComplete?: OnAgentComplete,
|
|
207
210
|
): Promise<AgentResult[]> {
|
|
208
211
|
const results: AgentResult[] = [];
|
|
209
212
|
|
|
@@ -212,14 +215,33 @@ export async function orchestrateSpecifyAgents(
|
|
|
212
215
|
for (const agent of sequentialAgents) {
|
|
213
216
|
const result = await runSingleAgent(agent, context, spawnFn);
|
|
214
217
|
results.push(result);
|
|
218
|
+
if (onComplete) {
|
|
219
|
+
onComplete({
|
|
220
|
+
agent: result.agent,
|
|
221
|
+
headline: result.status === 'complete'
|
|
222
|
+
? extractHeadline(result.content, result.agent)
|
|
223
|
+
: (result.error || 'Agent failed'),
|
|
224
|
+
status: result.status,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
215
227
|
}
|
|
216
228
|
|
|
217
229
|
// Phase 2: Parallel agents
|
|
218
230
|
const parallelAgents = agents.filter((a) => a.phase === 'parallel');
|
|
219
231
|
if (parallelAgents.length > 0) {
|
|
220
|
-
const parallelPromises = parallelAgents.map((agent) =>
|
|
221
|
-
runSingleAgent(agent, context, spawnFn)
|
|
222
|
-
|
|
232
|
+
const parallelPromises = parallelAgents.map(async (agent) => {
|
|
233
|
+
const result = await runSingleAgent(agent, context, spawnFn);
|
|
234
|
+
if (onComplete) {
|
|
235
|
+
onComplete({
|
|
236
|
+
agent: result.agent,
|
|
237
|
+
headline: result.status === 'complete'
|
|
238
|
+
? extractHeadline(result.content, result.agent)
|
|
239
|
+
: (result.error || 'Agent failed'),
|
|
240
|
+
status: result.status,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
});
|
|
223
245
|
const parallelResults = await Promise.all(parallelPromises);
|
|
224
246
|
results.push(...parallelResults);
|
|
225
247
|
}
|