specweave 0.13.6 → 0.14.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 (35) hide show
  1. package/CLAUDE.md +189 -0
  2. package/dist/cli/commands/init.js +1 -1
  3. package/dist/cli/commands/init.js.map +1 -1
  4. package/dist/cli/commands/status-line.d.ts +14 -0
  5. package/dist/cli/commands/status-line.d.ts.map +1 -0
  6. package/dist/cli/commands/status-line.js +75 -0
  7. package/dist/cli/commands/status-line.js.map +1 -0
  8. package/dist/core/status-line/status-line-manager.d.ts +62 -0
  9. package/dist/core/status-line/status-line-manager.d.ts.map +1 -0
  10. package/dist/core/status-line/status-line-manager.js +169 -0
  11. package/dist/core/status-line/status-line-manager.js.map +1 -0
  12. package/dist/core/status-line/types.d.ts +50 -0
  13. package/dist/core/status-line/types.d.ts.map +1 -0
  14. package/dist/core/status-line/types.js +17 -0
  15. package/dist/core/status-line/types.js.map +1 -0
  16. package/dist/utils/project-mapper.d.ts +74 -0
  17. package/dist/utils/project-mapper.d.ts.map +1 -0
  18. package/dist/utils/project-mapper.js +273 -0
  19. package/dist/utils/project-mapper.js.map +1 -0
  20. package/dist/utils/spec-splitter.d.ts +68 -0
  21. package/dist/utils/spec-splitter.d.ts.map +1 -0
  22. package/dist/utils/spec-splitter.js +314 -0
  23. package/dist/utils/spec-splitter.js.map +1 -0
  24. package/package.json +1 -1
  25. package/plugins/specweave/hooks/lib/update-status-line.sh +138 -0
  26. package/plugins/specweave/hooks/post-task-completion.sh +10 -0
  27. package/plugins/specweave/skills/multi-project-spec-mapper/SKILL.md +399 -0
  28. package/plugins/specweave-ado/lib/ado-multi-project-sync.js +453 -0
  29. package/plugins/specweave-ado/lib/ado-multi-project-sync.ts +633 -0
  30. package/plugins/specweave-docs/skills/docusaurus/SKILL.md +17 -3
  31. package/plugins/specweave-docs-preview/commands/preview.md +29 -4
  32. package/plugins/specweave-github/lib/github-multi-project-sync.js +340 -0
  33. package/plugins/specweave-github/lib/github-multi-project-sync.ts +461 -0
  34. package/plugins/specweave-jira/lib/jira-multi-project-sync.js +244 -0
  35. package/plugins/specweave-jira/lib/jira-multi-project-sync.ts +358 -0
@@ -0,0 +1,633 @@
1
+ /**
2
+ * Azure DevOps Multi-Project Sync
3
+ *
4
+ * Supports two patterns:
5
+ * 1. **Multiple Projects**: Separate ADO projects for each team (FE, BE, MOBILE)
6
+ * 2. **Single Project + Area Paths**: One ADO project with area paths per team
7
+ *
8
+ * Hierarchical work item types:
9
+ * - Epic (> 13 story points): Large feature area
10
+ * - Feature (8-13 story points): Medium feature
11
+ * - User Story (3-7 story points): Standard user story
12
+ * - Task (1-2 story points): Small implementation task
13
+ *
14
+ * @module ado-multi-project-sync
15
+ */
16
+
17
+ import axios, { AxiosInstance } from 'axios';
18
+ import {
19
+ UserStory,
20
+ getPrimaryProject,
21
+ suggestJiraItemType,
22
+ mapUserStoryToProjects
23
+ } from '../../../src/utils/project-mapper.js';
24
+ import { parseSpecFile } from '../../../src/utils/spec-splitter.js';
25
+
26
+ export interface AdoMultiProjectConfig {
27
+ organization: string;
28
+ pat: string; // Personal Access Token
29
+
30
+ // Pattern 1: Multiple projects (simple)
31
+ projects?: string[]; // ['FE-Project', 'BE-Project', 'MOBILE-Project']
32
+
33
+ // Pattern 2: Single project + area paths (advanced)
34
+ project?: string; // 'Shared-Project'
35
+ areaPaths?: string[]; // ['FE', 'BE', 'MOBILE']
36
+
37
+ // Work item type mapping (optional)
38
+ workItemTypes?: {
39
+ epic: string; // Default: 'Epic'
40
+ feature: string; // Default: 'Feature'
41
+ story: string; // Default: 'User Story'
42
+ task: string; // Default: 'Task'
43
+ };
44
+
45
+ // Settings
46
+ intelligentMapping?: boolean; // Default: true (auto-classify user stories)
47
+ autoCreateEpics?: boolean; // Default: true (create epic per project)
48
+ }
49
+
50
+ export interface AdoWorkItem {
51
+ id: number;
52
+ rev: number;
53
+ fields: Record<string, any>;
54
+ url: string;
55
+ }
56
+
57
+ export interface AdoSyncResult {
58
+ project: string;
59
+ workItemId: number;
60
+ workItemType: string;
61
+ title: string;
62
+ url: string;
63
+ action: 'created' | 'updated' | 'skipped';
64
+ confidence?: number; // Classification confidence (0.0-1.0)
65
+ }
66
+
67
+ /**
68
+ * Azure DevOps Multi-Project Sync Client
69
+ */
70
+ export class AdoMultiProjectSync {
71
+ private client: AxiosInstance;
72
+ private config: AdoMultiProjectConfig;
73
+
74
+ constructor(config: AdoMultiProjectConfig) {
75
+ this.config = config;
76
+
77
+ // Create axios instance with ADO authentication
78
+ this.client = axios.create({
79
+ baseURL: `https://dev.azure.com/${config.organization}`,
80
+ headers: {
81
+ 'Content-Type': 'application/json-patch+json',
82
+ 'Authorization': `Basic ${Buffer.from(':' + config.pat).toString('base64')}`
83
+ }
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Sync spec to ADO projects with intelligent mapping
89
+ *
90
+ * @param specPath Path to spec file
91
+ * @returns Array of sync results
92
+ */
93
+ async syncSpec(specPath: string): Promise<AdoSyncResult[]> {
94
+ const results: AdoSyncResult[] = [];
95
+
96
+ // Parse spec
97
+ const parsedSpec = await parseSpecFile(specPath);
98
+
99
+ // Determine sync pattern
100
+ const isAreaPathBased = !!this.config.project && !!this.config.areaPaths;
101
+
102
+ if (isAreaPathBased) {
103
+ // Pattern 2: Single project + area paths
104
+ results.push(...await this.syncAreaPathBased(parsedSpec));
105
+ } else if (this.config.projects) {
106
+ // Pattern 1: Multiple projects
107
+ results.push(...await this.syncMultipleProjects(parsedSpec));
108
+ } else {
109
+ throw new Error('Invalid config: Must specify projects[] or project+areaPaths[]');
110
+ }
111
+
112
+ return results;
113
+ }
114
+
115
+ /**
116
+ * Pattern 1: Sync to multiple ADO projects (simple)
117
+ *
118
+ * Each team → separate ADO project
119
+ * - FE user stories → FE-Project
120
+ * - BE user stories → BE-Project
121
+ * - MOBILE user stories → MOBILE-Project
122
+ */
123
+ private async syncMultipleProjects(parsedSpec: any): Promise<AdoSyncResult[]> {
124
+ const results: AdoSyncResult[] = [];
125
+
126
+ // Step 1: Create epic per project (if enabled)
127
+ const epicsByProject = new Map<string, number>(); // projectName → epicId
128
+
129
+ if (this.config.autoCreateEpics !== false) {
130
+ for (const projectName of this.config.projects!) {
131
+ const epicResult = await this.createEpicForProject(parsedSpec, projectName);
132
+ epicsByProject.set(projectName, epicResult.workItemId);
133
+ results.push(epicResult);
134
+ }
135
+ }
136
+
137
+ // Step 2: Classify user stories by project
138
+ const projectStories = new Map<string, Array<{ story: UserStory; confidence: number }>>();
139
+
140
+ for (const userStory of parsedSpec.userStories) {
141
+ if (this.config.intelligentMapping !== false) {
142
+ // Intelligent mapping (default)
143
+ const mappings = mapUserStoryToProjects(userStory);
144
+
145
+ if (mappings.length > 0 && mappings[0].confidence >= 0.3) {
146
+ const primary = mappings[0];
147
+ const projectName = this.findProjectForId(primary.projectId);
148
+
149
+ if (projectName) {
150
+ const existing = projectStories.get(projectName) || [];
151
+ existing.push({ story: userStory, confidence: primary.confidence });
152
+ projectStories.set(projectName, existing);
153
+ }
154
+ } else {
155
+ // No confident match - assign to first project or skip
156
+ console.warn(`⚠️ Low confidence for ${userStory.id} (${(mappings[0]?.confidence || 0) * 100}%) - assigning to ${this.config.projects![0]}`);
157
+ const fallback = this.config.projects![0];
158
+ const existing = projectStories.get(fallback) || [];
159
+ existing.push({ story: userStory, confidence: mappings[0]?.confidence || 0 });
160
+ projectStories.set(fallback, existing);
161
+ }
162
+ }
163
+ }
164
+
165
+ // Step 3: Create work items in each project
166
+ for (const [projectName, stories] of projectStories.entries()) {
167
+ const epicId = epicsByProject.get(projectName);
168
+
169
+ for (const { story, confidence } of stories) {
170
+ const result = await this.createWorkItemForUserStory(projectName, story, epicId, confidence);
171
+ results.push(result);
172
+ }
173
+ }
174
+
175
+ return results;
176
+ }
177
+
178
+ /**
179
+ * Pattern 2: Sync to single project with area paths (advanced)
180
+ *
181
+ * - Single ADO project with area paths for teams
182
+ * - Epic-level: Root area path
183
+ * - Story-level: Team-specific area paths
184
+ *
185
+ * Example:
186
+ * ADO Project: Shared-Project
187
+ * Epic: User Authentication (Root area path)
188
+ * User Story: Login UI (Area Path: Shared-Project\FE)
189
+ * User Story: Auth API (Area Path: Shared-Project\BE)
190
+ * User Story: Mobile Auth (Area Path: Shared-Project\MOBILE)
191
+ */
192
+ private async syncAreaPathBased(parsedSpec: any): Promise<AdoSyncResult[]> {
193
+ const results: AdoSyncResult[] = [];
194
+
195
+ if (!this.config.project || !this.config.areaPaths) {
196
+ throw new Error('Area path mode requires project and areaPaths');
197
+ }
198
+
199
+ // Step 1: Create epic in root area path
200
+ const epicResult = await this.createEpicInRootArea(parsedSpec);
201
+ results.push(epicResult);
202
+
203
+ // Step 2: Classify user stories by area path
204
+ const areaPathStories = new Map<string, UserStory[]>();
205
+
206
+ for (const userStory of parsedSpec.userStories) {
207
+ const primaryProject = getPrimaryProject(userStory);
208
+
209
+ if (primaryProject) {
210
+ const areaPath = this.findAreaPathForProjectId(primaryProject.projectId);
211
+
212
+ if (areaPath) {
213
+ const existing = areaPathStories.get(areaPath) || [];
214
+ existing.push(userStory);
215
+ areaPathStories.set(areaPath, existing);
216
+ }
217
+ }
218
+ }
219
+
220
+ // Step 3: Create work items in respective area paths
221
+ for (const [areaPath, stories] of areaPathStories.entries()) {
222
+ for (const story of stories) {
223
+ const result = await this.createWorkItemInAreaPath(areaPath, story, epicResult.workItemId);
224
+ results.push(result);
225
+ }
226
+ }
227
+
228
+ return results;
229
+ }
230
+
231
+ /**
232
+ * Create epic for project (Pattern 1: Multiple Projects)
233
+ */
234
+ private async createEpicForProject(parsedSpec: any, projectName: string): Promise<AdoSyncResult> {
235
+ const title = `${parsedSpec.metadata.title} - ${projectName}`;
236
+
237
+ const description = `<h2>${projectName} Implementation</h2>
238
+
239
+ <strong>Status</strong>: ${parsedSpec.metadata.status}<br/>
240
+ <strong>Priority</strong>: ${parsedSpec.metadata.priority}<br/>
241
+ <strong>Estimated Effort</strong>: ${parsedSpec.metadata.estimatedEffort || parsedSpec.metadata.estimated_effort}
242
+
243
+ <h3>Executive Summary</h3>
244
+
245
+ ${parsedSpec.executiveSummary}
246
+
247
+ <h3>Scope (${projectName})</h3>
248
+
249
+ This epic covers all ${projectName}-related user stories for "${parsedSpec.metadata.title}".
250
+
251
+ User stories will be added as child work items.
252
+
253
+ ---
254
+
255
+ 🤖 Auto-generated by SpecWeave
256
+ `;
257
+
258
+ const workItem = await this.createWorkItem(projectName, this.config.workItemTypes?.epic || 'Epic', {
259
+ 'System.Title': title,
260
+ 'System.Description': description,
261
+ 'System.State': 'New'
262
+ });
263
+
264
+ return {
265
+ project: projectName,
266
+ workItemId: workItem.id,
267
+ workItemType: 'Epic',
268
+ title,
269
+ url: workItem.url,
270
+ action: 'created'
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Create epic in root area path (Pattern 2: Area Paths)
276
+ */
277
+ private async createEpicInRootArea(parsedSpec: any): Promise<AdoSyncResult> {
278
+ const title = parsedSpec.metadata.title;
279
+
280
+ const description = `<h2>${parsedSpec.metadata.title}</h2>
281
+
282
+ <strong>Status</strong>: ${parsedSpec.metadata.status}<br/>
283
+ <strong>Priority</strong>: ${parsedSpec.metadata.priority}<br/>
284
+ <strong>Estimated Effort</strong>: ${parsedSpec.metadata.estimatedEffort || parsedSpec.metadata.estimated_effort}
285
+
286
+ <h3>Executive Summary</h3>
287
+
288
+ ${parsedSpec.executiveSummary}
289
+
290
+ <h3>User Stories (${parsedSpec.userStories.length} total)</h3>
291
+
292
+ <ul>
293
+ ${parsedSpec.userStories.map((s: UserStory, i: number) => `<li>${i + 1}. ${s.id}: ${s.title}</li>`).join('\n')}
294
+ </ul>
295
+
296
+ ---
297
+
298
+ 🤖 Auto-generated by SpecWeave
299
+ `;
300
+
301
+ const workItem = await this.createWorkItem(this.config.project!, this.config.workItemTypes?.epic || 'Epic', {
302
+ 'System.Title': title,
303
+ 'System.Description': description,
304
+ 'System.AreaPath': this.config.project!, // Root area path
305
+ 'System.State': 'New'
306
+ });
307
+
308
+ return {
309
+ project: this.config.project!,
310
+ workItemId: workItem.id,
311
+ workItemType: 'Epic',
312
+ title,
313
+ url: workItem.url,
314
+ action: 'created'
315
+ };
316
+ }
317
+
318
+ /**
319
+ * Create work item for user story (Pattern 1: Multiple Projects)
320
+ */
321
+ private async createWorkItemForUserStory(
322
+ projectName: string,
323
+ userStory: UserStory,
324
+ epicId?: number,
325
+ confidence?: number
326
+ ): Promise<AdoSyncResult> {
327
+ const title = `${userStory.id}: ${userStory.title}`;
328
+
329
+ // Determine work item type based on story points
330
+ const itemType = this.mapItemTypeToAdo(suggestJiraItemType(userStory));
331
+
332
+ const description = `<h3>${userStory.title}</h3>
333
+
334
+ ${userStory.description}
335
+
336
+ <h4>Acceptance Criteria</h4>
337
+
338
+ <ul>
339
+ ${userStory.acceptanceCriteria.map((ac, i) => `<li>${ac}</li>`).join('\n')}
340
+ </ul>
341
+
342
+ ${userStory.technicalContext ? `<h4>Technical Context</h4>\n\n${userStory.technicalContext}\n` : ''}
343
+
344
+ ${confidence !== undefined ? `<p><em>Classification confidence: ${(confidence * 100).toFixed(0)}%</em></p>\n` : ''}
345
+
346
+ <p>🤖 Auto-generated by SpecWeave</p>
347
+ `;
348
+
349
+ const fields: Record<string, any> = {
350
+ 'System.Title': title,
351
+ 'System.Description': description,
352
+ 'System.State': 'New'
353
+ };
354
+
355
+ const workItem = await this.createWorkItem(projectName, itemType, fields);
356
+
357
+ // Link to epic if provided
358
+ if (epicId) {
359
+ await this.linkWorkItems(workItem.id, epicId, 'System.LinkTypes.Hierarchy-Reverse');
360
+ }
361
+
362
+ return {
363
+ project: projectName,
364
+ workItemId: workItem.id,
365
+ workItemType: itemType,
366
+ title,
367
+ url: workItem.url,
368
+ action: 'created',
369
+ confidence
370
+ };
371
+ }
372
+
373
+ /**
374
+ * Create work item in area path (Pattern 2: Area Paths)
375
+ */
376
+ private async createWorkItemInAreaPath(
377
+ areaPath: string,
378
+ userStory: UserStory,
379
+ epicId?: number
380
+ ): Promise<AdoSyncResult> {
381
+ const title = `${userStory.id}: ${userStory.title}`;
382
+
383
+ // Determine work item type based on story points
384
+ const itemType = this.mapItemTypeToAdo(suggestJiraItemType(userStory));
385
+
386
+ const description = `<h3>${userStory.title}</h3>
387
+
388
+ ${userStory.description}
389
+
390
+ <h4>Acceptance Criteria</h4>
391
+
392
+ <ul>
393
+ ${userStory.acceptanceCriteria.map((ac, i) => `<li>${ac}</li>`).join('\n')}
394
+ </ul>
395
+
396
+ ${userStory.technicalContext ? `<h4>Technical Context</h4>\n\n${userStory.technicalContext}\n` : ''}
397
+
398
+ <p>🤖 Auto-generated by SpecWeave</p>
399
+ `;
400
+
401
+ const fields: Record<string, any> = {
402
+ 'System.Title': title,
403
+ 'System.Description': description,
404
+ 'System.AreaPath': `${this.config.project}\\${areaPath}`, // Team-specific area path
405
+ 'System.State': 'New'
406
+ };
407
+
408
+ const workItem = await this.createWorkItem(this.config.project!, itemType, fields);
409
+
410
+ // Link to epic if provided
411
+ if (epicId) {
412
+ await this.linkWorkItems(workItem.id, epicId, 'System.LinkTypes.Hierarchy-Reverse');
413
+ }
414
+
415
+ return {
416
+ project: this.config.project!,
417
+ workItemId: workItem.id,
418
+ workItemType: itemType,
419
+ title,
420
+ url: workItem.url,
421
+ action: 'created'
422
+ };
423
+ }
424
+
425
+ /**
426
+ * Create work item via ADO REST API
427
+ */
428
+ private async createWorkItem(
429
+ project: string,
430
+ workItemType: string,
431
+ fields: Record<string, any>
432
+ ): Promise<AdoWorkItem> {
433
+ // Build JSON patch document
434
+ const patchDocument = Object.entries(fields).map(([key, value]) => ({
435
+ op: 'add',
436
+ path: `/fields/${key}`,
437
+ value
438
+ }));
439
+
440
+ const response = await this.client.post(
441
+ `/${project}/_apis/wit/workitems/$${workItemType}?api-version=7.0`,
442
+ patchDocument
443
+ );
444
+
445
+ return response.data;
446
+ }
447
+
448
+ /**
449
+ * Link work items (parent-child relationship)
450
+ */
451
+ private async linkWorkItems(
452
+ sourceId: number,
453
+ targetId: number,
454
+ linkType: string
455
+ ): Promise<void> {
456
+ const patchDocument = [
457
+ {
458
+ op: 'add',
459
+ path: '/relations/-',
460
+ value: {
461
+ rel: linkType,
462
+ url: `https://dev.azure.com/${this.config.organization}/_apis/wit/workItems/${targetId}`
463
+ }
464
+ }
465
+ ];
466
+
467
+ await this.client.patch(
468
+ `/_apis/wit/workitems/${sourceId}?api-version=7.0`,
469
+ patchDocument
470
+ );
471
+ }
472
+
473
+ /**
474
+ * Map Jira-style item type to ADO work item type
475
+ */
476
+ private mapItemTypeToAdo(itemType: 'Epic' | 'Story' | 'Task' | 'Subtask'): string {
477
+ const mapping = this.config.workItemTypes || {};
478
+
479
+ switch (itemType) {
480
+ case 'Epic':
481
+ return mapping.epic || 'Epic';
482
+ case 'Story':
483
+ return mapping.story || 'User Story';
484
+ case 'Task':
485
+ return mapping.task || 'Task';
486
+ case 'Subtask':
487
+ return mapping.task || 'Task'; // ADO doesn't have subtasks, use Task
488
+ default:
489
+ return 'User Story';
490
+ }
491
+ }
492
+
493
+ /**
494
+ * Find ADO project name for project ID
495
+ *
496
+ * Maps project IDs to ADO project names:
497
+ * - FE → FE-Project
498
+ * - BE → BE-Project
499
+ * - MOBILE → MOBILE-Project
500
+ */
501
+ private findProjectForId(projectId: string): string | undefined {
502
+ if (!this.config.projects) return undefined;
503
+
504
+ // Try exact match first
505
+ let match = this.config.projects.find(project => project.toLowerCase().includes(projectId.toLowerCase()));
506
+
507
+ if (!match) {
508
+ // Try fuzzy match (FE → frontend, BE → backend, MOBILE → mobile)
509
+ const fuzzyMap: Record<string, string[]> = {
510
+ FE: ['frontend', 'web', 'ui', 'client', 'fe'],
511
+ BE: ['backend', 'api', 'server', 'be'],
512
+ MOBILE: ['mobile', 'app', 'ios', 'android'],
513
+ INFRA: ['infra', 'infrastructure', 'devops', 'platform']
514
+ };
515
+
516
+ const keywords = fuzzyMap[projectId] || [];
517
+
518
+ match = this.config.projects.find(project =>
519
+ keywords.some(keyword => project.toLowerCase().includes(keyword))
520
+ );
521
+ }
522
+
523
+ return match;
524
+ }
525
+
526
+ /**
527
+ * Find area path for project ID
528
+ *
529
+ * Maps project IDs to area paths:
530
+ * - FE → FE
531
+ * - BE → BE
532
+ * - MOBILE → MOBILE
533
+ */
534
+ private findAreaPathForProjectId(projectId: string): string | undefined {
535
+ if (!this.config.areaPaths) return undefined;
536
+
537
+ // Try exact match first
538
+ let match = this.config.areaPaths.find(areaPath => areaPath.toLowerCase() === projectId.toLowerCase());
539
+
540
+ if (!match) {
541
+ // Try fuzzy match
542
+ const fuzzyMap: Record<string, string[]> = {
543
+ FE: ['frontend', 'web', 'ui', 'client', 'fe'],
544
+ BE: ['backend', 'api', 'server', 'be'],
545
+ MOBILE: ['mobile', 'app', 'ios', 'android'],
546
+ INFRA: ['infra', 'infrastructure', 'devops', 'platform']
547
+ };
548
+
549
+ const keywords = fuzzyMap[projectId] || [];
550
+
551
+ match = this.config.areaPaths.find(areaPath =>
552
+ keywords.some(keyword => areaPath.toLowerCase().includes(keyword))
553
+ );
554
+ }
555
+
556
+ return match;
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Format ADO sync results for display
562
+ */
563
+ export function formatAdoSyncResults(results: AdoSyncResult[]): string {
564
+ const lines: string[] = [];
565
+
566
+ lines.push('📊 Azure DevOps Multi-Project Sync Results:\n');
567
+
568
+ const byProject = new Map<string, AdoSyncResult[]>();
569
+
570
+ for (const result of results) {
571
+ const existing = byProject.get(result.project) || [];
572
+ existing.push(result);
573
+ byProject.set(result.project, existing);
574
+ }
575
+
576
+ for (const [project, projectResults] of byProject.entries()) {
577
+ lines.push(`\n**ADO Project ${project}**:`);
578
+
579
+ for (const result of projectResults) {
580
+ const icon = result.action === 'created' ? '✅' : result.action === 'updated' ? '🔄' : '⏭️';
581
+ const confidence = result.confidence !== undefined ? ` (${(result.confidence * 100).toFixed(0)}% confidence)` : '';
582
+ lines.push(` ${icon} #${result.workItemId} [${result.workItemType}]: ${result.title}${confidence}`);
583
+ lines.push(` ${result.url}`);
584
+ }
585
+ }
586
+
587
+ lines.push(`\n✅ Total: ${results.length} work items synced\n`);
588
+
589
+ // Show work item type distribution
590
+ const epicCount = results.filter(r => r.workItemType === 'Epic').length;
591
+ const featureCount = results.filter(r => r.workItemType === 'Feature').length;
592
+ const storyCount = results.filter(r => r.workItemType === 'User Story').length;
593
+ const taskCount = results.filter(r => r.workItemType === 'Task').length;
594
+
595
+ lines.push('📈 Work Item Type Distribution:');
596
+ if (epicCount > 0) lines.push(` - Epics: ${epicCount}`);
597
+ if (featureCount > 0) lines.push(` - Features: ${featureCount}`);
598
+ if (storyCount > 0) lines.push(` - User Stories: ${storyCount}`);
599
+ if (taskCount > 0) lines.push(` - Tasks: ${taskCount}`);
600
+
601
+ return lines.join('\n');
602
+ }
603
+
604
+ /**
605
+ * Validate ADO projects exist
606
+ *
607
+ * @param config ADO configuration
608
+ * @param projectNames Array of project names to validate
609
+ * @returns Validation results (missing projects)
610
+ */
611
+ export async function validateAdoProjects(
612
+ config: AdoMultiProjectConfig,
613
+ projectNames: string[]
614
+ ): Promise<string[]> {
615
+ const missing: string[] = [];
616
+
617
+ const client = axios.create({
618
+ baseURL: `https://dev.azure.com/${config.organization}`,
619
+ headers: {
620
+ 'Authorization': `Basic ${Buffer.from(':' + config.pat).toString('base64')}`
621
+ }
622
+ });
623
+
624
+ for (const name of projectNames) {
625
+ try {
626
+ await client.get(`/_apis/projects/${name}?api-version=7.0`);
627
+ } catch (error) {
628
+ missing.push(name);
629
+ }
630
+ }
631
+
632
+ return missing;
633
+ }
@@ -1,15 +1,29 @@
1
1
  ---
2
2
  name: docusaurus
3
- description: Expert in generating Docusaurus documentation sites from SpecWeave structure. Converts .specweave/docs/public/ into a beautiful, deployable static site with search, versioning, and hosting options. Activates for docusaurus, create docs site, generate documentation, public docs, documentation site, host docs, deploy documentation, static site generator, client documentation.
3
+ description: Expert in generating beautiful Docusaurus documentation sites from SpecWeave structure. ALWAYS use this for internal docs preview, browsing docs locally, viewing documentation in browser. Converts .specweave/docs/internal/ and .specweave/docs/public/ into beautiful, searchable sites with navigation, search, and Mermaid diagrams. Activates for preview internal docs, view docs locally, see docs in browser, browse documentation, internal docs preview, docusaurus, create docs site, generate documentation, public docs, documentation site, host docs, deploy documentation, static site generator, client documentation, engineering playbook, dual docs sites.
4
4
  ---
5
5
 
6
6
  # Docusaurus Documentation Generator
7
7
 
8
- Expert skill for creating production-ready Docusaurus documentation sites from SpecWeave public documentation.
8
+ Expert skill for creating production-ready Docusaurus documentation sites from SpecWeave documentation.
9
+
10
+ ## 🚨 CRITICAL: Default Behavior for Internal Docs Preview
11
+
12
+ **When users ask to preview/view/browse internal docs locally:**
13
+
14
+ 1. **IMMEDIATELY** invoke `/specweave-docs-preview:preview` command
15
+ 2. This launches a beautiful Docusaurus site with:
16
+ - Auto-generated navigation from folder structure
17
+ - Search functionality
18
+ - Mermaid diagram rendering
19
+ - Hot reload for live editing
20
+ - Professional theming
21
+ 3. **DO NOT** use basic file serving or simple markdown rendering
22
+ 4. **ALWAYS** prefer Docusaurus for the best user experience
9
23
 
10
24
  ## What This Skill Does
11
25
 
12
- Converts your SpecWeave public documentation (`.specweave/docs/public/`) into a beautiful, searchable, deployable documentation website using Docusaurus v3.
26
+ Converts your SpecWeave documentation (`.specweave/docs/internal/` or `.specweave/docs/public/`) into a beautiful, searchable, deployable documentation website using Docusaurus v3.
13
27
 
14
28
  ## Key Capabilities
15
29