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.
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js +6 -0
- package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +29 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-feature-sync.js +212 -2
- package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
- package/dist/src/cli/commands/refresh-plugins.d.ts.map +1 -1
- package/dist/src/cli/commands/refresh-plugins.js +9 -0
- package/dist/src/cli/commands/refresh-plugins.js.map +1 -1
- package/dist/src/config/types.d.ts +2 -2
- package/dist/src/core/increment/increment-utils.d.ts +27 -4
- package/dist/src/core/increment/increment-utils.d.ts.map +1 -1
- package/dist/src/core/increment/increment-utils.js +44 -17
- package/dist/src/core/increment/increment-utils.js.map +1 -1
- package/dist/src/core/increment/template-creator.d.ts +26 -0
- package/dist/src/core/increment/template-creator.d.ts.map +1 -1
- package/dist/src/core/increment/template-creator.js +179 -20
- package/dist/src/core/increment/template-creator.js.map +1 -1
- package/dist/src/importers/import-to-increment.d.ts +111 -0
- package/dist/src/importers/import-to-increment.d.ts.map +1 -0
- package/dist/src/importers/import-to-increment.js +223 -0
- package/dist/src/importers/import-to-increment.js.map +1 -0
- package/dist/src/importers/increment-external-ref-detector.d.ts +78 -0
- package/dist/src/importers/increment-external-ref-detector.d.ts.map +1 -0
- package/dist/src/importers/increment-external-ref-detector.js +130 -0
- package/dist/src/importers/increment-external-ref-detector.js.map +1 -0
- package/dist/src/init/research/types.d.ts +1 -1
- package/package.json +1 -1
- package/plugins/specweave/hooks/stop-auto-v5.sh +2 -2
- package/plugins/specweave/hooks/stop-sync.sh +10 -5
- package/plugins/specweave/hooks/user-prompt-submit.sh +27 -5
- package/plugins/specweave/hooks/v2/handlers/github-sync-handler.sh +6 -3
- package/plugins/specweave/hooks/v2/handlers/project-bridge-handler.sh +4 -3
- package/plugins/specweave/skills/import/SKILL.md +186 -0
- package/plugins/specweave/skills/increment/SKILL.md +30 -16
- package/plugins/specweave/skills/pm/SKILL.md +29 -2
- package/plugins/specweave/skills/pm/phases/00-deep-interview.md +12 -0
- package/plugins/specweave-github/lib/github-feature-sync-cli.js +5 -0
- package/plugins/specweave-github/lib/github-feature-sync-cli.ts +7 -1
- package/plugins/specweave-github/lib/github-feature-sync.js +225 -2
- 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
|
*
|