specweave 1.0.301 → 1.0.303

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 (41) hide show
  1. package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js +6 -0
  2. package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js.map +1 -1
  3. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +29 -1
  4. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
  5. package/dist/plugins/specweave-github/lib/github-feature-sync.js +212 -2
  6. package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
  7. package/dist/src/cli/commands/refresh-plugins.d.ts.map +1 -1
  8. package/dist/src/cli/commands/refresh-plugins.js +9 -0
  9. package/dist/src/cli/commands/refresh-plugins.js.map +1 -1
  10. package/dist/src/config/types.d.ts +2 -2
  11. package/dist/src/core/increment/increment-utils.d.ts +27 -4
  12. package/dist/src/core/increment/increment-utils.d.ts.map +1 -1
  13. package/dist/src/core/increment/increment-utils.js +44 -17
  14. package/dist/src/core/increment/increment-utils.js.map +1 -1
  15. package/dist/src/core/increment/template-creator.d.ts +26 -0
  16. package/dist/src/core/increment/template-creator.d.ts.map +1 -1
  17. package/dist/src/core/increment/template-creator.js +179 -20
  18. package/dist/src/core/increment/template-creator.js.map +1 -1
  19. package/dist/src/importers/import-to-increment.d.ts +111 -0
  20. package/dist/src/importers/import-to-increment.d.ts.map +1 -0
  21. package/dist/src/importers/import-to-increment.js +223 -0
  22. package/dist/src/importers/import-to-increment.js.map +1 -0
  23. package/dist/src/importers/increment-external-ref-detector.d.ts +78 -0
  24. package/dist/src/importers/increment-external-ref-detector.d.ts.map +1 -0
  25. package/dist/src/importers/increment-external-ref-detector.js +130 -0
  26. package/dist/src/importers/increment-external-ref-detector.js.map +1 -0
  27. package/dist/src/init/research/types.d.ts +1 -1
  28. package/package.json +1 -1
  29. package/plugins/specweave/hooks/stop-auto-v5.sh +2 -2
  30. package/plugins/specweave/hooks/stop-sync.sh +10 -5
  31. package/plugins/specweave/hooks/user-prompt-submit.sh +27 -5
  32. package/plugins/specweave/hooks/v2/handlers/github-sync-handler.sh +6 -3
  33. package/plugins/specweave/hooks/v2/handlers/project-bridge-handler.sh +4 -3
  34. package/plugins/specweave/skills/import/SKILL.md +186 -0
  35. package/plugins/specweave/skills/increment/SKILL.md +30 -16
  36. package/plugins/specweave/skills/pm/SKILL.md +29 -2
  37. package/plugins/specweave/skills/pm/phases/00-deep-interview.md +12 -0
  38. package/plugins/specweave-github/lib/github-feature-sync-cli.js +5 -0
  39. package/plugins/specweave-github/lib/github-feature-sync-cli.ts +7 -1
  40. package/plugins/specweave-github/lib/github-feature-sync.js +225 -2
  41. package/plugins/specweave-github/lib/github-feature-sync.ts +290 -2
@@ -10,7 +10,7 @@
10
10
  * Creates ONE issue PER user story file from specs/{project}/FS-XXX/us-*.md
11
11
  */
12
12
 
13
- import { readdir, readFile, writeFile } from 'fs/promises';
13
+ import { readdir, readFile, writeFile, mkdir } from 'fs/promises';
14
14
  import { existsSync } from 'fs';
15
15
  import * as path from 'path';
16
16
  import * as yaml from 'yaml';
@@ -294,7 +294,8 @@ export class GitHubFeatureSync {
294
294
  }
295
295
 
296
296
  /**
297
- * Find Feature folder in specs directory
297
+ * Find Feature folder in specs directory.
298
+ * Falls back to auto-creating from increment spec.md if living docs don't exist.
298
299
  */
299
300
  private async findFeatureFolder(featureId: string): Promise<string | null> {
300
301
  // v5.0.0+: NO _features folder - features live in project folders
@@ -315,9 +316,296 @@ export class GitHubFeatureSync {
315
316
  return legacyFolder;
316
317
  }
317
318
 
319
+ // FALLBACK (v1.0.302): Auto-create feature folder from increment spec.md
320
+ // Most increments never get living docs created, so sync silently fails.
321
+ // This makes sync self-healing by creating minimal living docs on-the-fly.
322
+ console.log(` ℹ️ Feature folder not found in living docs, attempting auto-create from spec.md...`);
323
+ const created = await this.createFeatureFolderFromSpec(featureId, projectFolders);
324
+ if (created) {
325
+ return created;
326
+ }
327
+
318
328
  return null;
319
329
  }
320
330
 
331
+ /**
332
+ * Find the increment folder for a given feature ID.
333
+ * Converts FS-271 -> finds 0271-xxx-xxx/ in .specweave/increments/
334
+ */
335
+ private async findIncrementFolder(featureId: string): Promise<string | null> {
336
+ const numMatch = featureId.match(/FS-0*(\d+)E?/i);
337
+ if (!numMatch) return null;
338
+
339
+ const num = parseInt(numMatch[1], 10);
340
+ const paddedNum = String(num).padStart(4, '0');
341
+
342
+ const incrementsDir = path.join(this.projectRoot, '.specweave/increments');
343
+ if (!existsSync(incrementsDir)) return null;
344
+
345
+ const entries = await readdir(incrementsDir);
346
+ const match = entries.find(e => e.startsWith(paddedNum + '-'));
347
+ if (!match) return null;
348
+
349
+ return path.join(incrementsDir, match);
350
+ }
351
+
352
+ /**
353
+ * Auto-create a feature folder (FEATURE.md + us-NNN.md files) from an
354
+ * increment's spec.md. This enables GitHub sync even when the living docs
355
+ * builder hasn't run yet.
356
+ */
357
+ private async createFeatureFolderFromSpec(
358
+ featureId: string,
359
+ projectFolders: string[]
360
+ ): Promise<string | null> {
361
+ try {
362
+ const incrementFolder = await this.findIncrementFolder(featureId);
363
+ if (!incrementFolder) {
364
+ console.log(` ⚠️ No increment folder found for ${featureId}`);
365
+ return null;
366
+ }
367
+
368
+ const specPath = path.join(incrementFolder, 'spec.md');
369
+ if (!existsSync(specPath)) {
370
+ console.log(` ⚠️ No spec.md found in ${path.basename(incrementFolder)}`);
371
+ return null;
372
+ }
373
+
374
+ const specContent = await readFile(specPath, 'utf-8');
375
+
376
+ // Parse frontmatter
377
+ const fmMatch = specContent.match(/^---\n([\s\S]*?)\n---/);
378
+ if (!fmMatch) {
379
+ console.log(` ⚠️ spec.md has no YAML frontmatter`);
380
+ return null;
381
+ }
382
+
383
+ const frontmatter = yaml.parse(fmMatch[1]);
384
+ const title = frontmatter.title || path.basename(incrementFolder).replace(/^\d+-/, '');
385
+ const status = frontmatter.status || 'active';
386
+ const priority = frontmatter.priority || 'P2';
387
+ const created = frontmatter.created || new Date().toISOString().split('T')[0];
388
+ const incrementId = frontmatter.increment || path.basename(incrementFolder);
389
+
390
+ // Determine target project folder from spec.md user stories or first available
391
+ let targetProjectFolder = projectFolders[0]; // Default: first project folder
392
+ const projectMatch = specContent.match(/\*\*Project\*\*:\s*(\S+)/);
393
+ if (projectMatch) {
394
+ const projectName = projectMatch[1];
395
+ const matchingFolder = projectFolders.find(f => path.basename(f) === projectName);
396
+ if (matchingFolder) {
397
+ targetProjectFolder = matchingFolder;
398
+ }
399
+ }
400
+
401
+ if (!targetProjectFolder) {
402
+ console.log(` ⚠️ No project folder available for feature creation`);
403
+ return null;
404
+ }
405
+
406
+ // Create feature folder
407
+ const featureFolder = path.join(targetProjectFolder, featureId);
408
+ await mkdir(featureFolder, { recursive: true });
409
+
410
+ // Parse user stories from spec.md body
411
+ const userStories = this.parseUserStoriesFromSpec(specContent, featureId);
412
+
413
+ // Create FEATURE.md
414
+ const featureMd = this.buildFeatureMd(featureId, title, status, priority, created, incrementId, userStories);
415
+ await writeFile(path.join(featureFolder, 'FEATURE.md'), featureMd, 'utf-8');
416
+
417
+ // Create us-NNN.md files
418
+ for (const us of userStories) {
419
+ const usFilename = `us-${us.id.replace('US-', '').padStart(3, '0')}-${this.slugify(us.title)}.md`;
420
+ const usMd = this.buildUserStoryMd(us, featureId, incrementId);
421
+ await writeFile(path.join(featureFolder, usFilename), usMd, 'utf-8');
422
+ }
423
+
424
+ console.log(` ✅ Auto-created feature folder with ${userStories.length} user stories`);
425
+ return featureFolder;
426
+ } catch (error) {
427
+ console.log(` ⚠️ Failed to auto-create feature folder: ${(error as Error).message}`);
428
+ return null;
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Parse user stories from spec.md markdown content.
434
+ */
435
+ private parseUserStoriesFromSpec(
436
+ specContent: string,
437
+ featureId: string
438
+ ): Array<{
439
+ id: string;
440
+ title: string;
441
+ priority: string;
442
+ project: string;
443
+ storyText: string;
444
+ acceptanceCriteria: string[];
445
+ status: string;
446
+ }> {
447
+ const stories: Array<{
448
+ id: string;
449
+ title: string;
450
+ priority: string;
451
+ project: string;
452
+ storyText: string;
453
+ acceptanceCriteria: string[];
454
+ status: string;
455
+ }> = [];
456
+
457
+ // Match ### US-NNN: Title (Priority) sections
458
+ const usRegex = /### (US-\d+):\s*(.+?)(?:\s*\((P\d)\))?\s*\n([\s\S]*?)(?=\n### US-|\n## |\n---\s*\n### US-|$)/g;
459
+ let match;
460
+
461
+ while ((match = usRegex.exec(specContent)) !== null) {
462
+ const usId = match[1];
463
+ const rawTitle = match[2].trim();
464
+ const priority = match[3] || 'P2';
465
+ const body = match[4];
466
+
467
+ // Skip template placeholders
468
+ if (rawTitle === '[Story Title]') continue;
469
+
470
+ // Extract project
471
+ const projectMatch = body.match(/\*\*Project\*\*:\s*(\S+)/);
472
+ const project = projectMatch ? projectMatch[1] : 'specweave';
473
+
474
+ // Extract story text (As a... I want... So that...)
475
+ const storyMatch = body.match(/\*\*As a\*\*\s+([\s\S]*?)(?=\n\*\*Acceptance Criteria|$)/);
476
+ const storyText = storyMatch ? storyMatch[1].trim() : '';
477
+
478
+ // Extract acceptance criteria
479
+ const acs: string[] = [];
480
+ const acRegex = /- \[[ x]\] \*\*AC-[^*]+\*\*:\s*(.+)/g;
481
+ let acMatch;
482
+ while ((acMatch = acRegex.exec(body)) !== null) {
483
+ acs.push(acMatch[0]);
484
+ }
485
+
486
+ // Determine status from ACs
487
+ const totalAcs = acs.length;
488
+ const completedAcs = acs.filter(ac => ac.startsWith('- [x]')).length;
489
+ let status = 'not-started';
490
+ if (totalAcs > 0 && completedAcs === totalAcs) status = 'complete';
491
+ else if (completedAcs > 0) status = 'active';
492
+
493
+ stories.push({
494
+ id: usId,
495
+ title: rawTitle,
496
+ priority,
497
+ project,
498
+ storyText,
499
+ acceptanceCriteria: acs,
500
+ status,
501
+ });
502
+ }
503
+
504
+ return stories;
505
+ }
506
+
507
+ /**
508
+ * Build FEATURE.md content matching the living docs format.
509
+ */
510
+ private buildFeatureMd(
511
+ featureId: string,
512
+ title: string,
513
+ status: string,
514
+ priority: string,
515
+ created: string,
516
+ incrementId: string,
517
+ userStories: Array<{ id: string; title: string }>
518
+ ): string {
519
+ const now = new Date().toISOString().split('T')[0];
520
+ const mappedStatus = status === 'planned' ? 'planning'
521
+ : status === 'completed' || status === 'done' ? 'complete'
522
+ : status === 'active' || status === 'in-progress' ? 'active'
523
+ : 'planning';
524
+
525
+ const fm: Record<string, unknown> = {
526
+ id: featureId,
527
+ title,
528
+ type: 'feature',
529
+ status: mappedStatus,
530
+ priority,
531
+ created,
532
+ lastUpdated: now,
533
+ tldr: title,
534
+ complexity: 'medium',
535
+ auto_created: true,
536
+ };
537
+
538
+ const yamlFm = yaml.stringify(fm);
539
+
540
+ let body = `\n# ${title}\n\n## TL;DR\n\n**What**: ${title}\n**Status**: ${mappedStatus} | **Priority**: ${priority}\n**User Stories**: ${userStories.length}\n\n## Overview\n\n${title}\n\n## Implementation History\n\n| Increment | Status |\n|-----------|--------|\n| [${incrementId}](../../../../../increments/${incrementId}/spec.md) | ${mappedStatus} |\n\n## User Stories\n`;
541
+
542
+ for (const us of userStories) {
543
+ body += `\n- [${us.id}: ${us.title}](./${us.id.toLowerCase()}.md)`;
544
+ }
545
+
546
+ return `---\n${yamlFm}---${body}\n`;
547
+ }
548
+
549
+ /**
550
+ * Build us-NNN.md content matching the living docs format.
551
+ */
552
+ private buildUserStoryMd(
553
+ us: {
554
+ id: string;
555
+ title: string;
556
+ priority: string;
557
+ project: string;
558
+ storyText: string;
559
+ acceptanceCriteria: string[];
560
+ status: string;
561
+ },
562
+ featureId: string,
563
+ incrementId: string
564
+ ): string {
565
+ const now = new Date().toISOString().split('T')[0];
566
+
567
+ const fm: Record<string, unknown> = {
568
+ id: us.id,
569
+ feature: featureId,
570
+ title: us.title,
571
+ status: us.status,
572
+ priority: us.priority,
573
+ created: now,
574
+ project: us.project,
575
+ };
576
+
577
+ const yamlFm = yaml.stringify(fm);
578
+
579
+ let body = `\n# ${us.id}: ${us.title}\n\n**Feature**: [${featureId}](./FEATURE.md)\n\n`;
580
+
581
+ if (us.storyText) {
582
+ body += `${us.storyText}\n\n`;
583
+ }
584
+
585
+ body += `---\n\n## Acceptance Criteria\n\n`;
586
+
587
+ if (us.acceptanceCriteria.length > 0) {
588
+ body += us.acceptanceCriteria.join('\n') + '\n';
589
+ } else {
590
+ body += `- [ ] **AC-${us.id.replace('US-', 'US')}-01**: Pending specification\n`;
591
+ }
592
+
593
+ body += `\n---\n\n## Implementation\n\n**Increment**: [${incrementId}](../../../../../increments/${incrementId}/spec.md)\n`;
594
+
595
+ return `---\n${yamlFm}---${body}\n`;
596
+ }
597
+
598
+ /**
599
+ * Convert a title to a URL-safe slug.
600
+ */
601
+ private slugify(text: string): string {
602
+ return text
603
+ .toLowerCase()
604
+ .replace(/[^a-z0-9]+/g, '-')
605
+ .replace(/^-+|-+$/g, '')
606
+ .substring(0, 60);
607
+ }
608
+
321
609
  /**
322
610
  * Backfill increment metadata.json with GitHub issue reference (v1.0.240)
323
611
  *