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/config.ts ADDED
@@ -0,0 +1,164 @@
1
+ /**
2
+ * GSWD Config Module — config.json merge under gswd key
3
+ *
4
+ * Config is stored in .planning/config.json under the `gswd` key.
5
+ * Merge operations NEVER touch keys outside `gswd`.
6
+ *
7
+ * Schema: GSWD_SPEC.md Section 5.2
8
+ */
9
+
10
+ import * as fs from 'node:fs';
11
+ import { safeWriteJson } from './state.js';
12
+
13
+ // ─── Types ───────────────────────────────────────────────────────────────────
14
+
15
+ export interface GswdAutoConfig {
16
+ policy: 'strict' | 'balanced' | 'aggressive';
17
+ allow_paid_integrations_under_budget: boolean;
18
+ default_auth_model: string;
19
+ default_data_store: string;
20
+ default_stack: string;
21
+ preapproved_integrations: string[];
22
+ }
23
+
24
+ export interface GswdConfig {
25
+ mode: string;
26
+ strict_gates: boolean;
27
+ max_parallel_agents: number;
28
+ external_research: boolean;
29
+ integration_budget_usd_month: number;
30
+ doc_verbosity: string;
31
+ phase_style: string;
32
+ auto: GswdAutoConfig;
33
+ [key: string]: unknown;
34
+ }
35
+
36
+ // ─── Defaults ────────────────────────────────────────────────────────────────
37
+
38
+ /**
39
+ * Default GSWD config matching GSWD_SPEC Section 5.2.
40
+ */
41
+ export const GSWD_CONFIG_DEFAULTS: GswdConfig = {
42
+ mode: 'balanced',
43
+ strict_gates: true,
44
+ max_parallel_agents: 4,
45
+ external_research: true,
46
+ integration_budget_usd_month: 100,
47
+ doc_verbosity: 'normal',
48
+ phase_style: 'thin',
49
+ auto: {
50
+ policy: 'balanced',
51
+ allow_paid_integrations_under_budget: false,
52
+ default_auth_model: 'passwordless_email',
53
+ default_data_store: 'sqlite',
54
+ default_stack: 'node_typescript',
55
+ preapproved_integrations: [],
56
+ },
57
+ };
58
+
59
+ // ─── Config CRUD ─────────────────────────────────────────────────────────────
60
+
61
+ /**
62
+ * Read config.json. Returns empty object if missing or invalid.
63
+ */
64
+ export function readConfig(configPath: string): Record<string, unknown> {
65
+ try {
66
+ const content = fs.readFileSync(configPath, 'utf-8');
67
+ const parsed = JSON.parse(content);
68
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
69
+ return {};
70
+ }
71
+ return parsed as Record<string, unknown>;
72
+ } catch {
73
+ return {};
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Deep merge helper: merge source into target, source values take precedence.
79
+ * Only merges plain objects recursively; arrays and primitives are replaced.
80
+ */
81
+ function deepMerge(
82
+ target: Record<string, unknown>,
83
+ source: Record<string, unknown>
84
+ ): Record<string, unknown> {
85
+ const result: Record<string, unknown> = { ...target };
86
+
87
+ for (const key of Object.keys(source)) {
88
+ const sourceVal = source[key];
89
+ const targetVal = result[key];
90
+
91
+ if (
92
+ typeof sourceVal === 'object' &&
93
+ sourceVal !== null &&
94
+ !Array.isArray(sourceVal) &&
95
+ typeof targetVal === 'object' &&
96
+ targetVal !== null &&
97
+ !Array.isArray(targetVal)
98
+ ) {
99
+ result[key] = deepMerge(
100
+ targetVal as Record<string, unknown>,
101
+ sourceVal as Record<string, unknown>
102
+ );
103
+ } else {
104
+ result[key] = sourceVal;
105
+ }
106
+ }
107
+
108
+ return result;
109
+ }
110
+
111
+ /**
112
+ * Merge GSWD config into config.json under the `gswd` key.
113
+ * - Reads existing config (preserves ALL non-gswd keys)
114
+ * - Deep merges: existing gswd values take precedence over defaults
115
+ * - Fills in missing defaults
116
+ * - Writes back atomically
117
+ *
118
+ * CRITICAL: Never touches any key outside `gswd`.
119
+ */
120
+ export function mergeGswdConfig(
121
+ configPath: string,
122
+ gswdOverrides?: Partial<GswdConfig>
123
+ ): void {
124
+ // Read existing config (preserves GSD keys, etc.)
125
+ const existing = readConfig(configPath);
126
+
127
+ // Start with defaults
128
+ let mergedGswd: Record<string, unknown> = { ...GSWD_CONFIG_DEFAULTS } as Record<string, unknown>;
129
+
130
+ // Apply overrides on top of defaults
131
+ if (gswdOverrides) {
132
+ mergedGswd = deepMerge(mergedGswd, gswdOverrides as Record<string, unknown>);
133
+ }
134
+
135
+ // Apply existing user values on top (user values take precedence)
136
+ const existingGswd = existing.gswd;
137
+ if (typeof existingGswd === 'object' && existingGswd !== null && !Array.isArray(existingGswd)) {
138
+ mergedGswd = deepMerge(mergedGswd, existingGswd as Record<string, unknown>);
139
+ }
140
+
141
+ // Write back: existing config with updated gswd key
142
+ existing.gswd = mergedGswd;
143
+ safeWriteJson(configPath, existing);
144
+ }
145
+
146
+ /**
147
+ * Get GSWD config with all defaults filled in for missing fields.
148
+ */
149
+ export function getGswdConfig(configPath: string): GswdConfig {
150
+ const config = readConfig(configPath);
151
+ const gswdRaw = config.gswd;
152
+
153
+ if (typeof gswdRaw !== 'object' || gswdRaw === null || Array.isArray(gswdRaw)) {
154
+ return { ...GSWD_CONFIG_DEFAULTS };
155
+ }
156
+
157
+ // Merge defaults under existing values
158
+ const merged = deepMerge(
159
+ { ...GSWD_CONFIG_DEFAULTS } as Record<string, unknown>,
160
+ gswdRaw as Record<string, unknown>
161
+ );
162
+
163
+ return merged as GswdConfig;
164
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * GSWD Imagine Agents Module — Agent definitions, orchestration, and result collection
3
+ *
4
+ * Provides batched parallel agent spawning that respects max_parallel_agents config.
5
+ * Agent definitions describe the 5 research agents from GSWD_SPEC Section 11.1.
6
+ * Actual Task() integration happens in the workflow orchestrator (imagine.ts).
7
+ *
8
+ * Schema: GSWD_SPEC.md Section 8.2 Steps 3-4, Section 11.1
9
+ */
10
+
11
+ import * as fs from 'node:fs';
12
+ import type { StarterBrief } from './imagine-input.js';
13
+
14
+ // ─── Types ───────────────────────────────────────────────────────────────────
15
+
16
+ export interface AgentDefinition {
17
+ name: string; // e.g., 'market-researcher'
18
+ definitionPath: string; // e.g., 'agents/gswd/market-researcher.md'
19
+ outputArtifact: string; // e.g., 'COMPETITION.md' (which artifact this feeds)
20
+ requiredHeadings: string[]; // Minimum output validation
21
+ }
22
+
23
+ export interface AgentResult {
24
+ agent: string; // Agent name
25
+ content: string; // Agent's output content
26
+ status: 'complete' | 'failed';
27
+ error?: string; // Error message if failed
28
+ duration_ms?: number; // Execution time
29
+ }
30
+
31
+ export type SpawnFn = (prompt: string) => Promise<string>;
32
+
33
+ // ─── Agent Definitions ───────────────────────────────────────────────────────
34
+
35
+ /**
36
+ * The 5 research agents for the Imagine stage.
37
+ * Names match GSWD_SPEC Section 11.1 file names.
38
+ */
39
+ export const IMAGINE_AGENTS: AgentDefinition[] = [
40
+ {
41
+ name: 'market-researcher',
42
+ definitionPath: 'agents/gswd/market-researcher.md',
43
+ outputArtifact: 'COMPETITION.md',
44
+ requiredHeadings: ['## Market Overview', '## Competitors'],
45
+ },
46
+ {
47
+ name: 'icp-persona',
48
+ definitionPath: 'agents/gswd/icp-persona.md',
49
+ outputArtifact: 'ICP.md',
50
+ requiredHeadings: ['## ICP Profile', '## Pain Points'],
51
+ },
52
+ {
53
+ name: 'positioning',
54
+ definitionPath: 'agents/gswd/positioning.md',
55
+ outputArtifact: 'GTM.md',
56
+ requiredHeadings: ['## Value Proposition'],
57
+ },
58
+ {
59
+ name: 'brainstorm-alternatives',
60
+ definitionPath: 'agents/gswd/brainstorm-alternatives.md',
61
+ outputArtifact: 'DIRECTIONS',
62
+ requiredHeadings: ['## Direction 1', '## Direction 2', '## Direction 3'],
63
+ },
64
+ {
65
+ name: 'devils-advocate',
66
+ definitionPath: 'agents/gswd/devils-advocate.md',
67
+ outputArtifact: 'RISKS',
68
+ requiredHeadings: ['## Risks'],
69
+ },
70
+ ];
71
+
72
+ // ─── Prompt Building ─────────────────────────────────────────────────────────
73
+
74
+ /**
75
+ * Build the prompt that will be passed to Task() for an agent.
76
+ *
77
+ * Reads agent definition file and injects StarterBrief as context.
78
+ */
79
+ export function buildAgentPrompt(agent: AgentDefinition, brief: StarterBrief): string {
80
+ let definition: string;
81
+ try {
82
+ definition = fs.readFileSync(agent.definitionPath, 'utf-8');
83
+ } catch {
84
+ definition = `Agent: ${agent.name}\nRole: Research agent for ${agent.outputArtifact}`;
85
+ }
86
+
87
+ const briefJson = JSON.stringify(brief, null, 2);
88
+
89
+ return `${definition}
90
+
91
+ <starter_brief>
92
+ ${briefJson}
93
+ </starter_brief>
94
+
95
+ Produce your output now. Include all required headings: ${agent.requiredHeadings.join(', ')}.`;
96
+ }
97
+
98
+ // ─── Orchestration ───────────────────────────────────────────────────────────
99
+
100
+ /**
101
+ * Orchestrate agents in batches respecting maxParallel concurrency limit.
102
+ *
103
+ * Splits agents into batches of maxParallel size, executes each batch
104
+ * concurrently, collects results. Failed agents are marked with status: 'failed'
105
+ * but do not crash the orchestration.
106
+ *
107
+ * @param agents - Array of agent definitions to spawn
108
+ * @param maxParallel - Maximum concurrent agents (from config.max_parallel_agents)
109
+ * @param brief - StarterBrief input for all agents
110
+ * @param spawnFn - Function that spawns an agent (Task() wrapper)
111
+ * @returns All agent results including failures
112
+ */
113
+ export async function orchestrateAgents(
114
+ agents: AgentDefinition[],
115
+ maxParallel: number,
116
+ brief: StarterBrief,
117
+ spawnFn: SpawnFn,
118
+ ): Promise<AgentResult[]> {
119
+ const results: AgentResult[] = [];
120
+
121
+ // Split into batches
122
+ for (let i = 0; i < agents.length; i += maxParallel) {
123
+ const batch = agents.slice(i, i + maxParallel);
124
+
125
+ // Execute batch in parallel
126
+ const batchPromises = batch.map(async (agent): Promise<AgentResult> => {
127
+ const start = Date.now();
128
+ try {
129
+ const prompt = buildAgentPrompt(agent, brief);
130
+ const content = await spawnFn(prompt);
131
+ return {
132
+ agent: agent.name,
133
+ content,
134
+ status: 'complete',
135
+ duration_ms: Date.now() - start,
136
+ };
137
+ } catch (err: unknown) {
138
+ const message = err instanceof Error ? err.message : String(err);
139
+ return {
140
+ agent: agent.name,
141
+ content: '',
142
+ status: 'failed',
143
+ error: message,
144
+ duration_ms: Date.now() - start,
145
+ };
146
+ }
147
+ });
148
+
149
+ const batchResults = await Promise.all(batchPromises);
150
+ results.push(...batchResults);
151
+ }
152
+
153
+ return results;
154
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * GSWD Imagine Gate Module — Decision gate validation for DECISIONS.md
3
+ *
4
+ * Pure function: validates DECISIONS.md against hard gate requirements.
5
+ * No side effects, no file writes, no state mutations.
6
+ *
7
+ * Gate requirements (GSWD_SPEC Section 3.3, Appendix A1):
8
+ * - ## Frozen Decisions: >= 8 items
9
+ * - ## Success Metrics: 1-3 items
10
+ * - ## Out of Scope: >= 1 item
11
+ * - ## Risks & Mitigations: >= 5 items
12
+ * - ## Open Questions: >= 0 items (section must exist)
13
+ */
14
+
15
+ import { validateHeadings, extractHeadingContent } from './parse.js';
16
+
17
+ // ─── Types ───────────────────────────────────────────────────────────────────
18
+
19
+ export interface InsufficientCount {
20
+ section: string;
21
+ required: number;
22
+ actual: number;
23
+ constraint?: string; // e.g., "1-3" for metrics range
24
+ }
25
+
26
+ export interface GateResult {
27
+ passed: boolean;
28
+ missing_sections: string[];
29
+ insufficient_counts: InsufficientCount[];
30
+ summary: string;
31
+ }
32
+
33
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
34
+
35
+ /**
36
+ * Count list items in a section.
37
+ *
38
+ * Counts lines starting with:
39
+ * - `- ` (bullet)
40
+ * - `* ` (bullet)
41
+ * - `N. ` or `N) ` (numbered)
42
+ * - `### ` (sub-headings as items)
43
+ *
44
+ * Returns 0 for empty/null/undefined content.
45
+ */
46
+ export function countListItems(content: string | null | undefined): number {
47
+ if (!content) return 0;
48
+
49
+ const lines = content.split('\n');
50
+ let count = 0;
51
+
52
+ for (const line of lines) {
53
+ const trimmed = line.trim();
54
+ if (/^[-*]\s+/.test(trimmed)) {
55
+ count++;
56
+ } else if (/^\d+[.)]\s+/.test(trimmed)) {
57
+ count++;
58
+ } else if (/^###\s+/.test(trimmed)) {
59
+ count++;
60
+ }
61
+ }
62
+
63
+ return count;
64
+ }
65
+
66
+ // ─── Gate Validation ─────────────────────────────────────────────────────────
67
+
68
+ /**
69
+ * Validate DECISIONS.md against the hard gate requirements.
70
+ *
71
+ * Step 1: Required headings (via parse.ts validateHeadings)
72
+ * Step 2: Item counts per section (via parse.ts extractHeadingContent + countListItems)
73
+ * Step 3: Build structured result
74
+ *
75
+ * Returns: GateResult with passed/failed status, missing sections, and count issues.
76
+ */
77
+ export function validateDecisionGate(decisionsContent: string): GateResult {
78
+ // Step 1: Required headings
79
+ const headingResult = validateHeadings(decisionsContent, 'DECISIONS.md');
80
+
81
+ // Step 2: Count items per section
82
+ const insufficient: InsufficientCount[] = [];
83
+
84
+ // Frozen Decisions: >= 8
85
+ const frozenSection = extractHeadingContent(decisionsContent, '## Frozen Decisions');
86
+ const frozenCount = countListItems(frozenSection);
87
+ if (frozenCount < 8) {
88
+ insufficient.push({
89
+ section: 'Frozen Decisions',
90
+ required: 8,
91
+ actual: frozenCount,
92
+ });
93
+ }
94
+
95
+ // Success Metrics: 1-3
96
+ const metricsSection = extractHeadingContent(decisionsContent, '## Success Metrics');
97
+ const metricsCount = countListItems(metricsSection);
98
+ if (metricsCount < 1 || metricsCount > 3) {
99
+ insufficient.push({
100
+ section: 'Success Metrics',
101
+ required: metricsCount < 1 ? 1 : 3,
102
+ actual: metricsCount,
103
+ constraint: '1-3',
104
+ });
105
+ }
106
+
107
+ // Out of Scope: >= 1
108
+ const outOfScopeSection = extractHeadingContent(decisionsContent, '## Out of Scope');
109
+ const outOfScopeCount = countListItems(outOfScopeSection);
110
+ if (outOfScopeCount < 1) {
111
+ insufficient.push({
112
+ section: 'Out of Scope',
113
+ required: 1,
114
+ actual: outOfScopeCount,
115
+ });
116
+ }
117
+
118
+ // Risks & Mitigations: >= 5
119
+ const risksSection = extractHeadingContent(decisionsContent, '## Risks & Mitigations');
120
+ const risksCount = countListItems(risksSection);
121
+ if (risksCount < 5) {
122
+ insufficient.push({
123
+ section: 'Risks & Mitigations',
124
+ required: 5,
125
+ actual: risksCount,
126
+ });
127
+ }
128
+
129
+ // Open Questions: >= 0 (section must exist, but can be empty)
130
+ // No count check needed — heading presence is sufficient
131
+
132
+ // Step 3: Build result
133
+ const passed = headingResult.valid && insufficient.length === 0;
134
+ const totalIssues = headingResult.missing.length + insufficient.length;
135
+
136
+ let summary: string;
137
+ if (passed) {
138
+ summary = 'PASS: All 5 sections present, counts valid';
139
+ } else {
140
+ const issues: string[] = [];
141
+ if (headingResult.missing.length > 0) {
142
+ issues.push(`${headingResult.missing.length} missing heading(s)`);
143
+ }
144
+ if (insufficient.length > 0) {
145
+ issues.push(`${insufficient.length} insufficient count(s)`);
146
+ }
147
+ summary = `FAIL: ${totalIssues} issue(s) — ${issues.join(', ')}`;
148
+ }
149
+
150
+ return {
151
+ passed,
152
+ missing_sections: headingResult.missing,
153
+ insufficient_counts: insufficient,
154
+ summary,
155
+ };
156
+ }