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
@@ -0,0 +1,617 @@
1
+ /**
2
+ * GSWD Bootstrap Module — Pipeline orchestrator, policy engine, resume gate
3
+ *
4
+ * Contains:
5
+ * - runBootstrap(): async orchestrator that wires imagine -> specify -> audit -> compile
6
+ * - Types: BootstrapOptions, BootstrapResult, BootstrapStage, PolicyCheckInput, PolicyCheckResult
7
+ * - Policy engine: checkPolicy() with 4 detection functions
8
+ * - Resume gate: validateStageArtifacts(), shouldSkipStage()
9
+ * - Interrupt handler: handleInterrupt()
10
+ * - Auto decision display: renderAutoDecisionSummary()
11
+ * - Stage artifacts map: STAGE_ARTIFACTS
12
+ *
13
+ * Schema: GSWD_SPEC.md Section 8.1, 10.1-10.4
14
+ */
15
+
16
+ import * as fs from 'node:fs';
17
+ import * as path from 'node:path';
18
+
19
+ import { initState, readState, writeState, writeCheckpoint } from './state.js';
20
+ import { getGswdConfig } from './config.js';
21
+ import type { GswdConfig } from './config.js';
22
+ import { renderBanner, renderCheckpoint, renderNextUp } from './render.js';
23
+ import type { AutoDecision } from './imagine-synthesis.js';
24
+ import { runImagine } from './imagine.js';
25
+ import type { ImagineResult } from './imagine.js';
26
+ import { runSpecify } from './specify.js';
27
+ import type { SpecifyResult } from './specify.js';
28
+ import { runAuditWorkflow } from './audit.js';
29
+ import type { AuditWorkflowResult } from './audit.js';
30
+ import { runCompileWorkflow } from './compile.js';
31
+ import type { CompileWorkflowResult } from './compile.js';
32
+
33
+ // ─── Types ───────────────────────────────────────────────────────────────────
34
+
35
+ export interface BootstrapOptions {
36
+ ideaFilePath?: string;
37
+ auto: boolean;
38
+ resume?: boolean;
39
+ skipResearch?: boolean;
40
+ policy?: 'strict' | 'balanced' | 'aggressive';
41
+ planningDir?: string;
42
+ configPath?: string;
43
+ spawnFn?: unknown; // Agent Task() wrapper (injectable for testing)
44
+ }
45
+
46
+ export type BootstrapStage = 'imagine' | 'specify' | 'audit' | 'compile';
47
+
48
+ export interface BootstrapResult {
49
+ status: 'done' | 'failed' | 'interrupted';
50
+ stagesCompleted: BootstrapStage[];
51
+ artifactsWritten: string[];
52
+ autoDecisions: AutoDecision[];
53
+ interruptReasons: string[];
54
+ autoFixCycles: number;
55
+ error?: string;
56
+ }
57
+
58
+ export interface PolicyCheckInput {
59
+ config: GswdConfig;
60
+ integrationsContent?: string;
61
+ decisionsContent?: string;
62
+ nfrContent?: string;
63
+ }
64
+
65
+ export interface PolicyCheckResult {
66
+ shouldInterrupt: boolean;
67
+ reasons: string[];
68
+ }
69
+
70
+ // ─── Stage Artifacts ─────────────────────────────────────────────────────────
71
+
72
+ /**
73
+ * Required artifact filenames per stage.
74
+ * Used by validateStageArtifacts() and shouldSkipStage().
75
+ */
76
+ export const STAGE_ARTIFACTS: Record<BootstrapStage, string[]> = {
77
+ imagine: ['IMAGINE.md', 'ICP.md', 'GTM.md', 'COMPETITION.md', 'DECISIONS.md'],
78
+ specify: ['SPEC.md', 'JOURNEYS.md', 'NFR.md', 'ARCHITECTURE.md', 'INTEGRATIONS.md'],
79
+ audit: ['AUDIT.md'],
80
+ compile: ['PROJECT.md', 'REQUIREMENTS.md', 'ROADMAP.md', 'STATE.md'],
81
+ };
82
+
83
+ // ─── Policy Engine — Detection Functions ─────────────────────────────────────
84
+
85
+ /**
86
+ * Detect integrations with a positive monthly cost.
87
+ * Parses dollar amounts from integration sections (### I-NNN headings).
88
+ * Returns array of integration IDs with cost > $0.
89
+ */
90
+ function detectPaidIntegrations(integrationsContent: string): string[] {
91
+ const paidIds: string[] = [];
92
+ // Match integration section headings: ### I-001: Name or ## I-001: Name
93
+ const intRegex = /^#{2,3}\s+(I-\d{1,4})[:\s]/gm;
94
+ const positions: { id: string; start: number }[] = [];
95
+
96
+ let match: RegExpExecArray | null;
97
+ while ((match = intRegex.exec(integrationsContent)) !== null) {
98
+ positions.push({ id: match[1], start: match.index });
99
+ }
100
+
101
+ for (let i = 0; i < positions.length; i++) {
102
+ const end = i + 1 < positions.length ? positions[i + 1].start : integrationsContent.length;
103
+ const section = integrationsContent.slice(positions[i].start, end);
104
+ // Look for dollar amounts: $50, $25.99, 50 USD/month
105
+ const costMatch = section.match(/\$(\d+(?:\.\d+)?)|(\d+(?:\.\d+)?)\s*(?:USD|usd)\/month/);
106
+ if (costMatch) {
107
+ const amount = parseFloat(costMatch[1] ?? costMatch[2] ?? '0');
108
+ if (amount > 0) {
109
+ paidIds.push(positions[i].id);
110
+ }
111
+ }
112
+ }
113
+
114
+ return paidIds;
115
+ }
116
+
117
+ /**
118
+ * Detect integrations that require credentials (API keys, OAuth secrets, etc.).
119
+ * Returns array of { id, requirement } objects.
120
+ */
121
+ function detectCredentialRequirements(integrationsContent: string): { id: string; requirement: string }[] {
122
+ const credentials: { id: string; requirement: string }[] = [];
123
+ const intRegex = /^#{2,3}\s+(I-\d{1,4})[:\s]/gm;
124
+ const positions: { id: string; start: number }[] = [];
125
+
126
+ let match: RegExpExecArray | null;
127
+ while ((match = intRegex.exec(integrationsContent)) !== null) {
128
+ positions.push({ id: match[1], start: match.index });
129
+ }
130
+
131
+ for (let i = 0; i < positions.length; i++) {
132
+ const end = i + 1 < positions.length ? positions[i + 1].start : integrationsContent.length;
133
+ const section = integrationsContent.slice(positions[i].start, end);
134
+ // Look for credential-related patterns
135
+ const credPatterns = [
136
+ /\b(?:API\s*key)\s*required\b/i,
137
+ /\bOAuth\s*(?:secret|token|key)\b/i,
138
+ /\bcredentials?\s*(?:required|needed)\b/i,
139
+ /\bsecret\s*(?:key|required)\b/i,
140
+ ];
141
+ for (const pattern of credPatterns) {
142
+ const credMatch = section.match(pattern);
143
+ if (credMatch) {
144
+ credentials.push({ id: positions[i].id, requirement: credMatch[0] });
145
+ break; // One match per integration is enough
146
+ }
147
+ }
148
+ }
149
+
150
+ return credentials;
151
+ }
152
+
153
+ /**
154
+ * Detect contradictions between decisions and integrations.
155
+ * E.g., "no accounts" decision + Stripe integration = conflict.
156
+ */
157
+ function detectConflicts(decisionsContent: string, integrationsContent: string): string[] {
158
+ const conflicts: string[] = [];
159
+
160
+ const noAccountsDecision = /\bno\s+accounts?\b/i.test(decisionsContent) ||
161
+ /\bno\s+auth\b/i.test(decisionsContent);
162
+ const hasStripe = /stripe/i.test(integrationsContent);
163
+ const hasPayments = /payment/i.test(integrationsContent);
164
+
165
+ if (noAccountsDecision && (hasStripe || hasPayments)) {
166
+ conflicts.push(
167
+ 'Conflicting requirements: decision says "no accounts" but integrations include payment processing (requires accounts)'
168
+ );
169
+ }
170
+
171
+ return conflicts;
172
+ }
173
+
174
+ /**
175
+ * Detect security posture mismatches between NFR content and config.
176
+ * E.g., HIPAA/SOC2/PCI requirements without explicit config approval.
177
+ */
178
+ function detectSecurityPosture(nfrContent: string, config: GswdConfig): string[] {
179
+ const flags: string[] = [];
180
+ const securityPatterns: { pattern: RegExp; label: string }[] = [
181
+ { pattern: /\bHIPAA\b/i, label: 'HIPAA' },
182
+ { pattern: /\bSOC\s*2\b/i, label: 'SOC2' },
183
+ { pattern: /\bPCI[\s-]*DSS\b/i, label: 'PCI-DSS' },
184
+ { pattern: /\bGDPR\b/i, label: 'GDPR' },
185
+ ];
186
+
187
+ for (const { pattern, label } of securityPatterns) {
188
+ if (pattern.test(nfrContent)) {
189
+ // Check if config has explicit security approval
190
+ // For now, config has no explicit security_approvals field,
191
+ // so any compliance requirement triggers an interrupt
192
+ flags.push(
193
+ `Security posture mismatch: ${label} compliance referenced in NFR but not explicitly approved in config — requires human review`
194
+ );
195
+ }
196
+ }
197
+
198
+ return flags;
199
+ }
200
+
201
+ // ─── Policy Engine — Main Function ───────────────────────────────────────────
202
+
203
+ /**
204
+ * Check policy violations. Returns whether auto mode should be interrupted
205
+ * and the specific reasons.
206
+ *
207
+ * Checks:
208
+ * 1. Paid integrations not pre-approved
209
+ * 2. Credential requirements
210
+ * 3. Conflicting requirements (decisions vs integrations)
211
+ * 4. Security posture mismatches (NFR vs config)
212
+ */
213
+ export function checkPolicy(input: PolicyCheckInput): PolicyCheckResult {
214
+ const reasons: string[] = [];
215
+ const { config } = input;
216
+
217
+ // 1. Paid integrations not pre-approved
218
+ if (input.integrationsContent) {
219
+ const paidIntegrations = detectPaidIntegrations(input.integrationsContent);
220
+ const preapproved = new Set(config.auto.preapproved_integrations);
221
+ for (const intId of paidIntegrations) {
222
+ if (!preapproved.has(intId) && !config.auto.allow_paid_integrations_under_budget) {
223
+ reasons.push(`Paid integration ${intId} requires pre-approval (not in preapproved_integrations)`);
224
+ }
225
+ }
226
+ }
227
+
228
+ // 2. Credentials that cannot be inferred
229
+ if (input.integrationsContent) {
230
+ const credentialRequired = detectCredentialRequirements(input.integrationsContent);
231
+ for (const cred of credentialRequired) {
232
+ reasons.push(`Integration ${cred.id} requires credentials: ${cred.requirement}`);
233
+ }
234
+ }
235
+
236
+ // 3. Conflicting requirements
237
+ if (input.decisionsContent && input.integrationsContent) {
238
+ const conflicts = detectConflicts(input.decisionsContent, input.integrationsContent);
239
+ reasons.push(...conflicts);
240
+ }
241
+
242
+ // 4. Security posture mismatches
243
+ if (input.nfrContent) {
244
+ const securityFlags = detectSecurityPosture(input.nfrContent, config);
245
+ reasons.push(...securityFlags);
246
+ }
247
+
248
+ return { shouldInterrupt: reasons.length > 0, reasons };
249
+ }
250
+
251
+ // ─── Resume Gate ─────────────────────────────────────────────────────────────
252
+
253
+ /**
254
+ * Validate that all expected artifacts for a stage exist on disk and are non-empty.
255
+ * Returns { complete, missing } where missing lists filenames not found or empty.
256
+ */
257
+ export function validateStageArtifacts(
258
+ planningDir: string,
259
+ stage: BootstrapStage
260
+ ): { complete: boolean; missing: string[] } {
261
+ const required = STAGE_ARTIFACTS[stage] ?? [];
262
+ const missing: string[] = [];
263
+
264
+ for (const filename of required) {
265
+ const filepath = path.join(planningDir, filename);
266
+ if (!fs.existsSync(filepath)) {
267
+ missing.push(filename);
268
+ } else {
269
+ // Check non-zero bytes (Pitfall 2: empty files)
270
+ try {
271
+ const stat = fs.statSync(filepath);
272
+ if (stat.size === 0) {
273
+ missing.push(filename);
274
+ }
275
+ } catch {
276
+ missing.push(filename);
277
+ }
278
+ }
279
+ }
280
+
281
+ return { complete: missing.length === 0, missing };
282
+ }
283
+
284
+ /**
285
+ * Determine if a stage can be skipped during resume.
286
+ * Only skips if: resume=true AND stage is done/pass AND all artifacts are present.
287
+ */
288
+ export function shouldSkipStage(
289
+ stage: BootstrapStage,
290
+ state: { stage_status: Record<string, string> },
291
+ planningDir: string,
292
+ resume: boolean
293
+ ): boolean {
294
+ if (!resume) return false;
295
+
296
+ const stageStatus = state.stage_status[stage];
297
+ const isDone = stageStatus === 'done' || stageStatus === 'pass';
298
+ if (!isDone) return false;
299
+
300
+ // Validate artifacts before trusting checkpoint (RESM-03)
301
+ const { complete } = validateStageArtifacts(planningDir, stage);
302
+ return complete;
303
+ }
304
+
305
+ // ─── Interrupt Handler ───────────────────────────────────────────────────────
306
+
307
+ /**
308
+ * Handle a policy interrupt: write reasons to STATE.json, render checkpoint, return result.
309
+ * MUST write to STATE.json BEFORE rendering (Pitfall 6).
310
+ */
311
+ export function handleInterrupt(
312
+ reasons: string[],
313
+ statePath: string,
314
+ stagesCompleted: BootstrapStage[],
315
+ allArtifacts: string[],
316
+ allAutoDecisions: AutoDecision[],
317
+ autoFixCycles: number,
318
+ ): BootstrapResult {
319
+ // Step 1: Write to STATE.json FIRST
320
+ const state = readState(statePath);
321
+ if (state) {
322
+ if (!state.auto.interrupt_reasons) {
323
+ state.auto.interrupt_reasons = [];
324
+ }
325
+ state.auto.interrupt_reasons = [...state.auto.interrupt_reasons, ...reasons];
326
+ writeState(statePath, state);
327
+ }
328
+
329
+ // Step 2: Render checkpoint box
330
+ const box = renderCheckpoint(
331
+ 'Auto Mode Interrupted',
332
+ 'Bootstrap cannot continue automatically. Resolve:',
333
+ reasons.map(r => r.length > 50 ? r.slice(0, 47) + '...' : r),
334
+ 'Resolve issues above, then re-run bootstrap'
335
+ );
336
+ console.log(box);
337
+
338
+ // Step 3: Return result
339
+ return {
340
+ status: 'interrupted',
341
+ stagesCompleted: [...stagesCompleted],
342
+ artifactsWritten: [...allArtifacts],
343
+ autoDecisions: [...allAutoDecisions],
344
+ interruptReasons: [...reasons],
345
+ autoFixCycles,
346
+ };
347
+ }
348
+
349
+ // ─── Auto Decision Summary ───────────────────────────────────────────────────
350
+
351
+ /**
352
+ * Render a human-readable summary of auto-mode decisions.
353
+ * Returns empty string if no decisions.
354
+ */
355
+ export function renderAutoDecisionSummary(decisions: AutoDecision[]): string {
356
+ if (decisions.length === 0) return '';
357
+
358
+ const lines = ['Auto-mode decisions:'];
359
+ for (const d of decisions) {
360
+ lines.push(` \u26A1 ${d.type}: ${d.chosen}`);
361
+ lines.push(` Why: ${d.rationale}`);
362
+ lines.push(` Recorded in: DECISIONS.md + STATE.json auto.decisions[]`);
363
+ }
364
+ return lines.join('\n');
365
+ }
366
+
367
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
368
+
369
+ /**
370
+ * Try to read a file, returning undefined if it doesn't exist.
371
+ */
372
+ function tryReadFile(filePath: string): string | undefined {
373
+ try {
374
+ return fs.readFileSync(filePath, 'utf-8');
375
+ } catch {
376
+ return undefined;
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Derive a project slug from an idea file path or a fallback.
382
+ */
383
+ function deriveProjectSlug(input: string): string {
384
+ const base = path.basename(input, path.extname(input));
385
+ return base.replace(/[^a-z0-9-]/gi, '-').toLowerCase() || 'project';
386
+ }
387
+
388
+ // ─── Bootstrap Orchestrator ──────────────────────────────────────────────────
389
+
390
+ /**
391
+ * Run the full bootstrap pipeline: init -> imagine -> specify -> audit -> compile.
392
+ *
393
+ * Implements GSWD_SPEC Section 8.1:
394
+ * - Auto mode threads autoMode/auto/autoFix to each stage
395
+ * - Resume mode skips completed stages with valid artifacts
396
+ * - Policy checks fire after imagine and after specify
397
+ * - Audit FAIL blocks compile (BOOT-03)
398
+ * - Auto-fix enabled with max 2 cycles when policy allows (BOOT-04)
399
+ * - state.stage = 'done' after successful compile (Pitfall 8)
400
+ *
401
+ * IMPORTANT: runImagine() and runSpecify() are ASYNC — use await.
402
+ * runAuditWorkflow() and runCompileWorkflow() are SYNC — no await.
403
+ */
404
+ export async function runBootstrap(options: BootstrapOptions): Promise<BootstrapResult> {
405
+ const planningDir = options.planningDir ?? path.join(process.cwd(), '.planning');
406
+ const gswdDir = path.join(planningDir, 'gswd');
407
+ const statePath = path.join(gswdDir, 'STATE.json');
408
+ const configPath = options.configPath ?? path.join(planningDir, 'config.json');
409
+
410
+ const stagesCompleted: BootstrapStage[] = [];
411
+ const allArtifacts: string[] = [];
412
+ let allAutoDecisions: AutoDecision[] = [];
413
+ let autoFixCycles = 0;
414
+
415
+ // Early validation: auto mode requires @idea.md
416
+ if (options.auto && !options.ideaFilePath) {
417
+ return {
418
+ status: 'failed',
419
+ stagesCompleted,
420
+ artifactsWritten: allArtifacts,
421
+ autoDecisions: allAutoDecisions,
422
+ interruptReasons: [],
423
+ autoFixCycles,
424
+ error: 'Auto mode requires @idea.md — provide a file describing your idea',
425
+ };
426
+ }
427
+
428
+ // Step 1: Ensure STATE.json exists
429
+ if (!fs.existsSync(statePath)) {
430
+ fs.mkdirSync(gswdDir, { recursive: true });
431
+ initState(gswdDir, deriveProjectSlug(options.ideaFilePath ?? planningDir));
432
+ }
433
+
434
+ // Step 2: Load config
435
+ const config = getGswdConfig(configPath);
436
+ if (options.policy) {
437
+ config.auto.policy = options.policy;
438
+ }
439
+
440
+ // Step 3: Configure auto mode in state
441
+ const initStateObj = readState(statePath);
442
+ if (initStateObj) {
443
+ initStateObj.auto.enabled = options.auto;
444
+ initStateObj.auto.policy = config.auto.policy;
445
+ writeState(statePath, initStateObj);
446
+ }
447
+
448
+ console.log(renderBanner('BOOTSTRAP'));
449
+
450
+ // ── IMAGINE ──────────────────────────────────────────────────────────
451
+ const imagineState = readState(statePath)!;
452
+ if (!shouldSkipStage('imagine', imagineState, planningDir, options.resume ?? false)) {
453
+ writeCheckpoint(statePath, 'gswd/bootstrap', 'imagine-start');
454
+ console.log(renderBanner('IMAGINE'));
455
+ // runImagine is ASYNC — MUST await
456
+ const imagineResult: ImagineResult = await runImagine({
457
+ ideaFilePath: options.ideaFilePath,
458
+ autoMode: options.auto,
459
+ skipResearch: options.skipResearch ?? false,
460
+ planningDir,
461
+ configPath,
462
+ spawnFn: options.spawnFn as any,
463
+ });
464
+ if (imagineResult.status !== 'complete') {
465
+ return {
466
+ status: 'failed',
467
+ stagesCompleted,
468
+ artifactsWritten: allArtifacts,
469
+ autoDecisions: allAutoDecisions,
470
+ interruptReasons: [],
471
+ autoFixCycles,
472
+ error: imagineResult.error ?? 'Imagine stage failed',
473
+ };
474
+ }
475
+ allArtifacts.push(...(imagineResult.artifacts_written ?? []));
476
+ if (imagineResult.auto_decisions) {
477
+ allAutoDecisions = [...allAutoDecisions, ...imagineResult.auto_decisions];
478
+ }
479
+
480
+ // Record auto decisions in STATE.json (AUTO-04)
481
+ if (options.auto && allAutoDecisions.length > 0) {
482
+ const stateAfterImagine = readState(statePath);
483
+ if (stateAfterImagine) {
484
+ (stateAfterImagine.auto as Record<string, unknown>).decisions = allAutoDecisions;
485
+ writeState(statePath, stateAfterImagine);
486
+ }
487
+ }
488
+ }
489
+ stagesCompleted.push('imagine');
490
+
491
+ // Policy check after imagine (conflicting requirements check)
492
+ if (options.auto) {
493
+ const decisionsContent = tryReadFile(path.join(planningDir, 'DECISIONS.md'));
494
+ const check = checkPolicy({ config, decisionsContent });
495
+ if (check.shouldInterrupt) {
496
+ return handleInterrupt(check.reasons, statePath, stagesCompleted, allArtifacts, allAutoDecisions, autoFixCycles);
497
+ }
498
+ }
499
+
500
+ // ── SPECIFY ──────────────────────────────────────────────────────────
501
+ const specifyState = readState(statePath)!;
502
+ if (!shouldSkipStage('specify', specifyState, planningDir, options.resume ?? false)) {
503
+ writeCheckpoint(statePath, 'gswd/bootstrap', 'specify-start');
504
+ console.log(renderBanner('SPECIFY'));
505
+ // runSpecify is ASYNC — MUST await
506
+ // When no spawnFn provided, set skipAgents: true (Pitfall 1)
507
+ const specifyResult: SpecifyResult = await runSpecify({
508
+ auto: options.auto,
509
+ skipAgents: !options.spawnFn,
510
+ planningDir,
511
+ configPath,
512
+ spawnFn: options.spawnFn as any,
513
+ });
514
+ if (specifyResult.status === 'failed') {
515
+ return {
516
+ status: 'failed',
517
+ stagesCompleted,
518
+ artifactsWritten: allArtifacts,
519
+ autoDecisions: allAutoDecisions,
520
+ interruptReasons: [],
521
+ autoFixCycles,
522
+ error: (specifyResult.errors ?? []).join('; ') || 'Specify stage failed',
523
+ };
524
+ }
525
+ allArtifacts.push(...specifyResult.artifacts);
526
+ }
527
+ stagesCompleted.push('specify');
528
+
529
+ // Policy check after specify (check integrations, NFRs, decisions)
530
+ if (options.auto) {
531
+ const intContent = tryReadFile(path.join(planningDir, 'INTEGRATIONS.md'));
532
+ const nfrContent = tryReadFile(path.join(planningDir, 'NFR.md'));
533
+ const decisionsContent = tryReadFile(path.join(planningDir, 'DECISIONS.md'));
534
+ const check = checkPolicy({ config, integrationsContent: intContent, nfrContent, decisionsContent });
535
+ if (check.shouldInterrupt) {
536
+ return handleInterrupt(check.reasons, statePath, stagesCompleted, allArtifacts, allAutoDecisions, autoFixCycles);
537
+ }
538
+ }
539
+
540
+ // ── AUDIT ────────────────────────────────────────────────────────────
541
+ const auditState = readState(statePath)!;
542
+ if (!shouldSkipStage('audit', auditState, planningDir, options.resume ?? false)) {
543
+ writeCheckpoint(statePath, 'gswd/bootstrap', 'audit-start');
544
+ console.log(renderBanner('AUDIT'));
545
+ // runAuditWorkflow is SYNC — no await (Pitfall 7)
546
+ const auditResult: AuditWorkflowResult = runAuditWorkflow({
547
+ planningDir,
548
+ statePath,
549
+ autoFix: options.auto && config.auto.policy !== 'strict',
550
+ maxCycles: 2,
551
+ });
552
+ autoFixCycles = auditResult.autoFixCycles;
553
+ if (!auditResult.passed) {
554
+ // BOOT-03: Cannot finish if audit is FAIL
555
+ return {
556
+ status: 'failed',
557
+ stagesCompleted,
558
+ artifactsWritten: allArtifacts,
559
+ autoDecisions: allAutoDecisions,
560
+ interruptReasons: [],
561
+ autoFixCycles,
562
+ error: 'Audit FAIL — cannot compile',
563
+ };
564
+ }
565
+ }
566
+ stagesCompleted.push('audit');
567
+
568
+ // ── COMPILE ──────────────────────────────────────────────────────────
569
+ const compileState = readState(statePath)!;
570
+ if (!shouldSkipStage('compile', compileState, planningDir, options.resume ?? false)) {
571
+ writeCheckpoint(statePath, 'gswd/bootstrap', 'compile-start');
572
+ console.log(renderBanner('COMPILE'));
573
+ // runCompileWorkflow is SYNC — no await (Pitfall 7)
574
+ const compileResult: CompileWorkflowResult = runCompileWorkflow({
575
+ planningDir,
576
+ statePath,
577
+ });
578
+ if (!compileResult.passed) {
579
+ return {
580
+ status: 'failed',
581
+ stagesCompleted,
582
+ artifactsWritten: allArtifacts,
583
+ autoDecisions: allAutoDecisions,
584
+ interruptReasons: [],
585
+ autoFixCycles,
586
+ error: compileResult.error ?? 'Compile validator failed',
587
+ };
588
+ }
589
+ allArtifacts.push(...compileResult.filesWritten);
590
+ }
591
+ stagesCompleted.push('compile');
592
+
593
+ // Step final: Set stage = 'done' (Pitfall 8 — ONLY place 'done' is written)
594
+ const doneState = readState(statePath)!;
595
+ doneState.stage = 'done';
596
+ writeState(statePath, doneState);
597
+ writeCheckpoint(statePath, 'gswd/bootstrap', 'bootstrap-done');
598
+
599
+ // Auto decision summary (AUTO-06)
600
+ if (options.auto && allAutoDecisions.length > 0) {
601
+ console.log(renderAutoDecisionSummary(allAutoDecisions));
602
+ }
603
+
604
+ console.log(renderNextUp(
605
+ ['PROJECT.md', 'REQUIREMENTS.md', 'ROADMAP.md', 'STATE.md'],
606
+ '/gsd:plan-phase 01'
607
+ ));
608
+
609
+ return {
610
+ status: 'done',
611
+ stagesCompleted,
612
+ artifactsWritten: allArtifacts,
613
+ autoDecisions: allAutoDecisions,
614
+ interruptReasons: [],
615
+ autoFixCycles,
616
+ };
617
+ }