specweave 0.17.16 → 0.17.19
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/CLAUDE.md +405 -2495
- package/README.md +92 -2
- package/dist/locales/de/.gitkeep +0 -0
- package/dist/locales/de/cli.json +108 -0
- package/dist/locales/en/cli.json +287 -0
- package/dist/locales/en/errors.json +7 -0
- package/dist/locales/en/templates.json +6 -0
- package/dist/locales/es/.gitkeep +0 -0
- package/dist/locales/es/cli.json +41 -0
- package/dist/locales/fr/.gitkeep +0 -0
- package/dist/locales/fr/cli.json +108 -0
- package/dist/locales/ja/.gitkeep +0 -0
- package/dist/locales/ja/cli.json +108 -0
- package/dist/locales/ko/.gitkeep +0 -0
- package/dist/locales/ko/cli.json +108 -0
- package/dist/locales/pt/.gitkeep +0 -0
- package/dist/locales/pt/cli.json +108 -0
- package/dist/locales/ru/.gitkeep +0 -0
- package/dist/locales/ru/cli.json +269 -0
- package/dist/locales/zh/.gitkeep +0 -0
- package/dist/locales/zh/cli.json +108 -0
- package/dist/plugins/specweave/lib/hooks/sync-living-docs.d.ts.map +1 -1
- package/dist/plugins/specweave/lib/hooks/sync-living-docs.js +188 -36
- package/dist/plugins/specweave/lib/hooks/sync-living-docs.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-spec-content-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-spec-content-sync.js +65 -6
- package/dist/plugins/specweave-ado/lib/ado-spec-content-sync.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts +54 -0
- package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-ado/lib/ado-status-sync.js +86 -0
- package/dist/plugins/specweave-ado/lib/ado-status-sync.js.map +1 -0
- package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.d.ts +25 -0
- package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.js +191 -0
- package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts +139 -0
- package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/duplicate-detector.js +389 -0
- package/dist/plugins/specweave-github/lib/duplicate-detector.js.map +1 -0
- package/dist/plugins/specweave-github/lib/enhanced-github-sync.d.ts +26 -0
- package/dist/plugins/specweave-github/lib/enhanced-github-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/enhanced-github-sync.js +249 -0
- package/dist/plugins/specweave-github/lib/enhanced-github-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts +63 -0
- package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/epic-content-builder.js +216 -0
- package/dist/plugins/specweave-github/lib/epic-content-builder.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-client.d.ts +1 -1
- package/dist/plugins/specweave-github/lib/github-client.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client.js +25 -13
- package/dist/plugins/specweave-github/lib/github-client.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts +83 -0
- package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-epic-sync.js +466 -0
- package/dist/plugins/specweave-github/lib/github-epic-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-status-sync.d.ts +43 -0
- package/dist/plugins/specweave-github/lib/github-status-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-status-sync.js +82 -0
- package/dist/plugins/specweave-github/lib/github-status-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/task-sync.d.ts +5 -0
- package/dist/plugins/specweave-github/lib/task-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/task-sync.js +38 -2
- package/dist/plugins/specweave-github/lib/task-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts +28 -0
- package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js +156 -0
- package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts +66 -0
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.js +274 -0
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +56 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js +93 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -0
- package/dist/spec-parser.js +629 -0
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +107 -3
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/index.js +48 -3
- package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
- package/dist/src/core/deduplication/command-deduplicator.d.ts +166 -0
- package/dist/src/core/deduplication/command-deduplicator.d.ts.map +1 -0
- package/dist/src/core/deduplication/command-deduplicator.js +254 -0
- package/dist/src/core/deduplication/command-deduplicator.js.map +1 -0
- package/dist/src/core/living-docs/hierarchy-mapper.d.ts +142 -0
- package/dist/src/core/living-docs/hierarchy-mapper.d.ts.map +1 -0
- package/dist/src/core/living-docs/hierarchy-mapper.js +453 -0
- package/dist/src/core/living-docs/hierarchy-mapper.js.map +1 -0
- package/dist/src/core/living-docs/index.d.ts +10 -84
- package/dist/src/core/living-docs/index.d.ts.map +1 -1
- package/dist/src/core/living-docs/index.js +10 -164
- package/dist/src/core/living-docs/index.js.map +1 -1
- package/dist/src/core/living-docs/spec-distributor.d.ts +106 -0
- package/dist/src/core/living-docs/spec-distributor.d.ts.map +1 -0
- package/dist/src/core/living-docs/spec-distributor.js +823 -0
- package/dist/src/core/living-docs/spec-distributor.js.map +1 -0
- package/dist/src/core/living-docs/types.d.ts +201 -0
- package/dist/src/core/living-docs/types.d.ts.map +1 -0
- package/dist/src/core/living-docs/types.js +15 -0
- package/dist/src/core/living-docs/types.js.map +1 -0
- package/dist/src/core/logging/prompt-logger.d.ts +70 -0
- package/dist/src/core/logging/prompt-logger.d.ts.map +1 -0
- package/dist/src/core/logging/prompt-logger.js +247 -0
- package/dist/src/core/logging/prompt-logger.js.map +1 -0
- package/dist/src/core/status-line/status-line-manager.d.ts +15 -24
- package/dist/src/core/status-line/status-line-manager.d.ts.map +1 -1
- package/dist/src/core/status-line/status-line-manager.js +33 -70
- package/dist/src/core/status-line/status-line-manager.js.map +1 -1
- package/dist/src/core/status-line/types.d.ts +19 -31
- package/dist/src/core/status-line/types.d.ts.map +1 -1
- package/dist/src/core/status-line/types.js +5 -9
- package/dist/src/core/status-line/types.js.map +1 -1
- package/dist/src/core/sync/conflict-resolver.d.ts +66 -0
- package/dist/src/core/sync/conflict-resolver.d.ts.map +1 -0
- package/dist/src/core/sync/conflict-resolver.js +108 -0
- package/dist/src/core/sync/conflict-resolver.js.map +1 -0
- package/dist/src/core/sync/enhanced-content-builder.d.ts +55 -0
- package/dist/src/core/sync/enhanced-content-builder.d.ts.map +1 -0
- package/dist/src/core/sync/enhanced-content-builder.js +202 -0
- package/dist/src/core/sync/enhanced-content-builder.js.map +1 -0
- package/dist/src/core/sync/label-detector.d.ts +66 -0
- package/dist/src/core/sync/label-detector.d.ts.map +1 -0
- package/dist/src/core/sync/label-detector.js +211 -0
- package/dist/src/core/sync/label-detector.js.map +1 -0
- package/dist/src/core/sync/retry-logic.d.ts +64 -0
- package/dist/src/core/sync/retry-logic.d.ts.map +1 -0
- package/dist/src/core/sync/retry-logic.js +165 -0
- package/dist/src/core/sync/retry-logic.js.map +1 -0
- package/dist/src/core/sync/spec-content-sync.d.ts +88 -0
- package/dist/src/core/sync/spec-content-sync.d.ts.map +1 -0
- package/dist/src/core/sync/spec-content-sync.js +5 -0
- package/dist/src/core/sync/spec-content-sync.js.map +1 -0
- package/dist/src/core/sync/spec-increment-mapper.d.ts +100 -0
- package/dist/src/core/sync/spec-increment-mapper.d.ts.map +1 -0
- package/dist/src/core/sync/spec-increment-mapper.js +424 -0
- package/dist/src/core/sync/spec-increment-mapper.js.map +1 -0
- package/dist/src/core/sync/status-cache.d.ts +91 -0
- package/dist/src/core/sync/status-cache.d.ts.map +1 -0
- package/dist/src/core/sync/status-cache.js +140 -0
- package/dist/src/core/sync/status-cache.js.map +1 -0
- package/dist/src/core/sync/status-mapper.d.ts +69 -0
- package/dist/src/core/sync/status-mapper.d.ts.map +1 -0
- package/dist/src/core/sync/status-mapper.js +90 -0
- package/dist/src/core/sync/status-mapper.js.map +1 -0
- package/dist/src/core/sync/status-sync-engine.d.ts +162 -0
- package/dist/src/core/sync/status-sync-engine.d.ts.map +1 -0
- package/dist/src/core/sync/status-sync-engine.js +347 -0
- package/dist/src/core/sync/status-sync-engine.js.map +1 -0
- package/dist/src/core/sync/sync-event-logger.d.ts +99 -0
- package/dist/src/core/sync/sync-event-logger.d.ts.map +1 -0
- package/dist/src/core/sync/sync-event-logger.js +103 -0
- package/dist/src/core/sync/sync-event-logger.js.map +1 -0
- package/dist/src/core/sync/types.d.ts +52 -0
- package/dist/src/core/sync/types.d.ts.map +1 -0
- package/dist/src/core/sync/types.js +5 -0
- package/dist/src/core/sync/types.js.map +1 -0
- package/dist/src/core/sync/workflow-detector.d.ts +95 -0
- package/dist/src/core/sync/workflow-detector.d.ts.map +1 -0
- package/dist/src/core/sync/workflow-detector.js +175 -0
- package/dist/src/core/sync/workflow-detector.js.map +1 -0
- package/dist/src/core/types/config.d.ts +51 -0
- package/dist/src/core/types/config.d.ts.map +1 -1
- package/dist/src/core/types/config.js +47 -0
- package/dist/src/core/types/config.js.map +1 -1
- package/dist/src/core/types/increment-metadata.d.ts +4 -0
- package/dist/src/core/types/increment-metadata.d.ts.map +1 -1
- package/dist/src/core/types/increment-metadata.js.map +1 -1
- package/dist/src/utils/github-url.d.ts +53 -0
- package/dist/src/utils/github-url.d.ts.map +1 -0
- package/dist/src/utils/github-url.js +90 -0
- package/dist/src/utils/github-url.js.map +1 -0
- package/dist/src/utils/spec-parser.d.ts +145 -0
- package/dist/src/utils/spec-parser.d.ts.map +1 -0
- package/dist/src/utils/spec-parser.js +640 -0
- package/dist/src/utils/spec-parser.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +1 -1
- package/plugins/specweave/agents/pm/AGENT.md +160 -13
- package/plugins/specweave/agents/pm/templates/increment-spec.md +158 -0
- package/plugins/specweave/agents/pm/templates/living-docs-spec.md +113 -0
- package/plugins/specweave/commands/specweave-done.md +163 -0
- package/plugins/specweave/commands/specweave.md +70 -405
- package/plugins/specweave/hooks/hooks.json +4 -0
- package/plugins/specweave/hooks/lib/sync-spec-content.sh +2 -2
- package/plugins/specweave/hooks/lib/update-status-line.sh +79 -111
- package/plugins/specweave/hooks/post-increment-planning.sh +133 -37
- package/plugins/specweave/hooks/pre-command-deduplication.sh +86 -0
- package/plugins/specweave/lib/hooks/sync-living-docs.js +139 -34
- package/plugins/specweave/lib/hooks/sync-living-docs.ts +234 -38
- package/plugins/specweave/skills/SKILLS-INDEX.md +4 -24
- package/plugins/specweave/skills/increment-planner/SKILL.md +94 -0
- package/plugins/specweave/skills/increment-work-router/SKILL.md +466 -0
- package/plugins/specweave-ado/commands/specweave-ado-sync-spec.md +1 -1
- package/plugins/specweave-ado/lib/ado-spec-content-sync.js +49 -5
- package/plugins/specweave-ado/lib/ado-spec-content-sync.ts +72 -6
- package/plugins/specweave-ado/lib/ado-status-sync.js +80 -0
- package/plugins/specweave-ado/lib/ado-status-sync.ts +121 -0
- package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
- package/plugins/specweave-github/commands/specweave-github-cleanup-duplicates.md +205 -0
- package/plugins/specweave-github/commands/specweave-github-sync-epic.md +248 -0
- package/plugins/specweave-github/commands/specweave-github-sync-spec.md +1 -1
- package/plugins/specweave-github/hooks/post-task-completion.sh +32 -0
- package/plugins/specweave-github/lib/duplicate-detector.js +370 -0
- package/plugins/specweave-github/lib/duplicate-detector.ts +525 -0
- package/plugins/specweave-github/lib/enhanced-github-sync.js +220 -0
- package/plugins/specweave-github/lib/enhanced-github-sync.ts +322 -0
- package/plugins/specweave-github/lib/epic-content-builder.js +227 -0
- package/plugins/specweave-github/lib/epic-content-builder.ts +317 -0
- package/plugins/specweave-github/lib/github-client.js +21 -10
- package/plugins/specweave-github/lib/github-client.ts +27 -16
- package/plugins/specweave-github/lib/github-epic-sync.js +488 -0
- package/plugins/specweave-github/lib/github-epic-sync.ts +715 -0
- package/plugins/specweave-github/lib/github-status-sync.js +71 -0
- package/plugins/specweave-github/lib/github-status-sync.ts +107 -0
- package/plugins/specweave-github/lib/task-sync.js +33 -2
- package/plugins/specweave-github/lib/task-sync.ts +44 -2
- package/plugins/specweave-jira/commands/specweave-jira-sync-epic.md +267 -0
- package/plugins/specweave-jira/commands/specweave-jira-sync-spec.md +1 -1
- package/plugins/specweave-jira/lib/enhanced-jira-sync.js +134 -0
- package/plugins/specweave-jira/lib/enhanced-jira-sync.ts +196 -0
- package/plugins/specweave-jira/lib/jira-epic-sync.js +304 -0
- package/plugins/specweave-jira/lib/jira-epic-sync.ts +459 -0
- package/plugins/specweave-jira/lib/jira-status-sync.js +79 -0
- package/plugins/specweave-jira/lib/jira-status-sync.ts +139 -0
- package/plugins/specweave-release/commands/specweave-release-platform.md +1 -1
- package/plugins/specweave-release/hooks/post-task-completion.sh +2 -2
- package/src/templates/AGENTS.md.template +88 -1
- package/src/templates/CLAUDE.md.template +49 -0
- package/plugins/specweave/skills/increment-quality-judge/SKILL.md +0 -524
- package/plugins/specweave/skills/plugin-installer/SKILL.md +0 -353
|
@@ -20,8 +20,9 @@ import {
|
|
|
20
20
|
ContentSyncResult,
|
|
21
21
|
} from '../../../src/core/spec-content-sync.js';
|
|
22
22
|
import { SyncProfile } from '../../../src/core/types/sync-profile.js';
|
|
23
|
-
import
|
|
24
|
-
import
|
|
23
|
+
import { SpecIncrementMapper, TaskInfo } from '../../../src/core/sync/spec-increment-mapper.js';
|
|
24
|
+
import * as path from 'path';
|
|
25
|
+
import * as fs from 'fs/promises';
|
|
25
26
|
|
|
26
27
|
export interface AdoContentSyncOptions {
|
|
27
28
|
specPath: string;
|
|
@@ -91,9 +92,12 @@ async function createAdoFeature(
|
|
|
91
92
|
const { specPath, dryRun, verbose } = options;
|
|
92
93
|
|
|
93
94
|
try {
|
|
95
|
+
// Get task mappings (if available)
|
|
96
|
+
const tasks = await getTaskMappings(specPath, spec.id);
|
|
97
|
+
|
|
94
98
|
// Build feature title and description
|
|
95
99
|
const title = `[${spec.id.toUpperCase()}] ${spec.title}`;
|
|
96
|
-
const description = buildAdoDescription(spec);
|
|
100
|
+
const description = buildAdoDescription(spec, tasks);
|
|
97
101
|
|
|
98
102
|
if (verbose) {
|
|
99
103
|
console.log(`\n📝 Creating ADO feature:`);
|
|
@@ -196,9 +200,12 @@ async function updateAdoFeature(
|
|
|
196
200
|
}
|
|
197
201
|
}
|
|
198
202
|
|
|
203
|
+
// Get task mappings (if available)
|
|
204
|
+
const tasks = await getTaskMappings(specPath, spec.id);
|
|
205
|
+
|
|
199
206
|
// Build updated content
|
|
200
207
|
const newTitle = `[${spec.id.toUpperCase()}] ${spec.title}`;
|
|
201
|
-
const newDescription = buildAdoDescription(spec);
|
|
208
|
+
const newDescription = buildAdoDescription(spec, tasks);
|
|
202
209
|
|
|
203
210
|
if (dryRun) {
|
|
204
211
|
console.log('\n🔍 Dry run - would update feature:');
|
|
@@ -242,10 +249,10 @@ async function updateAdoFeature(
|
|
|
242
249
|
}
|
|
243
250
|
|
|
244
251
|
/**
|
|
245
|
-
* Build ADO description from spec content
|
|
252
|
+
* Build ADO description from spec content with optional task mappings
|
|
246
253
|
* ADO supports HTML in description
|
|
247
254
|
*/
|
|
248
|
-
function buildAdoDescription(spec: SpecContent): string {
|
|
255
|
+
function buildAdoDescription(spec: SpecContent, tasks?: TaskInfo[]): string {
|
|
249
256
|
let html = '';
|
|
250
257
|
|
|
251
258
|
// Add spec description
|
|
@@ -271,6 +278,20 @@ function buildAdoDescription(spec: SpecContent): string {
|
|
|
271
278
|
}
|
|
272
279
|
}
|
|
273
280
|
|
|
281
|
+
// Add task mappings (if provided)
|
|
282
|
+
if (tasks && tasks.length > 0) {
|
|
283
|
+
html += '<h2>Implementation Tasks</h2>';
|
|
284
|
+
html += '<ul>';
|
|
285
|
+
for (const task of tasks) {
|
|
286
|
+
html += `<li><strong>${task.id}</strong>: ${escapeHtml(task.title)}`;
|
|
287
|
+
if (task.userStories && task.userStories.length > 0) {
|
|
288
|
+
html += ` (${task.userStories.join(', ')})`;
|
|
289
|
+
}
|
|
290
|
+
html += '</li>';
|
|
291
|
+
}
|
|
292
|
+
html += '</ul>';
|
|
293
|
+
}
|
|
294
|
+
|
|
274
295
|
// Add metadata
|
|
275
296
|
if (spec.metadata.priority) {
|
|
276
297
|
html += `<p><strong>Priority:</strong> ${spec.metadata.priority}</p>`;
|
|
@@ -279,6 +300,51 @@ function buildAdoDescription(spec: SpecContent): string {
|
|
|
279
300
|
return html;
|
|
280
301
|
}
|
|
281
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Get task mappings for a spec (if available)
|
|
305
|
+
*/
|
|
306
|
+
async function getTaskMappings(specPath: string, specId: string): Promise<TaskInfo[] | undefined> {
|
|
307
|
+
try {
|
|
308
|
+
// Find SpecWeave root
|
|
309
|
+
const rootDir = await findSpecWeaveRoot(specPath);
|
|
310
|
+
|
|
311
|
+
// Use SpecIncrementMapper to get task mappings
|
|
312
|
+
const mapper = new SpecIncrementMapper(rootDir);
|
|
313
|
+
const mapping = await mapper.mapSpecToIncrements(specId);
|
|
314
|
+
|
|
315
|
+
if (mapping.increments.length > 0) {
|
|
316
|
+
// Return tasks from the first (most recent) increment
|
|
317
|
+
return mapping.increments[0].tasks;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return undefined;
|
|
321
|
+
} catch (error) {
|
|
322
|
+
// If mapping fails, just return undefined (not critical)
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Find SpecWeave root directory from spec path
|
|
329
|
+
*/
|
|
330
|
+
async function findSpecWeaveRoot(specPath: string): Promise<string> {
|
|
331
|
+
let currentDir = path.dirname(specPath);
|
|
332
|
+
|
|
333
|
+
while (true) {
|
|
334
|
+
const specweaveDir = path.join(currentDir, '.specweave');
|
|
335
|
+
try {
|
|
336
|
+
await fs.access(specweaveDir);
|
|
337
|
+
return currentDir;
|
|
338
|
+
} catch {
|
|
339
|
+
const parentDir = path.dirname(currentDir);
|
|
340
|
+
if (parentDir === currentDir) {
|
|
341
|
+
throw new Error('.specweave directory not found');
|
|
342
|
+
}
|
|
343
|
+
currentDir = parentDir;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
282
348
|
/**
|
|
283
349
|
* Escape HTML special characters
|
|
284
350
|
*/
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
class AdoStatusSync {
|
|
3
|
+
constructor(organization, project, personalAccessToken) {
|
|
4
|
+
this.organization = organization;
|
|
5
|
+
this.project = project;
|
|
6
|
+
this.client = axios.create({
|
|
7
|
+
baseURL: `https://dev.azure.com/${organization}/${project}/_apis`,
|
|
8
|
+
auth: {
|
|
9
|
+
username: "",
|
|
10
|
+
// Empty for PAT auth
|
|
11
|
+
password: personalAccessToken
|
|
12
|
+
},
|
|
13
|
+
headers: {
|
|
14
|
+
"Content-Type": "application/json-patch+json",
|
|
15
|
+
"Accept": "application/json"
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Get current status from ADO work item
|
|
21
|
+
*
|
|
22
|
+
* @param workItemId - ADO work item ID (e.g., 123)
|
|
23
|
+
* @returns Current work item state
|
|
24
|
+
*/
|
|
25
|
+
async getStatus(workItemId) {
|
|
26
|
+
const response = await this.client.get(
|
|
27
|
+
`/wit/workitems/${workItemId}?api-version=7.0`
|
|
28
|
+
);
|
|
29
|
+
return {
|
|
30
|
+
state: response.data.fields["System.State"]
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Update ADO work item state
|
|
35
|
+
*
|
|
36
|
+
* Uses JSON Patch format to update System.State field.
|
|
37
|
+
*
|
|
38
|
+
* @param workItemId - ADO work item ID (e.g., 123)
|
|
39
|
+
* @param status - Desired status
|
|
40
|
+
*/
|
|
41
|
+
async updateStatus(workItemId, status) {
|
|
42
|
+
const patch = [
|
|
43
|
+
{
|
|
44
|
+
op: "add",
|
|
45
|
+
path: "/fields/System.State",
|
|
46
|
+
value: status.state
|
|
47
|
+
}
|
|
48
|
+
];
|
|
49
|
+
await this.client.patch(
|
|
50
|
+
`/wit/workitems/${workItemId}?api-version=7.0`,
|
|
51
|
+
patch
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Post comment about status change to ADO work item
|
|
56
|
+
*
|
|
57
|
+
* @param workItemId - ADO work item ID (e.g., 123)
|
|
58
|
+
* @param oldStatus - Previous SpecWeave status
|
|
59
|
+
* @param newStatus - New SpecWeave status
|
|
60
|
+
*/
|
|
61
|
+
async postStatusComment(workItemId, oldStatus, newStatus) {
|
|
62
|
+
const text = `\u{1F504} Status Update
|
|
63
|
+
|
|
64
|
+
SpecWeave status changed:
|
|
65
|
+
\u2022 From: ${oldStatus}
|
|
66
|
+
\u2022 To: ${newStatus}
|
|
67
|
+
\u2022 When: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
68
|
+
|
|
69
|
+
Synced from SpecWeave`;
|
|
70
|
+
await this.client.post(
|
|
71
|
+
`/wit/workitems/${workItemId}/comments?api-version=7.0-preview.3`,
|
|
72
|
+
{
|
|
73
|
+
text
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
AdoStatusSync
|
|
80
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure DevOps Status Sync
|
|
3
|
+
*
|
|
4
|
+
* Synchronizes SpecWeave increment statuses with ADO work item states.
|
|
5
|
+
*
|
|
6
|
+
* ADO Work Item State Updates:
|
|
7
|
+
* - Uses JSON Patch format for updates
|
|
8
|
+
* - System.State field controls work item state
|
|
9
|
+
* - Available states: New, Active, On Hold, Resolved, Closed, Removed
|
|
10
|
+
*
|
|
11
|
+
* @module ado-status-sync
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import axios, { AxiosInstance } from 'axios';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* External status representation (ADO-specific)
|
|
18
|
+
*/
|
|
19
|
+
export interface ExternalStatus {
|
|
20
|
+
state: string; // e.g., "New", "Active", "Closed"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Azure DevOps Status Sync
|
|
25
|
+
*
|
|
26
|
+
* Handles status synchronization with ADO work items.
|
|
27
|
+
*/
|
|
28
|
+
export class AdoStatusSync {
|
|
29
|
+
private client: AxiosInstance;
|
|
30
|
+
private organization: string;
|
|
31
|
+
private project: string;
|
|
32
|
+
|
|
33
|
+
constructor(
|
|
34
|
+
organization: string,
|
|
35
|
+
project: string,
|
|
36
|
+
personalAccessToken: string
|
|
37
|
+
) {
|
|
38
|
+
this.organization = organization;
|
|
39
|
+
this.project = project;
|
|
40
|
+
|
|
41
|
+
// Create ADO API client
|
|
42
|
+
this.client = axios.create({
|
|
43
|
+
baseURL: `https://dev.azure.com/${organization}/${project}/_apis`,
|
|
44
|
+
auth: {
|
|
45
|
+
username: '', // Empty for PAT auth
|
|
46
|
+
password: personalAccessToken
|
|
47
|
+
},
|
|
48
|
+
headers: {
|
|
49
|
+
'Content-Type': 'application/json-patch+json',
|
|
50
|
+
'Accept': 'application/json'
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get current status from ADO work item
|
|
57
|
+
*
|
|
58
|
+
* @param workItemId - ADO work item ID (e.g., 123)
|
|
59
|
+
* @returns Current work item state
|
|
60
|
+
*/
|
|
61
|
+
async getStatus(workItemId: number): Promise<ExternalStatus> {
|
|
62
|
+
const response = await this.client.get(
|
|
63
|
+
`/wit/workitems/${workItemId}?api-version=7.0`
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
state: response.data.fields['System.State']
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Update ADO work item state
|
|
73
|
+
*
|
|
74
|
+
* Uses JSON Patch format to update System.State field.
|
|
75
|
+
*
|
|
76
|
+
* @param workItemId - ADO work item ID (e.g., 123)
|
|
77
|
+
* @param status - Desired status
|
|
78
|
+
*/
|
|
79
|
+
async updateStatus(workItemId: number, status: ExternalStatus): Promise<void> {
|
|
80
|
+
// ADO uses JSON Patch format for updates
|
|
81
|
+
const patch = [
|
|
82
|
+
{
|
|
83
|
+
op: 'add',
|
|
84
|
+
path: '/fields/System.State',
|
|
85
|
+
value: status.state
|
|
86
|
+
}
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
await this.client.patch(
|
|
90
|
+
`/wit/workitems/${workItemId}?api-version=7.0`,
|
|
91
|
+
patch
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Post comment about status change to ADO work item
|
|
97
|
+
*
|
|
98
|
+
* @param workItemId - ADO work item ID (e.g., 123)
|
|
99
|
+
* @param oldStatus - Previous SpecWeave status
|
|
100
|
+
* @param newStatus - New SpecWeave status
|
|
101
|
+
*/
|
|
102
|
+
async postStatusComment(
|
|
103
|
+
workItemId: number,
|
|
104
|
+
oldStatus: string,
|
|
105
|
+
newStatus: string
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
const text = `🔄 Status Update\n\n` +
|
|
108
|
+
`SpecWeave status changed:\n` +
|
|
109
|
+
`• From: ${oldStatus}\n` +
|
|
110
|
+
`• To: ${newStatus}\n` +
|
|
111
|
+
`• When: ${new Date().toISOString()}\n\n` +
|
|
112
|
+
`Synced from SpecWeave`;
|
|
113
|
+
|
|
114
|
+
await this.client.post(
|
|
115
|
+
`/wit/workitems/${workItemId}/comments?api-version=7.0-preview.3`,
|
|
116
|
+
{
|
|
117
|
+
text
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { AdoClientV2 } from "./ado-client-v2.js";
|
|
2
|
+
import { EnhancedContentBuilder } from "../../../src/core/sync/enhanced-content-builder.js";
|
|
3
|
+
import { SpecIncrementMapper } from "../../../src/core/sync/spec-increment-mapper.js";
|
|
4
|
+
import { parseSpecContent } from "../../../src/core/spec-content-sync.js";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import fs from "fs/promises";
|
|
7
|
+
async function syncSpecToAdoWithEnhancedContent(options) {
|
|
8
|
+
const { specPath, organization, project, dryRun = false, verbose = false } = options;
|
|
9
|
+
try {
|
|
10
|
+
const baseSpec = await parseSpecContent(specPath);
|
|
11
|
+
if (!baseSpec) {
|
|
12
|
+
return {
|
|
13
|
+
success: false,
|
|
14
|
+
action: "error",
|
|
15
|
+
error: "Failed to parse spec content"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (verbose) {
|
|
19
|
+
console.log(`\u{1F4C4} Parsed spec: ${baseSpec.identifier.compact}`);
|
|
20
|
+
}
|
|
21
|
+
const specId = baseSpec.identifier.full || baseSpec.identifier.compact;
|
|
22
|
+
const rootDir = await findSpecWeaveRoot(specPath);
|
|
23
|
+
const mapper = new SpecIncrementMapper(rootDir);
|
|
24
|
+
const mapping = await mapper.mapSpecToIncrements(specId);
|
|
25
|
+
if (verbose) {
|
|
26
|
+
console.log(`\u{1F517} Found ${mapping.increments.length} related increments`);
|
|
27
|
+
}
|
|
28
|
+
const taskMapping = buildTaskMapping(mapping.increments, organization, project);
|
|
29
|
+
const architectureDocs = await findArchitectureDocs(rootDir, specId);
|
|
30
|
+
const enhancedSpec = {
|
|
31
|
+
...baseSpec,
|
|
32
|
+
summary: baseSpec.description,
|
|
33
|
+
taskMapping,
|
|
34
|
+
architectureDocs
|
|
35
|
+
};
|
|
36
|
+
const builder = new EnhancedContentBuilder();
|
|
37
|
+
const description = builder.buildExternalDescription(enhancedSpec);
|
|
38
|
+
if (verbose) {
|
|
39
|
+
console.log(`\u{1F4DD} Generated description: ${description.length} characters`);
|
|
40
|
+
}
|
|
41
|
+
if (dryRun) {
|
|
42
|
+
console.log("\u{1F50D} DRY RUN - Would create/update feature with:");
|
|
43
|
+
console.log(` Title: ${baseSpec.title}`);
|
|
44
|
+
console.log(` Description length: ${description.length}`);
|
|
45
|
+
return {
|
|
46
|
+
success: true,
|
|
47
|
+
action: "no-change",
|
|
48
|
+
tasksLinked: taskMapping?.tasks.length || 0
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (!organization || !project) {
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
action: "error",
|
|
55
|
+
error: "Azure DevOps organization/project not specified"
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const profile = {
|
|
59
|
+
provider: "ado",
|
|
60
|
+
displayName: `${organization}/${project}`,
|
|
61
|
+
config: {
|
|
62
|
+
organization,
|
|
63
|
+
project
|
|
64
|
+
},
|
|
65
|
+
timeRange: { default: "1M", max: "6M" }
|
|
66
|
+
};
|
|
67
|
+
const pat = process.env.AZURE_DEVOPS_PAT || "";
|
|
68
|
+
const client = new AdoClientV2(profile, pat);
|
|
69
|
+
const existingFeature = await findExistingFeature(client, baseSpec.identifier.compact);
|
|
70
|
+
let result;
|
|
71
|
+
if (existingFeature) {
|
|
72
|
+
await client.updateWorkItem(existingFeature.id, {
|
|
73
|
+
title: `[${baseSpec.identifier.compact}] ${baseSpec.title}`,
|
|
74
|
+
description
|
|
75
|
+
});
|
|
76
|
+
result = {
|
|
77
|
+
success: true,
|
|
78
|
+
action: "updated",
|
|
79
|
+
featureId: existingFeature.id,
|
|
80
|
+
featureUrl: `https://dev.azure.com/${organization}/${project}/_workitems/edit/${existingFeature.id}`,
|
|
81
|
+
tasksLinked: taskMapping?.tasks.length || 0
|
|
82
|
+
};
|
|
83
|
+
} else {
|
|
84
|
+
const feature = await client.createEpic({
|
|
85
|
+
title: `[${baseSpec.identifier.compact}] ${baseSpec.title}`,
|
|
86
|
+
description,
|
|
87
|
+
tags: ["spec", "external-tool-sync"]
|
|
88
|
+
});
|
|
89
|
+
result = {
|
|
90
|
+
success: true,
|
|
91
|
+
action: "created",
|
|
92
|
+
featureId: feature.id,
|
|
93
|
+
featureUrl: `https://dev.azure.com/${organization}/${project}/_workitems/edit/${feature.id}`,
|
|
94
|
+
tasksLinked: taskMapping?.tasks.length || 0
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (verbose) {
|
|
98
|
+
console.log(`\u2705 ${result.action === "created" ? "Created" : "Updated"} feature #${result.featureId}`);
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return {
|
|
103
|
+
success: false,
|
|
104
|
+
action: "error",
|
|
105
|
+
error: error.message
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function findSpecWeaveRoot(specPath) {
|
|
110
|
+
let currentDir = path.dirname(specPath);
|
|
111
|
+
while (true) {
|
|
112
|
+
const specweaveDir = path.join(currentDir, ".specweave");
|
|
113
|
+
try {
|
|
114
|
+
await fs.access(specweaveDir);
|
|
115
|
+
return currentDir;
|
|
116
|
+
} catch {
|
|
117
|
+
const parentDir = path.dirname(currentDir);
|
|
118
|
+
if (parentDir === currentDir) {
|
|
119
|
+
throw new Error(".specweave directory not found");
|
|
120
|
+
}
|
|
121
|
+
currentDir = parentDir;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function buildTaskMapping(increments, organization, project) {
|
|
126
|
+
if (increments.length === 0) return void 0;
|
|
127
|
+
const firstIncrement = increments[0];
|
|
128
|
+
const tasks = firstIncrement.tasks.map((task) => ({
|
|
129
|
+
id: task.id,
|
|
130
|
+
title: task.title,
|
|
131
|
+
userStories: task.userStories
|
|
132
|
+
}));
|
|
133
|
+
return {
|
|
134
|
+
incrementId: firstIncrement.id,
|
|
135
|
+
tasks,
|
|
136
|
+
tasksUrl: `https://dev.azure.com/${organization}/${project}/_git/repo?path=/.specweave/increments/${firstIncrement.id}/tasks.md`
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
async function findArchitectureDocs(rootDir, specId) {
|
|
140
|
+
const docs = [];
|
|
141
|
+
const archDir = path.join(rootDir, ".specweave/docs/internal/architecture");
|
|
142
|
+
try {
|
|
143
|
+
const adrDir = path.join(archDir, "adr");
|
|
144
|
+
try {
|
|
145
|
+
const adrs = await fs.readdir(adrDir);
|
|
146
|
+
const relatedAdrs = adrs.filter((file) => file.includes(specId.replace("spec-", "")));
|
|
147
|
+
for (const adr of relatedAdrs) {
|
|
148
|
+
docs.push({
|
|
149
|
+
type: "adr",
|
|
150
|
+
path: path.join(adrDir, adr),
|
|
151
|
+
title: adr.replace(".md", "").replace(/-/g, " ")
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
return docs;
|
|
159
|
+
}
|
|
160
|
+
async function findExistingFeature(client, specId) {
|
|
161
|
+
try {
|
|
162
|
+
const features = await client.queryWorkItems(`[System.Title] Contains '[${specId}]' AND [System.WorkItemType] = 'Feature'`);
|
|
163
|
+
return features[0] || null;
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export {
|
|
169
|
+
syncSpecToAdoWithEnhancedContent
|
|
170
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: specweave-github:cleanup-duplicates
|
|
3
|
+
description: Clean up duplicate GitHub issues for an Epic. Finds issues with duplicate titles and closes all except the first created issue.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Clean Up Duplicate GitHub Issues
|
|
7
|
+
|
|
8
|
+
**CRITICAL**: This command detects and closes duplicate GitHub issues created by multiple syncs.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
/specweave-github:cleanup-duplicates <epic-id> [--dry-run]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## What It Does
|
|
17
|
+
|
|
18
|
+
**Duplicate Detection & Cleanup**:
|
|
19
|
+
|
|
20
|
+
1. **Find all issues** for the Epic (searches by Epic ID in title)
|
|
21
|
+
2. **Group by title** (detect duplicates)
|
|
22
|
+
3. **For each duplicate group**:
|
|
23
|
+
- Keep the **FIRST created** issue (lowest number)
|
|
24
|
+
- Close all **LATER** issues with comment: "Duplicate of #XXX"
|
|
25
|
+
4. **Update Epic README** with correct issue numbers
|
|
26
|
+
|
|
27
|
+
## Examples
|
|
28
|
+
|
|
29
|
+
### Dry Run (No Changes)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
/specweave-github:cleanup-duplicates FS-031 --dry-run
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Output**:
|
|
36
|
+
```
|
|
37
|
+
🔍 Scanning for duplicates in Epic FS-031...
|
|
38
|
+
Found 25 total issues
|
|
39
|
+
Detected 10 duplicate groups:
|
|
40
|
+
|
|
41
|
+
📋 Group 1: "[FS-031] External Tool Status Synchronization"
|
|
42
|
+
- #250 (KEEP) - Created 2025-11-10
|
|
43
|
+
- #255 (CLOSE) - Created 2025-11-11 - DUPLICATE
|
|
44
|
+
- #260 (CLOSE) - Created 2025-11-12 - DUPLICATE
|
|
45
|
+
|
|
46
|
+
📋 Group 2: "[FS-031] Multi-Project GitHub Sync"
|
|
47
|
+
- #251 (KEEP) - Created 2025-11-10
|
|
48
|
+
- #256 (CLOSE) - Created 2025-11-11 - DUPLICATE
|
|
49
|
+
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
✅ Dry run complete!
|
|
53
|
+
Total issues: 25
|
|
54
|
+
Duplicate groups: 10
|
|
55
|
+
Issues to close: 15
|
|
56
|
+
|
|
57
|
+
⚠️ This was a DRY RUN - no changes made.
|
|
58
|
+
Run without --dry-run to actually close duplicates.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Actual Cleanup
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
/specweave-github:cleanup-duplicates FS-031
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Output**:
|
|
68
|
+
```
|
|
69
|
+
🔍 Scanning for duplicates in Epic FS-031...
|
|
70
|
+
Found 25 total issues
|
|
71
|
+
Detected 10 duplicate groups
|
|
72
|
+
|
|
73
|
+
⚠️ CONFIRM: Close 15 duplicate issues? [y/N]
|
|
74
|
+
> y
|
|
75
|
+
|
|
76
|
+
🗑️ Closing duplicates...
|
|
77
|
+
✅ Closed #255 (duplicate of #250)
|
|
78
|
+
✅ Closed #256 (duplicate of #251)
|
|
79
|
+
✅ Closed #260 (duplicate of #250)
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
📝 Updating Epic README frontmatter...
|
|
83
|
+
✅ Updated frontmatter with correct issue numbers
|
|
84
|
+
|
|
85
|
+
✅ Cleanup complete!
|
|
86
|
+
Closed: 15 duplicates
|
|
87
|
+
Kept: 10 original issues
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Arguments
|
|
91
|
+
|
|
92
|
+
- `<epic-id>` - Epic ID (e.g., `FS-031` or just `031`)
|
|
93
|
+
- `--dry-run` - Preview changes without actually closing issues (optional)
|
|
94
|
+
|
|
95
|
+
## Safety Features
|
|
96
|
+
|
|
97
|
+
✅ **Confirmation prompt**: Asks before closing issues (unless --dry-run)
|
|
98
|
+
✅ **Dry run mode**: Preview changes safely
|
|
99
|
+
✅ **Keeps oldest issue**: Preserves the first created issue
|
|
100
|
+
✅ **Adds closure comment**: Links to the original issue
|
|
101
|
+
✅ **Updates metadata**: Fixes Epic README frontmatter
|
|
102
|
+
|
|
103
|
+
## What Gets Closed
|
|
104
|
+
|
|
105
|
+
**Closed issues**:
|
|
106
|
+
- ✅ Duplicate titles (second, third, etc. occurrences)
|
|
107
|
+
- ✅ Closed with comment: "Duplicate of #XXX"
|
|
108
|
+
- ✅ Original issue kept open (or maintains its status)
|
|
109
|
+
|
|
110
|
+
**Example comment on closed duplicate**:
|
|
111
|
+
```markdown
|
|
112
|
+
Duplicate of #250
|
|
113
|
+
|
|
114
|
+
This issue was automatically closed by SpecWeave cleanup because it is a duplicate.
|
|
115
|
+
|
|
116
|
+
The original issue (#250) contains the same content and should be used for tracking instead.
|
|
117
|
+
|
|
118
|
+
🤖 Auto-closed by SpecWeave Duplicate Cleanup
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Requirements
|
|
122
|
+
|
|
123
|
+
1. **GitHub CLI** (`gh`) installed and authenticated
|
|
124
|
+
2. **Write access** to repository (for closing issues)
|
|
125
|
+
3. **Epic folder exists** at `.specweave/docs/internal/specs/FS-XXX-name/`
|
|
126
|
+
|
|
127
|
+
## When to Use
|
|
128
|
+
|
|
129
|
+
**Use this command when**:
|
|
130
|
+
- ✅ You see multiple issues with the same title in GitHub
|
|
131
|
+
- ✅ Epic sync ran multiple times and created duplicates
|
|
132
|
+
- ✅ Epic README frontmatter got corrupted and reset
|
|
133
|
+
- ✅ Post-sync validation warns about duplicates
|
|
134
|
+
|
|
135
|
+
**Example warning that triggers this**:
|
|
136
|
+
```
|
|
137
|
+
⚠️ WARNING: 10 duplicate(s) detected!
|
|
138
|
+
Run cleanup command to resolve:
|
|
139
|
+
/specweave-github:cleanup-duplicates FS-031
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Troubleshooting
|
|
143
|
+
|
|
144
|
+
**"No duplicates found"**:
|
|
145
|
+
- Good! Your Epic has no duplicate issues
|
|
146
|
+
- Run epic sync is working correctly with duplicate detection
|
|
147
|
+
|
|
148
|
+
**"GitHub CLI not authenticated"**:
|
|
149
|
+
- Run: `gh auth login`
|
|
150
|
+
- Ensure you have write access to the repository
|
|
151
|
+
|
|
152
|
+
**"Could not find Epic folder"**:
|
|
153
|
+
- Check Epic exists: `ls .specweave/docs/internal/specs/`
|
|
154
|
+
- Verify Epic ID format: `FS-031-epic-name/`
|
|
155
|
+
|
|
156
|
+
**"Error closing issue"**:
|
|
157
|
+
- Check GitHub CLI: `gh auth status`
|
|
158
|
+
- Verify write permissions: `gh repo view`
|
|
159
|
+
|
|
160
|
+
## Architecture
|
|
161
|
+
|
|
162
|
+
**Duplicate Detection Logic**:
|
|
163
|
+
1. Group issues by **exact title match**
|
|
164
|
+
2. Within each group, sort by **issue number** (ascending)
|
|
165
|
+
3. Keep **first issue** (lowest number = oldest)
|
|
166
|
+
4. Close **all others** as duplicates
|
|
167
|
+
|
|
168
|
+
**Why lowest number?**:
|
|
169
|
+
- Lower issue numbers were created first
|
|
170
|
+
- Preserves chronological order
|
|
171
|
+
- Maintains links from old documentation
|
|
172
|
+
|
|
173
|
+
## Related Commands
|
|
174
|
+
|
|
175
|
+
- `/specweave-github:sync-epic` - Sync Epic (now with duplicate detection!)
|
|
176
|
+
- `/specweave:validate` - Validate increment completeness
|
|
177
|
+
- `gh issue list` - View all issues (GitHub CLI)
|
|
178
|
+
|
|
179
|
+
## Implementation
|
|
180
|
+
|
|
181
|
+
**File**: `plugins/specweave-github/lib/github-epic-sync.ts`
|
|
182
|
+
|
|
183
|
+
**Method**: `cleanupDuplicates(epicId: string, dryRun: boolean)`
|
|
184
|
+
|
|
185
|
+
**Algorithm**:
|
|
186
|
+
1. Search GitHub for all issues with Epic ID
|
|
187
|
+
2. Group by title (Map<string, number[]>)
|
|
188
|
+
3. Filter groups with >1 issue (duplicates)
|
|
189
|
+
4. For each duplicate group:
|
|
190
|
+
- Keep first issue (lowest number)
|
|
191
|
+
- Close others with gh CLI
|
|
192
|
+
5. Update Epic README frontmatter
|
|
193
|
+
|
|
194
|
+
## Next Steps
|
|
195
|
+
|
|
196
|
+
After cleanup:
|
|
197
|
+
|
|
198
|
+
1. **Verify cleanup**: `gh issue list --search "[FS-031]"`
|
|
199
|
+
2. **Check Epic README**: Verify frontmatter has correct issue numbers
|
|
200
|
+
3. **Re-run sync**: `/specweave-github:sync-epic FS-031` (should show no duplicates)
|
|
201
|
+
4. **Enable duplicate detection**: Already enabled in v0.18.0+
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
**✅ SAFE TO USE**: This command is idempotent and safe to run multiple times.
|