gswd 0.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 (42) hide show
  1. package/agents/gswd/architecture-drafter.md +70 -0
  2. package/agents/gswd/brainstorm-alternatives.md +60 -0
  3. package/agents/gswd/devils-advocate.md +57 -0
  4. package/agents/gswd/icp-persona.md +58 -0
  5. package/agents/gswd/integrations-checker.md +68 -0
  6. package/agents/gswd/journey-mapper.md +69 -0
  7. package/agents/gswd/market-researcher.md +54 -0
  8. package/agents/gswd/positioning.md +54 -0
  9. package/bin/gswd-tools.cjs +716 -0
  10. package/lib/audit.ts +959 -0
  11. package/lib/bootstrap.ts +617 -0
  12. package/lib/compile.ts +940 -0
  13. package/lib/config.ts +164 -0
  14. package/lib/imagine-agents.ts +154 -0
  15. package/lib/imagine-gate.ts +156 -0
  16. package/lib/imagine-input.ts +242 -0
  17. package/lib/imagine-synthesis.ts +402 -0
  18. package/lib/imagine.ts +433 -0
  19. package/lib/parse.ts +196 -0
  20. package/lib/render.ts +200 -0
  21. package/lib/specify-agents.ts +332 -0
  22. package/lib/specify-journeys.ts +410 -0
  23. package/lib/specify-nfr.ts +208 -0
  24. package/lib/specify-roles.ts +122 -0
  25. package/lib/specify.ts +773 -0
  26. package/lib/state.ts +305 -0
  27. package/package.json +26 -0
  28. package/templates/gswd/ARCHITECTURE.template.md +17 -0
  29. package/templates/gswd/AUDIT.template.md +31 -0
  30. package/templates/gswd/COMPETITION.template.md +18 -0
  31. package/templates/gswd/DECISIONS.template.md +18 -0
  32. package/templates/gswd/GTM.template.md +18 -0
  33. package/templates/gswd/ICP.template.md +18 -0
  34. package/templates/gswd/IMAGINE.template.md +24 -0
  35. package/templates/gswd/INTEGRATIONS.template.md +7 -0
  36. package/templates/gswd/JOURNEYS.template.md +7 -0
  37. package/templates/gswd/NFR.template.md +7 -0
  38. package/templates/gswd/PROJECT.template.md +21 -0
  39. package/templates/gswd/REQUIREMENTS.template.md +31 -0
  40. package/templates/gswd/ROADMAP.template.md +21 -0
  41. package/templates/gswd/SPEC.template.md +19 -0
  42. package/templates/gswd/STATE.template.md +15 -0
package/lib/render.ts ADDED
@@ -0,0 +1,200 @@
1
+ /**
2
+ * GSWD Render Module — Terminal UI rendering functions
3
+ *
4
+ * All functions return strings (not print to stdout).
5
+ * Zero external dependencies. Uses only string operations.
6
+ *
7
+ * Schema: GSWD_SPEC.md Section 7.1-7.3
8
+ */
9
+
10
+ // ─── Status Symbols ──────────────────────────────────────────────────────────
11
+
12
+ export const SYMBOLS = {
13
+ complete: '\u2713', // ✓
14
+ failed: '\u2717', // ✗
15
+ inProgress: '\u25C6', // ◆
16
+ pending: '\u25CB', // ○
17
+ autoApproved: '\u26A1', // ⚡
18
+ warning: '\u26A0', // ⚠
19
+ } as const;
20
+
21
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
22
+
23
+ /**
24
+ * Pad or truncate a string to exact width.
25
+ * If str exceeds width, truncate and append '...' (total = width).
26
+ */
27
+ export function padRight(str: string, width: number): string {
28
+ if (str.length > width) {
29
+ return str.slice(0, width - 3) + '...';
30
+ }
31
+ return str + ' '.repeat(width - str.length);
32
+ }
33
+
34
+ // ─── Banner ──────────────────────────────────────────────────────────────────
35
+
36
+ /**
37
+ * Render a stage banner matching GSWD_SPEC Section 7.1.
38
+ *
39
+ * ```
40
+ * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
41
+ * GSWD ► {STAGE NAME}
42
+ * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
43
+ * ```
44
+ *
45
+ * Banner width = 55 characters. Stage name is uppercased.
46
+ */
47
+ export function renderBanner(stageName: string): string {
48
+ const BANNER_WIDTH = 55;
49
+ const border = '\u2501'.repeat(BANNER_WIDTH);
50
+ const title = ` GSWD \u25B6 ${stageName.toUpperCase()}`;
51
+ return `${border}\n${title}\n${border}`;
52
+ }
53
+
54
+ // ─── Checkpoint Box ──────────────────────────────────────────────────────────
55
+
56
+ /**
57
+ * Render a checkpoint box matching GSWD_SPEC Section 7.2.
58
+ *
59
+ * Box total width: 56 characters (54 inner + 2 border characters).
60
+ * Uses single-line box-drawing characters.
61
+ *
62
+ * @param type - Header text (e.g., "Decision Required")
63
+ * @param context - Context paragraph
64
+ * @param options - Numbered options list
65
+ * @param actionPrompt - Action prompt (appears after → )
66
+ */
67
+ export function renderCheckpoint(
68
+ type: string,
69
+ context: string,
70
+ options: string[],
71
+ actionPrompt: string,
72
+ ): string {
73
+ const BOX_WIDTH = 56;
74
+ const INNER_WIDTH = BOX_WIDTH - 4; // 52 (accounting for "│ " and " │")
75
+
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`;
79
+
80
+ const line = (text: string): string => {
81
+ const padded = padRight(text, INNER_WIDTH);
82
+ return `\u2502 ${padded} \u2502`;
83
+ };
84
+
85
+ const blankLine = line('');
86
+
87
+ const lines: string[] = [];
88
+ lines.push(topBorder);
89
+ lines.push(line(type));
90
+ lines.push(midBorder);
91
+ lines.push(line(context));
92
+ lines.push(blankLine);
93
+
94
+ for (let i = 0; i < options.length; i++) {
95
+ lines.push(line(`${i + 1}) ${options[i]}`));
96
+ }
97
+
98
+ lines.push(blankLine);
99
+ lines.push(line(`\u2192 ${actionPrompt}`));
100
+ lines.push(bottomBorder);
101
+
102
+ return lines.join('\n');
103
+ }
104
+
105
+ // ─── Next Up ─────────────────────────────────────────────────────────────────
106
+
107
+ /**
108
+ * Render a Next Up block matching GSWD_SPEC Section 7.3.
109
+ *
110
+ * ```
111
+ * ✓ Wrote: file1, file2, ...
112
+ *
113
+ * Next up:
114
+ * {nextCommand}
115
+ *
116
+ * Tip:
117
+ * If context is getting crowded, run /clear.
118
+ * ```
119
+ */
120
+ export function renderNextUp(
121
+ files: string[],
122
+ nextCommand: string,
123
+ alsoAvailable?: string[],
124
+ ): string {
125
+ const parts: string[] = [];
126
+
127
+ parts.push(`${SYMBOLS.complete} Wrote: ${files.join(', ')}`);
128
+ parts.push('');
129
+ parts.push('Next up:');
130
+ parts.push(` ${nextCommand}`);
131
+ parts.push('');
132
+ parts.push('Tip:');
133
+ parts.push(' If context is getting crowded, run /clear.');
134
+
135
+ if (alsoAvailable && alsoAvailable.length > 0) {
136
+ parts.push('');
137
+ parts.push('Also available:');
138
+ for (const item of alsoAvailable) {
139
+ parts.push(` - ${item}`);
140
+ }
141
+ }
142
+
143
+ return parts.join('\n');
144
+ }
145
+
146
+ // ─── Status Line ─────────────────────────────────────────────────────────────
147
+
148
+ /**
149
+ * Render a single status line with the appropriate symbol.
150
+ *
151
+ * Maps: done/pass -> ✓, fail -> ✗, in_progress -> ◆, not_started -> ○
152
+ */
153
+ export function renderStatusLine(stage: string, status: string): string {
154
+ let symbol: string;
155
+ switch (status) {
156
+ case 'done':
157
+ case 'pass':
158
+ symbol = SYMBOLS.complete;
159
+ break;
160
+ case 'fail':
161
+ symbol = SYMBOLS.failed;
162
+ break;
163
+ case 'in_progress':
164
+ symbol = SYMBOLS.inProgress;
165
+ break;
166
+ case 'not_started':
167
+ symbol = SYMBOLS.pending;
168
+ break;
169
+ default:
170
+ symbol = SYMBOLS.pending;
171
+ }
172
+ return `${symbol} ${stage}: ${status}`;
173
+ }
174
+
175
+ // ─── Progress Bar ────────────────────────────────────────────────────────────
176
+
177
+ /**
178
+ * Render an ASCII progress bar.
179
+ *
180
+ * ```
181
+ * Progress: ████████░░ 80%
182
+ * ```
183
+ *
184
+ * @param current - Current value
185
+ * @param total - Total value
186
+ * @param width - Bar width in characters (default 10)
187
+ */
188
+ export function renderProgressBar(
189
+ current: number,
190
+ total: number,
191
+ width: number = 10,
192
+ ): string {
193
+ const ratio = total === 0 ? 0 : Math.min(current / total, 1);
194
+ const filled = Math.round(ratio * width);
195
+ const empty = width - filled;
196
+ const percent = Math.round(ratio * 100);
197
+
198
+ const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(empty);
199
+ return `Progress: ${bar} ${percent}%`;
200
+ }
@@ -0,0 +1,332 @@
1
+ /**
2
+ * GSWD Specify Agents Module — Agent definitions, sequential-then-parallel orchestration
3
+ *
4
+ * Provides the 3 specify agents: journey-mapper (sequential), then
5
+ * architecture-drafter + integrations-checker (parallel).
6
+ *
7
+ * Follows the imagine-agents.ts pattern but with phased orchestration.
8
+ *
9
+ * Schema: GSWD_SPEC.md Section 8.3, Section 11.1
10
+ */
11
+
12
+ import * as fs from 'node:fs';
13
+ import type { Journey, FunctionalRequirement } from './specify-journeys.js';
14
+
15
+ // ─── Types ───────────────────────────────────────────────────────────────────
16
+
17
+ export interface SpecifyAgentDefinition {
18
+ /** Agent name matching file name (without .md) */
19
+ name: string;
20
+ /** Path to agent definition file */
21
+ definitionPath: string;
22
+ /** Phase: 'sequential' runs first, 'parallel' runs after sequential completes */
23
+ phase: 'sequential' | 'parallel';
24
+ /** Minimum headings expected in agent output */
25
+ requiredHeadings: string[];
26
+ }
27
+
28
+ export interface Integration {
29
+ /** Integration ID in I-001 format */
30
+ id: string;
31
+ /** Integration name */
32
+ name: string;
33
+ /** Steps to set up the integration */
34
+ setupSteps: string[];
35
+ /** Authentication method */
36
+ authMethod: string;
37
+ /** Monthly cost or quota description */
38
+ costQuota: string;
39
+ /** Fallback if integration unavailable */
40
+ fallback: string;
41
+ /** Approval status — MANDATORY field */
42
+ status: 'approved' | 'deferred with fallback' | 'rejected';
43
+ }
44
+
45
+ export interface ArchitectureComponent {
46
+ /** Component ID in C-001 format */
47
+ id: string;
48
+ /** Component name */
49
+ name: string;
50
+ /** What this component does */
51
+ responsibility: string;
52
+ /** Other C-XXX IDs this depends on */
53
+ dependencies: string[];
54
+ /** FR-XXX IDs this component implements */
55
+ linkedFRs: string[];
56
+ }
57
+
58
+ export interface AgentResult {
59
+ /** Agent name */
60
+ agent: string;
61
+ /** Agent output content */
62
+ content: string;
63
+ /** Completion status */
64
+ status: 'complete' | 'failed';
65
+ /** Error message if failed */
66
+ error?: string;
67
+ /** Execution time in milliseconds */
68
+ duration_ms?: number;
69
+ }
70
+
71
+ export type SpawnFn = (prompt: string) => Promise<string>;
72
+
73
+ // ─── Agent Definitions ───────────────────────────────────────────────────────
74
+
75
+ /**
76
+ * The 3 specify agents from GSWD_SPEC Section 11.1.
77
+ * journey-mapper runs first (sequential); others run after (parallel).
78
+ */
79
+ export const SPECIFY_AGENTS: SpecifyAgentDefinition[] = [
80
+ {
81
+ name: 'journey-mapper',
82
+ definitionPath: 'agents/gswd/journey-mapper.md',
83
+ phase: 'sequential',
84
+ requiredHeadings: ['### J-'],
85
+ },
86
+ {
87
+ name: 'architecture-drafter',
88
+ definitionPath: 'agents/gswd/architecture-drafter.md',
89
+ phase: 'parallel',
90
+ requiredHeadings: ['### Components', '### Data Model'],
91
+ },
92
+ {
93
+ name: 'integrations-checker',
94
+ definitionPath: 'agents/gswd/integrations-checker.md',
95
+ phase: 'parallel',
96
+ requiredHeadings: ['## Integrations'],
97
+ },
98
+ ];
99
+
100
+ // ─── Prompt Building ─────────────────────────────────────────────────────────
101
+
102
+ export interface SpecifyAgentContext {
103
+ /** DECISIONS.md content */
104
+ decisionsContent: string;
105
+ /** IMAGINE.md content (optional) */
106
+ imagineContent?: string;
107
+ /** Extracted journeys (available after journey-mapper) */
108
+ journeys?: Journey[];
109
+ /** Extracted FRs (available after FR extraction) */
110
+ frs?: FunctionalRequirement[];
111
+ /** Auto policy config content */
112
+ autoPolicy?: string;
113
+ }
114
+
115
+ /**
116
+ * Build the prompt for a specify agent.
117
+ *
118
+ * - journey-mapper: gets DECISIONS.md + IMAGINE.md
119
+ * - architecture-drafter: gets journeys + FRs + DECISIONS.md
120
+ * - integrations-checker: gets journeys + FRs + DECISIONS.md + auto policy
121
+ */
122
+ export function buildSpecifyAgentPrompt(
123
+ agent: SpecifyAgentDefinition,
124
+ context: SpecifyAgentContext,
125
+ ): string {
126
+ let definition: string;
127
+ try {
128
+ definition = fs.readFileSync(agent.definitionPath, 'utf-8');
129
+ } catch {
130
+ definition = `Agent: ${agent.name}\nRole: Specify agent`;
131
+ }
132
+
133
+ const sections: string[] = [definition];
134
+
135
+ // Always include decisions
136
+ if (context.decisionsContent) {
137
+ sections.push(`<decisions>\n${context.decisionsContent}\n</decisions>`);
138
+ }
139
+
140
+ switch (agent.name) {
141
+ case 'journey-mapper':
142
+ if (context.imagineContent) {
143
+ sections.push(`<imagine>\n${context.imagineContent}\n</imagine>`);
144
+ }
145
+ break;
146
+
147
+ case 'architecture-drafter':
148
+ if (context.frs && context.frs.length > 0) {
149
+ const frSummary = context.frs
150
+ .map((fr) => `- ${fr.id}: ${fr.description} [${fr.scope}/${fr.priority}]`)
151
+ .join('\n');
152
+ sections.push(`<functional_requirements>\n${frSummary}\n</functional_requirements>`);
153
+ }
154
+ if (context.journeys && context.journeys.length > 0) {
155
+ const journeySummary = context.journeys
156
+ .map((j) => `- ${j.id}: ${j.name} (${j.type}, ${j.steps.length} steps)`)
157
+ .join('\n');
158
+ sections.push(`<journeys>\n${journeySummary}\n</journeys>`);
159
+ }
160
+ break;
161
+
162
+ case 'integrations-checker':
163
+ if (context.frs && context.frs.length > 0) {
164
+ const frSummary = context.frs
165
+ .map((fr) => `- ${fr.id}: ${fr.description} [${fr.scope}/${fr.priority}]`)
166
+ .join('\n');
167
+ sections.push(`<functional_requirements>\n${frSummary}\n</functional_requirements>`);
168
+ }
169
+ if (context.journeys && context.journeys.length > 0) {
170
+ const journeySummary = context.journeys
171
+ .map((j) => `- ${j.id}: ${j.name} (${j.type}, ${j.steps.length} steps)`)
172
+ .join('\n');
173
+ sections.push(`<journeys>\n${journeySummary}\n</journeys>`);
174
+ }
175
+ if (context.autoPolicy) {
176
+ sections.push(`<auto_policy>\n${context.autoPolicy}\n</auto_policy>`);
177
+ }
178
+ break;
179
+ }
180
+
181
+ sections.push(
182
+ `Produce your output now. Include all required headings: ${agent.requiredHeadings.join(', ')}.`
183
+ );
184
+
185
+ return sections.join('\n\n');
186
+ }
187
+
188
+ // ─── Orchestration ───────────────────────────────────────────────────────────
189
+
190
+ /**
191
+ * Orchestrate specify agents with sequential-then-parallel pattern.
192
+ *
193
+ * Phase 1 (sequential): Run journey-mapper, wait for completion.
194
+ * Phase 2 (parallel): Run architecture-drafter + integrations-checker concurrently.
195
+ *
196
+ * Failed agents are marked with status: 'failed' but do not crash orchestration.
197
+ *
198
+ * @param agents - Agent definitions (defaults to SPECIFY_AGENTS)
199
+ * @param context - Shared context for prompt building
200
+ * @param spawnFn - Function that spawns an agent (Task() wrapper)
201
+ * @returns All agent results including failures
202
+ */
203
+ export async function orchestrateSpecifyAgents(
204
+ agents: SpecifyAgentDefinition[],
205
+ context: SpecifyAgentContext,
206
+ spawnFn: SpawnFn,
207
+ ): Promise<AgentResult[]> {
208
+ const results: AgentResult[] = [];
209
+
210
+ // Phase 1: Sequential agents
211
+ const sequentialAgents = agents.filter((a) => a.phase === 'sequential');
212
+ for (const agent of sequentialAgents) {
213
+ const result = await runSingleAgent(agent, context, spawnFn);
214
+ results.push(result);
215
+ }
216
+
217
+ // Phase 2: Parallel agents
218
+ const parallelAgents = agents.filter((a) => a.phase === 'parallel');
219
+ if (parallelAgents.length > 0) {
220
+ const parallelPromises = parallelAgents.map((agent) =>
221
+ runSingleAgent(agent, context, spawnFn)
222
+ );
223
+ const parallelResults = await Promise.all(parallelPromises);
224
+ results.push(...parallelResults);
225
+ }
226
+
227
+ return results;
228
+ }
229
+
230
+ /**
231
+ * Run a single agent and return its result.
232
+ */
233
+ async function runSingleAgent(
234
+ agent: SpecifyAgentDefinition,
235
+ context: SpecifyAgentContext,
236
+ spawnFn: SpawnFn,
237
+ ): Promise<AgentResult> {
238
+ const start = Date.now();
239
+ try {
240
+ const prompt = buildSpecifyAgentPrompt(agent, context);
241
+ const content = await spawnFn(prompt);
242
+ return {
243
+ agent: agent.name,
244
+ content,
245
+ status: 'complete',
246
+ duration_ms: Date.now() - start,
247
+ };
248
+ } catch (err: unknown) {
249
+ const message = err instanceof Error ? err.message : String(err);
250
+ return {
251
+ agent: agent.name,
252
+ content: '',
253
+ status: 'failed',
254
+ error: message,
255
+ duration_ms: Date.now() - start,
256
+ };
257
+ }
258
+ }
259
+
260
+ // ─── Integration Validation ──────────────────────────────────────────────────
261
+
262
+ /**
263
+ * Valid integration status values.
264
+ */
265
+ export const VALID_INTEGRATION_STATUSES = [
266
+ 'approved',
267
+ 'deferred with fallback',
268
+ 'rejected',
269
+ ] as const;
270
+
271
+ /**
272
+ * Validate that an integration has all required fields including mandatory Status.
273
+ */
274
+ export function validateIntegration(
275
+ integration: Integration,
276
+ ): { valid: boolean; errors: string[] } {
277
+ const errors: string[] = [];
278
+
279
+ // ID format
280
+ if (!/^I-\d{3,}$/.test(integration.id)) {
281
+ errors.push(`Invalid integration ID format: ${integration.id} (expected I-XXX)`);
282
+ }
283
+
284
+ // Name non-empty
285
+ if (!integration.name || integration.name.trim() === '') {
286
+ errors.push(`Integration ${integration.id} has empty name`);
287
+ }
288
+
289
+ // Status is mandatory and must be a valid value
290
+ if (!VALID_INTEGRATION_STATUSES.includes(integration.status)) {
291
+ errors.push(
292
+ `Integration ${integration.id} has invalid status: "${integration.status}" (expected: ${VALID_INTEGRATION_STATUSES.join(', ')})`
293
+ );
294
+ }
295
+
296
+ // Deferred integrations must have a fallback
297
+ if (integration.status === 'deferred with fallback') {
298
+ if (!integration.fallback || integration.fallback.trim() === '') {
299
+ errors.push(
300
+ `Integration ${integration.id} is deferred but has no fallback`
301
+ );
302
+ }
303
+ }
304
+
305
+ return { valid: errors.length === 0, errors };
306
+ }
307
+
308
+ /**
309
+ * Validate that a component has correct ID format and required fields.
310
+ */
311
+ export function validateComponent(
312
+ component: ArchitectureComponent,
313
+ ): { valid: boolean; errors: string[] } {
314
+ const errors: string[] = [];
315
+
316
+ // ID format
317
+ if (!/^C-\d{3,}$/.test(component.id)) {
318
+ errors.push(`Invalid component ID format: ${component.id} (expected C-XXX)`);
319
+ }
320
+
321
+ // Name non-empty
322
+ if (!component.name || component.name.trim() === '') {
323
+ errors.push(`Component ${component.id} has empty name`);
324
+ }
325
+
326
+ // Responsibility non-empty
327
+ if (!component.responsibility || component.responsibility.trim() === '') {
328
+ errors.push(`Component ${component.id} has empty responsibility`);
329
+ }
330
+
331
+ return { valid: errors.length === 0, errors };
332
+ }