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/specify.ts ADDED
@@ -0,0 +1,773 @@
1
+ /**
2
+ * GSWD Specify Workflow Orchestrator
3
+ *
4
+ * Full pipeline: roles checkpoint -> journey mapping -> FR extraction ->
5
+ * NFR generation -> architecture + integrations -> write artifacts -> state
6
+ * Implements GSWD_SPEC Section 8.3 end-to-end.
7
+ *
8
+ * Both interactive and auto modes are supported:
9
+ * - Interactive: presents checkpoints for roles, journey review, FR confirmation
10
+ * - Auto: uses defaults, skips reviews, auto-defers paid integrations
11
+ *
12
+ * Schema: GSWD_SPEC.md Section 8.3
13
+ */
14
+
15
+ import * as fs from 'node:fs';
16
+ import * as path from 'node:path';
17
+
18
+ import { safeWriteFile, readState, writeState, writeCheckpoint, allocateIdRange } from './state.js';
19
+ import { getGswdConfig } from './config.js';
20
+ import { validateHeadings } from './parse.js';
21
+ import { collectRoles, formatRolesForSpec } from './specify-roles.js';
22
+ import type { RolesConfig } from './specify-roles.js';
23
+ import {
24
+ JOURNEY_TYPES,
25
+ generateJourneyStructure,
26
+ validateJourney,
27
+ extractFRsFromJourneys,
28
+ buildTraceabilityMap,
29
+ validateFRCoverage,
30
+ assignScope,
31
+ assignPriority,
32
+ } from './specify-journeys.js';
33
+ import type { Journey, JourneyStep, FailureMode, FunctionalRequirement } from './specify-journeys.js';
34
+ import { generateNFRs, validateNFRs } from './specify-nfr.js';
35
+ import type { NonFunctionalRequirement } from './specify-nfr.js';
36
+ import {
37
+ SPECIFY_AGENTS,
38
+ orchestrateSpecifyAgents,
39
+ validateIntegration,
40
+ validateComponent,
41
+ } from './specify-agents.js';
42
+ import type {
43
+ SpecifyAgentContext,
44
+ Integration,
45
+ ArchitectureComponent,
46
+ AgentResult,
47
+ SpawnFn,
48
+ } from './specify-agents.js';
49
+
50
+ // ─── Types ───────────────────────────────────────────────────────────────────
51
+
52
+ export interface SpecifyOptions {
53
+ /** Use auto policy for all checkpoints */
54
+ auto: boolean;
55
+ /** Resume from last checkpoint */
56
+ resume?: boolean;
57
+ /** Integration budget override */
58
+ integrationBudget?: number;
59
+ /** Override .planning/ path (for testing) */
60
+ planningDir?: string;
61
+ /** Override config.json path (for testing) */
62
+ configPath?: string;
63
+ /** Override templates directory path (for testing) */
64
+ templatesDir?: string;
65
+ /** Task() wrapper for agent spawning (injectable for testing) */
66
+ spawnFn?: SpawnFn;
67
+ /** Skip agent spawning, use provided journeys directly (for testing) */
68
+ skipAgents?: boolean;
69
+ /** Pre-built journeys (for testing or resume) */
70
+ providedJourneys?: Journey[];
71
+ /** Pre-built architecture content (for testing) */
72
+ providedArchitecture?: string;
73
+ /** Pre-built integrations content (for testing) */
74
+ providedIntegrations?: string;
75
+ }
76
+
77
+ export interface SpecifyResult {
78
+ /** Overall status */
79
+ status: 'complete' | 'failed' | 'interrupted';
80
+ /** File paths of written artifacts */
81
+ artifacts: string[];
82
+ /** Count of journeys generated */
83
+ journeyCount: number;
84
+ /** Count of FRs extracted */
85
+ frCount: number;
86
+ /** Count of NFRs generated */
87
+ nfrCount: number;
88
+ /** Count of integrations */
89
+ integrationCount: number;
90
+ /** Count of architecture components */
91
+ componentCount: number;
92
+ /** Error messages if any */
93
+ errors?: string[];
94
+ }
95
+
96
+ // ─── Default Journey Content (for auto/skipAgents mode) ─────────────────────
97
+
98
+ /**
99
+ * Build default journeys from DECISIONS.md context.
100
+ * Used when skipAgents=true or as fallback.
101
+ */
102
+ function buildDefaultJourneys(decisionsContent: string): Journey[] {
103
+ const journeys: Journey[] = [];
104
+ let counter = 1;
105
+
106
+ // Onboarding journey
107
+ const onboarding = generateJourneyStructure('onboarding', counter++);
108
+ onboarding.name = 'New User Onboarding';
109
+ onboarding.preconditions = ['User has not previously used the application'];
110
+ onboarding.steps = [
111
+ { number: 1, action: 'User navigates to application landing page', frIds: [] },
112
+ { number: 2, action: 'User clicks sign up / get started button', frIds: [] },
113
+ { number: 3, action: 'User provides account details', frIds: [] },
114
+ { number: 4, action: 'User confirms account creation', frIds: [] },
115
+ { number: 5, action: 'User completes onboarding walkthrough', frIds: [] },
116
+ ];
117
+ onboarding.success = 'User has an active account and sees the main dashboard';
118
+ onboarding.failureModes = [
119
+ { scenario: 'Invalid email format provided during sign up', handling: 'Show inline validation error, prevent form submission' },
120
+ { scenario: 'Network timeout during account creation', handling: 'Show retry button with "Connection lost" message' },
121
+ ];
122
+ onboarding.acceptanceTests = ['User can create an account and reach the dashboard within 3 steps after landing'];
123
+ journeys.push(onboarding);
124
+
125
+ // Core action journey
126
+ const coreAction = generateJourneyStructure('core_action', counter++);
127
+ coreAction.name = 'Core Action';
128
+ coreAction.preconditions = ['User is authenticated', 'User has completed onboarding'];
129
+ coreAction.steps = [
130
+ { number: 1, action: 'User opens main dashboard', frIds: [] },
131
+ { number: 2, action: 'User clicks create new item button', frIds: [] },
132
+ { number: 3, action: 'User fills in required fields in creation form', frIds: [] },
133
+ { number: 4, action: 'User reviews item details before submission', frIds: [] },
134
+ { number: 5, action: 'User submits the new item', frIds: [] },
135
+ { number: 6, action: 'User sees confirmation and item appears in list', frIds: [] },
136
+ ];
137
+ coreAction.success = 'New item is created and visible in the user list';
138
+ coreAction.failureModes = [
139
+ { scenario: 'Required fields left empty on submission', handling: 'Highlight missing fields with error messages' },
140
+ { scenario: 'Server error during item creation', handling: 'Show error toast, preserve form data for retry' },
141
+ ];
142
+ coreAction.acceptanceTests = ['User can create a new item and see it in the list immediately after creation'];
143
+ journeys.push(coreAction);
144
+
145
+ // View results journey
146
+ const viewResults = generateJourneyStructure('view_results', counter++);
147
+ viewResults.name = 'View Results and History';
148
+ viewResults.preconditions = ['User is authenticated', 'User has created at least one item'];
149
+ viewResults.steps = [
150
+ { number: 1, action: 'User navigates to history or results view', frIds: [] },
151
+ { number: 2, action: 'User sees list of previously created items', frIds: [] },
152
+ { number: 3, action: 'User clicks on a specific item to view details', frIds: [] },
153
+ { number: 4, action: 'User reviews item details and status', frIds: [] },
154
+ { number: 5, action: 'User returns to the list view', frIds: [] },
155
+ ];
156
+ viewResults.success = 'User can browse and view details of all their items';
157
+ viewResults.failureModes = [
158
+ { scenario: 'Item details fail to load', handling: 'Show error state with retry option' },
159
+ { scenario: 'List takes too long to load', handling: 'Show skeleton loading state, load in batches' },
160
+ ];
161
+ viewResults.acceptanceTests = ['User can view a list of items and navigate to any item detail page'];
162
+ journeys.push(viewResults);
163
+
164
+ // Settings journey
165
+ const settings = generateJourneyStructure('settings', counter++);
166
+ settings.name = 'Settings and Preferences';
167
+ settings.preconditions = ['User is authenticated'];
168
+ settings.steps = [
169
+ { number: 1, action: 'User navigates to settings page', frIds: [] },
170
+ { number: 2, action: 'User views current account settings', frIds: [] },
171
+ { number: 3, action: 'User modifies a setting value', frIds: [] },
172
+ { number: 4, action: 'User saves updated settings', frIds: [] },
173
+ { number: 5, action: 'User sees confirmation of saved changes', frIds: [] },
174
+ ];
175
+ settings.success = 'User settings are updated and persisted';
176
+ settings.failureModes = [
177
+ { scenario: 'Settings save fails due to validation error', handling: 'Show specific validation error next to the field' },
178
+ { scenario: 'Network error during save', handling: 'Show error toast, keep unsaved changes in form' },
179
+ ];
180
+ settings.acceptanceTests = ['User can modify settings and see them persisted after page reload'];
181
+ journeys.push(settings);
182
+
183
+ // Error states journey
184
+ const errorStates = generateJourneyStructure('error_states', counter++);
185
+ errorStates.name = 'Error States';
186
+ errorStates.preconditions = ['User is authenticated'];
187
+ errorStates.steps = [
188
+ { number: 1, action: 'User performs an action that triggers an error', frIds: [] },
189
+ { number: 2, action: 'System displays contextual error message', frIds: [] },
190
+ { number: 3, action: 'User follows recovery action suggested in error message', frIds: [] },
191
+ ];
192
+ errorStates.success = 'User understands the error and can recover or retry';
193
+ errorStates.failureModes = [
194
+ { scenario: 'Error message is too vague to act on', handling: 'Include specific error code and link to help' },
195
+ { scenario: 'Recovery action fails repeatedly', handling: 'Escalate to support contact with error context attached' },
196
+ ];
197
+ errorStates.acceptanceTests = ['Every error state shows a message with a clear recovery action'];
198
+ journeys.push(errorStates);
199
+
200
+ // Empty states journey
201
+ const emptyStates = generateJourneyStructure('empty_states', counter++);
202
+ emptyStates.name = 'Empty States';
203
+ emptyStates.preconditions = ['User is authenticated', 'User has no items yet'];
204
+ emptyStates.steps = [
205
+ { number: 1, action: 'User navigates to a section with no content', frIds: [] },
206
+ { number: 2, action: 'System shows empty state with call-to-action', frIds: [] },
207
+ { number: 3, action: 'User clicks the call-to-action to create first item', frIds: [] },
208
+ ];
209
+ emptyStates.success = 'User is guided from empty state to creating their first item';
210
+ emptyStates.failureModes = [
211
+ { scenario: 'Call-to-action button not visible on small screens', handling: 'Make CTA responsive and always visible' },
212
+ { scenario: 'Empty state appears even when items exist', handling: 'Show loading indicator before checking data' },
213
+ ];
214
+ emptyStates.acceptanceTests = ['Empty state shows a clear call-to-action that leads to item creation'];
215
+ journeys.push(emptyStates);
216
+
217
+ return journeys;
218
+ }
219
+
220
+ // ─── Artifact Formatting ────────────────────────────────────────────────────
221
+
222
+ /**
223
+ * Format journeys for JOURNEYS.md content.
224
+ */
225
+ function formatJourneysContent(journeys: Journey[]): string {
226
+ return journeys.map((j) => {
227
+ const stepsContent = j.steps
228
+ .map((s) => `${s.number}. ${s.action}`)
229
+ .join('\n');
230
+ const fmContent = j.failureModes
231
+ .map((fm) => `- **${fm.scenario}:** ${fm.handling}`)
232
+ .join('\n');
233
+ const atContent = j.acceptanceTests
234
+ .map((at) => `- ${at}`)
235
+ .join('\n');
236
+ const frRefs = j.linkedFRs.length > 0 ? j.linkedFRs.join(', ') : 'None yet';
237
+ const nfrRefs = j.linkedNFRs.length > 0 ? j.linkedNFRs.join(', ') : 'None yet';
238
+
239
+ return `### ${j.id}: ${j.name}
240
+
241
+ **Type:** ${j.type}
242
+
243
+ **Preconditions:**
244
+ ${j.preconditions.map((p) => `- ${p}`).join('\n')}
245
+
246
+ **Steps:**
247
+ ${stepsContent}
248
+
249
+ **Success:** ${j.success}
250
+
251
+ **Failure Modes:**
252
+ ${fmContent}
253
+
254
+ **Acceptance Tests:**
255
+ ${atContent}
256
+
257
+ **Linked FRs:** ${frRefs}
258
+ **Linked NFRs:** ${nfrRefs}`;
259
+ }).join('\n\n---\n\n');
260
+ }
261
+
262
+ /**
263
+ * Format FRs for SPEC.md content.
264
+ */
265
+ function formatFRsContent(frs: FunctionalRequirement[]): string {
266
+ return frs.map((fr) => {
267
+ const journeyRefs = fr.sourceSteps.join(', ');
268
+ return `### ${fr.id}: ${fr.description}
269
+ **Scope:** ${fr.scope}
270
+ **Priority:** ${fr.priority}
271
+ **Source:** ${journeyRefs}`;
272
+ }).join('\n\n');
273
+ }
274
+
275
+ /**
276
+ * Format acceptance criteria from journeys for SPEC.md.
277
+ */
278
+ function formatAcceptanceCriteria(journeys: Journey[]): string {
279
+ const criteria: string[] = [];
280
+ for (const j of journeys) {
281
+ for (const at of j.acceptanceTests) {
282
+ criteria.push(`- **${j.id}:** ${at}`);
283
+ }
284
+ }
285
+ return criteria.join('\n');
286
+ }
287
+
288
+ /**
289
+ * Format traceability table for SPEC.md.
290
+ */
291
+ function formatTraceabilityTable(frs: FunctionalRequirement[]): string {
292
+ const map = buildTraceabilityMap(frs);
293
+ const lines = [
294
+ '| FR | Description | Source Journeys |',
295
+ '|----|-------------|----------------|',
296
+ ];
297
+ for (const entry of map) {
298
+ lines.push(`| ${entry.frId} | ${entry.description} | ${entry.journeyRefs.join(', ')} |`);
299
+ }
300
+ return lines.join('\n');
301
+ }
302
+
303
+ /**
304
+ * Format NFRs for NFR.md content, grouped by category.
305
+ */
306
+ function formatNFRsContent(nfrs: NonFunctionalRequirement[]): string {
307
+ const categories = ['security', 'privacy', 'performance', 'observability'] as const;
308
+ return categories.map((cat) => {
309
+ const catNfrs = nfrs.filter((n) => n.category === cat);
310
+ const catContent = catNfrs.map((n) => {
311
+ const frRefs = n.linkedFRs.length > 0 ? n.linkedFRs.join(', ') : 'All';
312
+ return `#### ${n.id}: ${n.description}
313
+ **Threshold:** ${n.threshold}
314
+ **Linked FRs:** ${frRefs}`;
315
+ }).join('\n\n');
316
+ return `### ${cat.charAt(0).toUpperCase() + cat.slice(1)}\n\n${catContent}`;
317
+ }).join('\n\n');
318
+ }
319
+
320
+ /**
321
+ * Format default architecture content (used when skipAgents=true).
322
+ */
323
+ function buildDefaultArchitectureContent(frs: FunctionalRequirement[]): string {
324
+ const components: string[] = [];
325
+ components.push(`#### C-001: Application Core
326
+
327
+ **Responsibility:** Handles main application logic and user workflows
328
+ **Dependencies:** None
329
+ **Linked FRs:** ${frs.filter(f => f.priority === 'P0').map(f => f.id).join(', ') || 'All P0 FRs'}`);
330
+
331
+ components.push(`#### C-002: Data Layer
332
+
333
+ **Responsibility:** Manages data persistence and retrieval
334
+ **Dependencies:** C-001
335
+ **Linked FRs:** ${frs.filter(f => f.scope === 'v1').map(f => f.id).slice(0, 5).join(', ') || 'All v1 FRs'}`);
336
+
337
+ components.push(`#### C-003: User Interface
338
+
339
+ **Responsibility:** Renders views and handles user input
340
+ **Dependencies:** C-001
341
+ **Linked FRs:** ${frs.filter(f => f.scope === 'v1').map(f => f.id).slice(0, 5).join(', ') || 'All v1 FRs'}`);
342
+
343
+ const componentsContent = components.join('\n\n');
344
+
345
+ const dataModel = `#### Item
346
+
347
+ | Field | Type | Notes |
348
+ |-------|------|-------|
349
+ | id | string | Primary key |
350
+ | title | string | Required |
351
+ | created_at | timestamp | Auto-set |
352
+ | updated_at | timestamp | Auto-set |
353
+
354
+ #### User
355
+
356
+ | Field | Type | Notes |
357
+ |-------|------|-------|
358
+ | id | string | Primary key |
359
+ | email | string | Unique |
360
+ | created_at | timestamp | Auto-set |
361
+
362
+ **Relationships:**
363
+ - User has many Items
364
+ - Item belongs to User`;
365
+
366
+ const ownership = `| Component | Owns | Operations |
367
+ |-----------|------|------------|
368
+ | C-001: Application Core | Business logic | Workflow orchestration |
369
+ | C-002: Data Layer | Item, User entities | CRUD operations |
370
+ | C-003: User Interface | Views, Forms | Render, Input handling |`;
371
+
372
+ return `${componentsContent}\n\n---\n\n${dataModel}\n\n---\n\n${ownership}`;
373
+ }
374
+
375
+ /**
376
+ * Format default integrations content (no external integrations for v1).
377
+ */
378
+ function buildDefaultIntegrationsContent(): string {
379
+ return 'No external integrations required for v1 MVP. All functionality is self-contained.';
380
+ }
381
+
382
+ // ─── Template Injection ─────────────────────────────────────────────────────
383
+
384
+ /**
385
+ * Inject content into a template at a GSWD:CONTENT marker.
386
+ */
387
+ function injectContent(template: string, marker: string, content: string): string {
388
+ const markerComment = `<!-- GSWD:CONTENT:${marker} -->`;
389
+ return template.replace(markerComment, content);
390
+ }
391
+
392
+ // ─── Main Workflow ──────────────────────────────────────────────────────────
393
+
394
+ /**
395
+ * Run the Specify workflow end-to-end.
396
+ *
397
+ * Implements GSWD_SPEC Section 8.3:
398
+ * 1. Roles & Permissions checkpoint
399
+ * 2. Journey mapping
400
+ * 3. FR extraction
401
+ * 4. NFR generation
402
+ * 5. Architecture + Integrations (parallel agents)
403
+ * 6. Write 5 artifacts
404
+ * 7. Update state
405
+ */
406
+ export async function runSpecify(options: SpecifyOptions): Promise<SpecifyResult> {
407
+ const planningDir = options.planningDir || path.join(process.cwd(), '.planning');
408
+ const gswdDir = path.join(planningDir, 'gswd');
409
+ const statePath = path.join(gswdDir, 'STATE.json');
410
+ const configPath = options.configPath || path.join(planningDir, 'config.json');
411
+ const templatesDir = options.templatesDir || path.join(path.dirname(planningDir), 'templates', 'gswd');
412
+
413
+ const errors: string[] = [];
414
+
415
+ try {
416
+ // ── Step 1: Load state and config ──────────────────────────────────
417
+ const state = readState(statePath);
418
+ if (!state) {
419
+ return {
420
+ status: 'failed',
421
+ artifacts: [],
422
+ journeyCount: 0,
423
+ frCount: 0,
424
+ nfrCount: 0,
425
+ integrationCount: 0,
426
+ componentCount: 0,
427
+ errors: ['No STATE.json found. Run init first.'],
428
+ };
429
+ }
430
+
431
+ const config = getGswdConfig(configPath);
432
+
433
+ // Mark specify as in_progress
434
+ state.stage = 'specify';
435
+ state.stage_status.specify = 'in_progress';
436
+ writeState(statePath, state);
437
+
438
+ // Load DECISIONS.md
439
+ const decisionsPath = path.join(planningDir, 'DECISIONS.md');
440
+ let decisionsContent = '';
441
+ try {
442
+ decisionsContent = fs.readFileSync(decisionsPath, 'utf-8');
443
+ } catch {
444
+ return {
445
+ status: 'failed',
446
+ artifacts: [],
447
+ journeyCount: 0,
448
+ frCount: 0,
449
+ nfrCount: 0,
450
+ integrationCount: 0,
451
+ componentCount: 0,
452
+ errors: ['DECISIONS.md not found. Run imagine stage first.'],
453
+ };
454
+ }
455
+
456
+ // Load IMAGINE.md (optional)
457
+ let imagineContent = '';
458
+ try {
459
+ imagineContent = fs.readFileSync(path.join(planningDir, 'IMAGINE.md'), 'utf-8');
460
+ } catch {
461
+ // Optional — continue without it
462
+ }
463
+
464
+ // ── Step 2: Roles checkpoint ───────────────────────────────────────
465
+ const rolesConfig = await collectRoles({ auto: options.auto });
466
+ const rolesContent = formatRolesForSpec(rolesConfig);
467
+
468
+ // ── Step 3: Journey mapping ────────────────────────────────────────
469
+ let journeys: Journey[];
470
+
471
+ if (options.providedJourneys) {
472
+ journeys = options.providedJourneys;
473
+ } else if (options.skipAgents) {
474
+ journeys = buildDefaultJourneys(decisionsContent);
475
+ } else {
476
+ // Use agent orchestration for journey-mapper
477
+ const agentContext: SpecifyAgentContext = {
478
+ decisionsContent,
479
+ imagineContent,
480
+ };
481
+
482
+ const sequentialAgents = SPECIFY_AGENTS.filter((a) => a.phase === 'sequential');
483
+ const spawnFn = options.spawnFn;
484
+
485
+ if (spawnFn) {
486
+ // Allocate ID ranges for specify agents before spawning (FNDN-05)
487
+ if (options.spawnFn && !options.skipAgents) {
488
+ try {
489
+ allocateIdRange(statePath, 'J', 'journey-mapper', 50);
490
+ allocateIdRange(statePath, 'FR', 'architecture-drafter', 50);
491
+ allocateIdRange(statePath, 'NFR', 'architecture-drafter', 50);
492
+ allocateIdRange(statePath, 'I', 'integrations-checker', 50);
493
+ allocateIdRange(statePath, 'C', 'architecture-drafter', 50);
494
+ } catch {
495
+ // Non-fatal — ID allocation failure should not block specify
496
+ errors.push('Warning: ID range allocation failed');
497
+ }
498
+ }
499
+ const jmResults = await orchestrateSpecifyAgents(sequentialAgents, agentContext, spawnFn);
500
+ const jmResult = jmResults.find((r) => r.agent === 'journey-mapper');
501
+
502
+ if (jmResult && jmResult.status === 'complete' && jmResult.content.trim()) {
503
+ // Use agent's raw journey content for template injection
504
+ // Store raw content for JOURNEYS.md, but still need Journey[] for FR extraction
505
+ journeys = buildDefaultJourneys(decisionsContent);
506
+ // Override: if agent content contains J-XXX patterns, use it as journeys source
507
+ const jPattern = /###\s+J-\d{3}/;
508
+ if (jPattern.test(jmResult.content)) {
509
+ // Agent produced structured journey content — store for template injection
510
+ (options as any)._agentJourneyContent = jmResult.content;
511
+ }
512
+ } else {
513
+ // Fallback to default journeys
514
+ journeys = buildDefaultJourneys(decisionsContent);
515
+ errors.push('Journey mapper agent failed, using default journeys');
516
+ }
517
+ } else {
518
+ journeys = buildDefaultJourneys(decisionsContent);
519
+ }
520
+ }
521
+
522
+ // Mid-stage checkpoint: journeys complete (RESM-02)
523
+ writeCheckpoint(statePath, 'gswd/specify', 'journeys-complete');
524
+
525
+ // Validate journeys
526
+ for (const journey of journeys) {
527
+ const validation = validateJourney(journey);
528
+ if (!validation.valid) {
529
+ errors.push(...validation.errors);
530
+ }
531
+ }
532
+
533
+ // ── Step 4: FR extraction ──────────────────────────────────────────
534
+ const frs = extractFRsFromJourneys(journeys);
535
+
536
+ // Validate FR coverage
537
+ const coverageResult = validateFRCoverage(journeys, frs);
538
+ if (!coverageResult.valid) {
539
+ errors.push(...coverageResult.errors);
540
+ }
541
+
542
+ // Mid-stage checkpoint: FRs complete (RESM-02)
543
+ writeCheckpoint(statePath, 'gswd/specify', 'frs-complete');
544
+
545
+ // ── Step 5: NFR generation ─────────────────────────────────────────
546
+ const nfrs = generateNFRs(frs, decisionsContent);
547
+
548
+ // Validate NFRs
549
+ const nfrValidation = validateNFRs(nfrs);
550
+ if (!nfrValidation.valid) {
551
+ errors.push(...nfrValidation.errors);
552
+ }
553
+
554
+ // ── Step 6: Architecture + Integrations ────────────────────────────
555
+ let architectureContent: string;
556
+ let integrationsContent: string = buildDefaultIntegrationsContent();
557
+
558
+ if (options.providedArchitecture) {
559
+ architectureContent = options.providedArchitecture;
560
+ } else if (options.skipAgents) {
561
+ architectureContent = buildDefaultArchitectureContent(frs);
562
+ } else if (options.spawnFn) {
563
+ // Run parallel agents
564
+ const agentContext: SpecifyAgentContext = {
565
+ decisionsContent,
566
+ imagineContent,
567
+ journeys,
568
+ frs,
569
+ autoPolicy: JSON.stringify(config.auto),
570
+ };
571
+
572
+ const parallelAgents = SPECIFY_AGENTS.filter((a) => a.phase === 'parallel');
573
+ const parallelResults = await orchestrateSpecifyAgents(
574
+ parallelAgents,
575
+ agentContext,
576
+ options.spawnFn,
577
+ );
578
+
579
+ const archResult = parallelResults.find((r) => r.agent === 'architecture-drafter');
580
+ architectureContent = archResult && archResult.status === 'complete'
581
+ ? archResult.content
582
+ : buildDefaultArchitectureContent(frs);
583
+
584
+ const intResult = parallelResults.find((r) => r.agent === 'integrations-checker');
585
+ integrationsContent = intResult && intResult.status === 'complete'
586
+ ? intResult.content
587
+ : buildDefaultIntegrationsContent();
588
+ } else {
589
+ architectureContent = buildDefaultArchitectureContent(frs);
590
+ integrationsContent = buildDefaultIntegrationsContent();
591
+ }
592
+
593
+ if (!options.providedIntegrations && !integrationsContent) {
594
+ integrationsContent = buildDefaultIntegrationsContent();
595
+ }
596
+ if (options.providedIntegrations) {
597
+ integrationsContent = options.providedIntegrations;
598
+ }
599
+
600
+ // ── Step 6b: Validate integrations and components ──────────────────
601
+ // Extract I-XXX IDs from integrationsContent; for each found, validate
602
+ const integrationIdPattern = /###\s+(I-\d{3}):\s*(.+)/g;
603
+ let iMatch: RegExpExecArray | null;
604
+ while ((iMatch = integrationIdPattern.exec(integrationsContent)) !== null) {
605
+ const integrationData: Integration = {
606
+ id: iMatch[1],
607
+ name: iMatch[2].trim(),
608
+ setupSteps: [],
609
+ authMethod: '',
610
+ costQuota: '',
611
+ fallback: '',
612
+ status: 'approved', // default — will be overridden by actual status parsing
613
+ };
614
+ // Try to extract actual status from content following the ID
615
+ const statusMatch = integrationsContent
616
+ .slice(iMatch.index)
617
+ .match(/\*\*Status:\*\*\s*(approved|deferred[^\n]*|rejected)/i);
618
+ if (statusMatch) {
619
+ integrationData.status = statusMatch[1].toLowerCase().startsWith('approved')
620
+ ? 'approved'
621
+ : statusMatch[1].toLowerCase().startsWith('rejected')
622
+ ? 'rejected'
623
+ : 'deferred with fallback';
624
+ }
625
+ // Try to extract fallback from content following the ID
626
+ const fallbackMatch = integrationsContent
627
+ .slice(iMatch.index)
628
+ .match(/\*\*Fallback:\*\*\s*(.+)/i);
629
+ if (fallbackMatch) {
630
+ integrationData.fallback = fallbackMatch[1].trim();
631
+ }
632
+ const intValidation = validateIntegration(integrationData);
633
+ if (!intValidation.valid) {
634
+ errors.push(...intValidation.errors);
635
+ }
636
+ }
637
+
638
+ // Validate architecture components — parse C-XXX IDs from architectureContent
639
+ const componentIdPattern = /###\s+(C-\d{3}):\s*(.+)/g;
640
+ let cMatch: RegExpExecArray | null;
641
+ while ((cMatch = componentIdPattern.exec(architectureContent)) !== null) {
642
+ const componentData: ArchitectureComponent = {
643
+ id: cMatch[1],
644
+ name: cMatch[2].trim(),
645
+ responsibility: '',
646
+ dependencies: [],
647
+ linkedFRs: [],
648
+ };
649
+ // Extract responsibility
650
+ const respMatch = architectureContent
651
+ .slice(cMatch.index)
652
+ .match(/\*\*Responsibility:\*\*\s*(.+)/i);
653
+ if (respMatch) {
654
+ componentData.responsibility = respMatch[1].trim();
655
+ }
656
+ const compValidation = validateComponent(componentData);
657
+ if (!compValidation.valid) {
658
+ errors.push(...compValidation.errors);
659
+ }
660
+ }
661
+
662
+ // ── Step 7: Build and write artifacts ──────────────────────────────
663
+
664
+ // Load templates
665
+ const specTemplate = loadTemplate(templatesDir, 'SPEC.template.md');
666
+ const journeysTemplate = loadTemplate(templatesDir, 'JOURNEYS.template.md');
667
+ const nfrTemplate = loadTemplate(templatesDir, 'NFR.template.md');
668
+ const archTemplate = loadTemplate(templatesDir, 'ARCHITECTURE.template.md');
669
+ const intTemplate = loadTemplate(templatesDir, 'INTEGRATIONS.template.md');
670
+
671
+ // Build artifact content by injecting into templates
672
+ let specContent = specTemplate;
673
+ specContent = injectContent(specContent, 'ROLES', rolesContent);
674
+ specContent = injectContent(specContent, 'FRS', formatFRsContent(frs));
675
+ specContent = injectContent(specContent, 'ACCEPTANCE', formatAcceptanceCriteria(journeys));
676
+ specContent = injectContent(specContent, 'TRACEABILITY', formatTraceabilityTable(frs));
677
+
678
+ let journeysContent = journeysTemplate;
679
+ const agentJourneyContent = (options as any)._agentJourneyContent;
680
+ if (agentJourneyContent) {
681
+ journeysContent = injectContent(journeysContent, 'JOURNEYS', agentJourneyContent);
682
+ } else {
683
+ journeysContent = injectContent(journeysContent, 'JOURNEYS', formatJourneysContent(journeys));
684
+ }
685
+
686
+ let nfrContent = nfrTemplate;
687
+ nfrContent = injectContent(nfrContent, 'NFRS', formatNFRsContent(nfrs));
688
+
689
+ // For architecture — parse components/data model/ownership from agent content or defaults
690
+ let archContent = archTemplate;
691
+ archContent = injectContent(archContent, 'COMPONENTS', architectureContent.split('---')[0]?.trim() || architectureContent);
692
+ archContent = injectContent(archContent, 'DATA_MODEL', architectureContent.split('---')[1]?.trim() || '');
693
+ archContent = injectContent(archContent, 'OWNERSHIP', architectureContent.split('---')[2]?.trim() || '');
694
+
695
+ let intContent = intTemplate;
696
+ intContent = injectContent(intContent, 'INTEGRATIONS', integrationsContent);
697
+
698
+ // Write all 5 artifacts
699
+ const artifacts: { name: string; content: string; fileType: string }[] = [
700
+ { name: 'SPEC.md', content: specContent, fileType: 'SPEC.md' },
701
+ { name: 'JOURNEYS.md', content: journeysContent, fileType: 'JOURNEYS.md' },
702
+ { name: 'NFR.md', content: nfrContent, fileType: 'NFR.md' },
703
+ { name: 'ARCHITECTURE.md', content: archContent, fileType: 'ARCHITECTURE.md' },
704
+ { name: 'INTEGRATIONS.md', content: intContent, fileType: 'INTEGRATIONS.md' },
705
+ ];
706
+
707
+ const artifactsWritten: string[] = [];
708
+
709
+ for (const artifact of artifacts) {
710
+ const artifactPath = path.join(planningDir, artifact.name);
711
+ safeWriteFile(artifactPath, artifact.content);
712
+ artifactsWritten.push(artifactPath);
713
+
714
+ // Validate headings
715
+ const headingValidation = validateHeadings(artifact.content, artifact.fileType);
716
+ if (!headingValidation.valid) {
717
+ errors.push(`${artifact.name} missing headings: ${headingValidation.missing.join(', ')}`);
718
+ }
719
+ }
720
+
721
+ // ── Step 8: Update state ───────────────────────────────────────────
722
+ const finalState = readState(statePath);
723
+ if (finalState) {
724
+ finalState.stage_status.specify = 'done';
725
+ finalState.stage = 'specify';
726
+ finalState.last_checkpoint = {
727
+ workflow: 'gswd/specify',
728
+ checkpoint_id: 'complete',
729
+ timestamp: new Date().toISOString(),
730
+ };
731
+ writeState(statePath, finalState);
732
+ }
733
+
734
+ // ── Step 9: Return result ──────────────────────────────────────────
735
+ return {
736
+ status: errors.length > 0 ? 'complete' : 'complete', // Non-fatal errors don't block
737
+ artifacts: artifactsWritten,
738
+ journeyCount: journeys.length,
739
+ frCount: frs.length,
740
+ nfrCount: nfrs.length,
741
+ integrationCount: 0, // Would be populated from parsed integrations
742
+ componentCount: 3, // Default component count
743
+ errors: errors.length > 0 ? errors : undefined,
744
+ };
745
+ } catch (err: unknown) {
746
+ const message = err instanceof Error ? err.message : String(err);
747
+ return {
748
+ status: 'failed',
749
+ artifacts: [],
750
+ journeyCount: 0,
751
+ frCount: 0,
752
+ nfrCount: 0,
753
+ integrationCount: 0,
754
+ componentCount: 0,
755
+ errors: [message],
756
+ };
757
+ }
758
+ }
759
+
760
+ // ─── Helpers ────────────────────────────────────────────────────────────────
761
+
762
+ /**
763
+ * Load a template file. Returns a minimal template if not found.
764
+ */
765
+ function loadTemplate(templatesDir: string, filename: string): string {
766
+ try {
767
+ return fs.readFileSync(path.join(templatesDir, filename), 'utf-8');
768
+ } catch {
769
+ // Fallback: return a minimal template
770
+ const name = filename.replace('.template.md', '');
771
+ return `# ${name}\n\n<!-- GSWD:COMPLETE -->\n`;
772
+ }
773
+ }