Automatically detect and resolve duplicate increments with smart conflict resolution
Automatically detects and resolves duplicate increments by keeping the best version and optionally merging valuable content. Use this when you have duplicate increment numbers across active/archive/backlog folders to clean up your workspace and free space.
/plugin marketplace add anton-abyzov/specweave/plugin install sw@specweaveIntelligent conflict resolution: Automatically detects duplicate increments and resolves conflicts by selecting the winner, merging valuable content, and cleaning up losers.
Data safety first - This command provides multiple safety layers:
--force flag# Detect and fix all duplicates interactively
/sw:fix-duplicates
# Preview what would be done (dry-run)
/sw:fix-duplicates --dry-run
# Auto-fix with content merging
/sw:fix-duplicates --merge
# Force fix without confirmation (use with caution!)
/sw:fix-duplicates --force --merge
# Fix specific increment number
/sw:fix-duplicates 0031
<increment-number>: Optional. Fix only duplicates of specific increment number (e.g., "31", "0031")--dry-run: Show what would be done without making changes--merge: Merge valuable content from losers to winner before deletion--force: Skip confirmation prompts (for automated scripts)--strategy <name>: Override default resolution strategy (advanced)Scans .specweave/increments/ and all special folders:
.specweave/increments/NNNN-*).specweave/increments/_archive/).specweave/increments/_abandoned/).specweave/increments/_backlog/)Detects duplicates by increment number (e.g., all folders starting with 0031-).
Automatic prioritization algorithm:
Status priority (highest wins):
Most recent activity (tiebreaker):
lastActivity from metadata.jsonCompleteness (second tiebreaker):
Location preference (final tiebreaker):
Example:
Increment 0031 exists in 3 locations:
1. active/0031-external-tool-sync (status: active, activity: 2025-11-14) → WINNER
2. archive/0031-external-tool-sync (status: completed, activity: 2025-11-12)
3. abandoned/0031-old-attempt (status: abandoned, activity: 2025-10-01)
Winner: Location 1 (active status + most recent activity)
When --merge flag is enabled:
Merge reports/ folder:
reports/ to winner's reports/REPORT.md → REPORT-{timestamp}.mdMerge metadata.json:
Create resolution report:
reports/DUPLICATE-RESOLUTION-{timestamp}.mdExample merge:
Winner: .specweave/increments/0031-external-tool-sync/
reports/
├─ IMPLEMENTATION-COMPLETE.md (original)
├─ SESSION-NOTES-2025-11-12.md (merged from archive)
└─ DUPLICATE-RESOLUTION-20251115-070000.md (NEW)
Loser 1: .specweave/increments/_archive/0031-external-tool-sync/
reports/
└─ SESSION-NOTES-2025-11-12.md → MERGED to winner
Loser 2: .specweave/increments/_abandoned/0031-old-attempt/
(No valuable content to merge)
Safety measures:
Delete {path}? This will permanently remove {N} files. [y/N]N (no deletion unless confirmed)--force flag skips prompts (for CI/automated scripts)/sw:fix-duplicates
Output:
🔍 Scanning for duplicate increments...
Found 2 duplicates:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Duplicate 1: Increment 0031-external-tool-status-sync
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Locations:
1. ✓ (recommended) active/0031-external-tool-status-sync
Status: active | Activity: 2025-11-14 | Files: 12
Reason: Active status + most recent
2. ✗ archive/0031-external-tool-sync
Status: completed | Activity: 2025-11-12 | Files: 10
Resolution Plan:
• Keep: active/0031-external-tool-status-sync
• Delete: archive/0031-external-tool-sync
Delete archive/0031-external-tool-sync? [y/N]: y
✅ Resolved duplicate 0031
• Kept: active/0031-external-tool-status-sync
• Deleted: archive/0031-external-tool-sync (10 files removed)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Summary
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Duplicates found: 2
Duplicates resolved: 2
Files deleted: 15
Space freed: 2.3 MB
📝 Resolution reports:
• active/0031-external-tool-status-sync/reports/DUPLICATE-RESOLUTION-20251115-070000.md
/sw:fix-duplicates --dry-run
Output:
🔍 DRY RUN - No files will be deleted
Found 2 duplicates:
Duplicate 1: Increment 0031
[DRY RUN] Would keep: active/0031-external-tool-status-sync
[DRY RUN] Would delete: archive/0031-external-tool-sync (10 files)
Duplicate 2: Increment 0032
[DRY RUN] Would keep: active/0032-increment-number-gap-prevention
[DRY RUN] Would delete: active/0032-prevent-increment-number-gaps (8 files)
Summary:
Would resolve: 2 duplicates
Would delete: 18 files
Would free: 2.5 MB
Run without --dry-run to proceed:
/sw:fix-duplicates --merge
/sw:fix-duplicates --merge --force
Output:
🔍 Scanning for duplicate increments...
Found 2 duplicates:
Duplicate 1: Increment 0031
✓ Winner: active/0031-external-tool-status-sync
📦 Merging content from losers...
• Merged: reports/SESSION-NOTES-2025-11-12.md
• Created: reports/DUPLICATE-RESOLUTION-20251115-070000.md
✓ Deleted: archive/0031-external-tool-sync
Duplicate 2: Increment 0032
✓ Winner: active/0032-increment-number-gap-prevention
⚠️ No content to merge (loser empty)
✓ Deleted: active/0032-prevent-increment-number-gaps
✅ All duplicates resolved!
Summary:
Resolved: 2 duplicates
Merged: 1 file
Deleted: 2 paths (18 files)
Space freed: 2.5 MB
/sw:fix-duplicates 0031
Output:
🔍 Scanning for duplicates of increment 0031...
Found 1 duplicate:
Duplicate: Increment 0031-external-tool-status-sync
Locations:
1. ✓ active/0031-external-tool-status-sync (recommended)
2. ✗ archive/0031-external-tool-sync
Delete archive/0031-external-tool-sync? [y/N]: y
✅ Duplicate 0031 resolved
• Kept: active/0031-external-tool-status-sync
• Deleted: archive/0031-external-tool-sync
🔍 Scanning for duplicate increments...
✅ No duplicates found!
All increments have unique numbers and locations.
Delete archive/0031-external-tool-sync? [y/N]: n
⏭️ Skipped deletion of archive/0031-external-tool-sync
Summary:
Duplicates found: 2
Duplicates resolved: 0
User declined: 2
💡 Tip: Use --force to skip confirmation prompts
❌ Error resolving duplicate 0031:
Failed to delete archive/0031-external-tool-sync
Reason: Permission denied
Recommendation:
1. Check file permissions
2. Close any programs using these files
3. Retry with sudo (if appropriate)
File: reports/DUPLICATE-RESOLUTION-{timestamp}.md
# Duplicate Resolution Report
**Increment**: 0031-external-tool-status-sync
**Resolved**: 2025-11-15 07:00:00 UTC
**Command**: /sw:fix-duplicates --merge
## Detected Duplicates
Increment 0031 existed in 2 locations:
1. active/0031-external-tool-status-sync (status: active, activity: 2025-11-14)
2. archive/0031-external-tool-sync (status: completed, activity: 2025-11-12)
## Winner Selection
**Winner**: active/0031-external-tool-status-sync
**Reason**: Active status (5 points) + Most recent activity (2025-11-14)
## Content Merged
Merged from archive/0031-external-tool-sync:
- reports/SESSION-NOTES-2025-11-12.md → reports/SESSION-NOTES-2025-11-12.md
- metadata.json (union of external links)
## Deleted Paths
- archive/0031-external-tool-sync (10 files, 1.2 MB)
## Metadata Union
**GitHub**:
- Issue #45 (from winner)
- Issue #42 (from archive) ← merged
**JIRA**:
- PROJ-123 (from winner)
## Summary
- Duplicates resolved: 1
- Files merged: 1
- Paths deleted: 1
- Space freed: 1.2 MB
- Total time: 0.5s
import { Task } from '@claude/types';
const task = new Task('fix-duplicates', 'Fix duplicate increments');
task.run(async () => {
const { detectAllDuplicates } = await import('../../../dist/src/core/increment/duplicate-detector.js');
const { resolveConflict } = await import('../../../dist/src/core/increment/conflict-resolver.js');
// Parse arguments
const args = process.argv.slice(2);
const incrementNumber = args.find(arg => /^\d+$/.test(arg));
// Parse options
const options = {
dryRun: args.includes('--dry-run'),
merge: args.includes('--merge'),
force: args.includes('--force'),
strategy: args.includes('--strategy') ? args[args.indexOf('--strategy') + 1] : undefined
};
// Detect duplicates
console.log('🔍 Scanning for duplicate increments...\n');
const report = await detectAllDuplicates(process.cwd());
// Filter by increment number if specified
let duplicates = report.duplicates;
if (incrementNumber) {
const paddedNumber = incrementNumber.padStart(4, '0');
duplicates = duplicates.filter(d => d.incrementNumber === paddedNumber);
}
if (duplicates.length === 0) {
console.log('✅ No duplicates found!\n');
console.log('All increments have unique numbers and locations.');
return;
}
console.log(`Found ${duplicates.length} duplicate${duplicates.length > 1 ? 's' : ''}:\n`);
// Display resolution plan
if (options.dryRun) {
console.log('🔍 DRY RUN - No files will be deleted\n');
}
// Resolve each duplicate
const results = [];
for (const duplicate of duplicates) {
console.log('━'.repeat(60));
console.log(`Duplicate: Increment ${duplicate.incrementNumber}`);
console.log('━'.repeat(60));
// Show locations
console.log('\nLocations:');
duplicate.locations.forEach((loc, index) => {
const isWinner = loc === duplicate.recommendedWinner;
const marker = isWinner ? '✓ (recommended)' : '✗';
console.log(` ${index + 1}. ${marker} ${path.basename(loc.path)}`);
console.log(` Status: ${loc.metadata?.status || 'unknown'} | Activity: ${loc.metadata?.lastActivity || 'unknown'} | Files: ${loc.fileCount}`);
});
// Show resolution reason
console.log(`\nResolution reason: ${duplicate.resolutionReason}`);
// Resolve conflict
try {
const result = await resolveConflict(duplicate, options);
results.push(result);
if (options.dryRun) {
console.log(`\n[DRY RUN] Would keep: ${path.basename(result.winner)}`);
console.log(`[DRY RUN] Would delete: ${result.deleted.map(p => path.basename(p)).join(', ')}`);
} else {
console.log(`\n✅ Resolved duplicate ${duplicate.incrementNumber}`);
console.log(` • Kept: ${path.basename(result.winner)}`);
console.log(` • Deleted: ${result.deleted.map(p => path.basename(p)).join(', ')}`);
if (result.merged.length > 0) {
console.log(` • Merged: ${result.merged.length} file(s)`);
}
}
} catch (error) {
console.error(`\n❌ Error resolving duplicate ${duplicate.incrementNumber}:`);
console.error(` ${error.message}`);
}
console.log();
}
// Summary
console.log('━'.repeat(60));
console.log('Summary');
console.log('━'.repeat(60));
const totalDeleted = results.reduce((sum, r) => sum + r.deleted.length, 0);
const totalMerged = results.reduce((sum, r) => sum + r.merged.length, 0);
if (options.dryRun) {
console.log(`\nWould resolve: ${results.length} duplicate${results.length > 1 ? 's' : ''}`);
console.log(`Would delete: ${totalDeleted} path${totalDeleted > 1 ? 's' : ''}`);
if (totalMerged > 0) {
console.log(`Would merge: ${totalMerged} file${totalMerged > 1 ? 's' : ''}`);
}
console.log('\nRun without --dry-run to proceed:');
console.log(' /sw:fix-duplicates --merge');
} else {
console.log(`\nDuplicates resolved: ${results.length}`);
console.log(`Paths deleted: ${totalDeleted}`);
if (totalMerged > 0) {
console.log(`Files merged: ${totalMerged}`);
}
console.log('\n📝 Resolution reports:');
results.forEach(r => {
console.log(` • ${r.reportPath}`);
});
}
});
export default task;
No configuration needed - all options are provided via command-line flags.
/sw:archive <id> - Archive specific increment (prevents duplicates)/sw:restore <id> - Restore increment from archive/sw:status - View all increment statuses--dry-run first to preview changes--merge flag to preserve reports and session notes--force skips confirmations - use only in automated scriptsAlways preserved (in winner):
--merge)Conditionally preserved (if --merge):
Always deleted:
Best Practice: Run with --dry-run --merge first to see what would happen, then run without --dry-run to apply changes.