specweave 0.32.2 → 0.32.5
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 +51 -9
- package/bin/specweave.js +34 -0
- package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.d.ts +100 -0
- package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.d.ts.map +1 -0
- package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.js +291 -0
- package/dist/plugins/specweave-ado/lib/ado-duplicate-detector.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.d.ts +103 -0
- package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.js +310 -0
- package/dist/plugins/specweave-jira/lib/jira-duplicate-detector.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-permission-gate.d.ts +126 -0
- package/dist/plugins/specweave-jira/lib/jira-permission-gate.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-permission-gate.js +207 -0
- package/dist/plugins/specweave-jira/lib/jira-permission-gate.js.map +1 -0
- package/dist/src/adapters/codex/README.md +1 -1
- package/dist/src/adapters/codex/adapter.js +1 -1
- package/dist/src/cli/commands/archive.d.ts +2 -0
- package/dist/src/cli/commands/archive.d.ts.map +1 -1
- package/dist/src/cli/commands/archive.js +33 -0
- package/dist/src/cli/commands/archive.js.map +1 -1
- package/dist/src/cli/commands/context.d.ts +92 -0
- package/dist/src/cli/commands/context.d.ts.map +1 -0
- package/dist/src/cli/commands/context.js +205 -0
- package/dist/src/cli/commands/context.js.map +1 -0
- package/dist/src/cli/commands/init-multiproject.js +1 -1
- package/dist/src/cli/commands/init-multiproject.js.map +1 -1
- package/dist/src/cli/commands/init.d.ts.map +1 -1
- package/dist/src/cli/commands/init.js +111 -69
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/migrate-to-multiproject.js +2 -2
- package/dist/src/cli/commands/migrate-to-multiproject.js.map +1 -1
- package/dist/src/cli/helpers/init/external-import.d.ts +3 -0
- package/dist/src/cli/helpers/init/external-import.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/external-import.js +17 -4
- package/dist/src/cli/helpers/init/external-import.js.map +1 -1
- package/dist/src/cli/helpers/init/index.d.ts +1 -0
- package/dist/src/cli/helpers/init/index.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/index.js +2 -0
- package/dist/src/cli/helpers/init/index.js.map +1 -1
- package/dist/src/cli/helpers/init/jira-ado-auto-detect.d.ts +70 -0
- package/dist/src/cli/helpers/init/jira-ado-auto-detect.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/jira-ado-auto-detect.js +214 -4
- package/dist/src/cli/helpers/init/jira-ado-auto-detect.js.map +1 -1
- package/dist/src/cli/helpers/init/living-docs-preflight.d.ts +4 -0
- package/dist/src/cli/helpers/init/living-docs-preflight.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/living-docs-preflight.js +34 -3
- package/dist/src/cli/helpers/init/living-docs-preflight.js.map +1 -1
- package/dist/src/cli/helpers/init/testing-config.d.ts +3 -0
- package/dist/src/cli/helpers/init/testing-config.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/testing-config.js +9 -2
- package/dist/src/cli/helpers/init/testing-config.js.map +1 -1
- package/dist/src/cli/helpers/init/translation-config.d.ts +3 -0
- package/dist/src/cli/helpers/init/translation-config.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/translation-config.js +21 -4
- package/dist/src/cli/helpers/init/translation-config.js.map +1 -1
- package/dist/src/cli/helpers/init/wizard-navigation.d.ts +45 -0
- package/dist/src/cli/helpers/init/wizard-navigation.d.ts.map +1 -0
- package/dist/src/cli/helpers/init/wizard-navigation.js +97 -0
- package/dist/src/cli/helpers/init/wizard-navigation.js.map +1 -0
- package/dist/src/core/increment/increment-archiver.d.ts +25 -4
- package/dist/src/core/increment/increment-archiver.d.ts.map +1 -1
- package/dist/src/core/increment/increment-archiver.js +64 -20
- package/dist/src/core/increment/increment-archiver.js.map +1 -1
- package/dist/src/core/increment/increment-utils.d.ts +65 -0
- package/dist/src/core/increment/increment-utils.d.ts.map +1 -1
- package/dist/src/core/increment/increment-utils.js +114 -0
- package/dist/src/core/increment/increment-utils.js.map +1 -1
- package/dist/src/core/living-docs/cross-project-sync.d.ts +97 -0
- package/dist/src/core/living-docs/cross-project-sync.d.ts.map +1 -0
- package/dist/src/core/living-docs/cross-project-sync.js +135 -0
- package/dist/src/core/living-docs/cross-project-sync.js.map +1 -0
- package/dist/src/core/living-docs/external-sync-orchestrator.d.ts +106 -0
- package/dist/src/core/living-docs/external-sync-orchestrator.d.ts.map +1 -0
- package/dist/src/core/living-docs/external-sync-orchestrator.js +146 -0
- package/dist/src/core/living-docs/external-sync-orchestrator.js.map +1 -0
- package/dist/src/core/living-docs/feature-archiver.d.ts +4 -0
- package/dist/src/core/living-docs/feature-archiver.d.ts.map +1 -1
- package/dist/src/core/living-docs/feature-archiver.js +32 -10
- package/dist/src/core/living-docs/feature-archiver.js.map +1 -1
- package/dist/src/core/living-docs/feature-id-manager.d.ts.map +1 -1
- package/dist/src/core/living-docs/feature-id-manager.js +7 -3
- package/dist/src/core/living-docs/feature-id-manager.js.map +1 -1
- package/dist/src/core/living-docs/governance/ecosystem-detector.d.ts +38 -0
- package/dist/src/core/living-docs/governance/ecosystem-detector.d.ts.map +1 -0
- package/dist/src/core/living-docs/governance/ecosystem-detector.js +325 -0
- package/dist/src/core/living-docs/governance/ecosystem-detector.js.map +1 -0
- package/dist/src/core/living-docs/governance/frontend-standards-parser.d.ts +74 -0
- package/dist/src/core/living-docs/governance/frontend-standards-parser.d.ts.map +1 -0
- package/dist/src/core/living-docs/governance/frontend-standards-parser.js +366 -0
- package/dist/src/core/living-docs/governance/frontend-standards-parser.js.map +1 -0
- package/dist/src/core/living-docs/governance/go-standards-parser.d.ts +64 -0
- package/dist/src/core/living-docs/governance/go-standards-parser.d.ts.map +1 -0
- package/dist/src/core/living-docs/governance/go-standards-parser.js +229 -0
- package/dist/src/core/living-docs/governance/go-standards-parser.js.map +1 -0
- package/dist/src/core/living-docs/governance/index.d.ts +50 -0
- package/dist/src/core/living-docs/governance/index.d.ts.map +1 -0
- package/dist/src/core/living-docs/governance/index.js +56 -0
- package/dist/src/core/living-docs/governance/index.js.map +1 -0
- package/dist/src/core/living-docs/governance/java-standards-parser.d.ts +89 -0
- package/dist/src/core/living-docs/governance/java-standards-parser.d.ts.map +1 -0
- package/dist/src/core/living-docs/governance/java-standards-parser.js +356 -0
- package/dist/src/core/living-docs/governance/java-standards-parser.js.map +1 -0
- package/dist/src/core/living-docs/governance/python-standards-parser.d.ts +83 -0
- package/dist/src/core/living-docs/governance/python-standards-parser.d.ts.map +1 -0
- package/dist/src/core/living-docs/governance/python-standards-parser.js +347 -0
- package/dist/src/core/living-docs/governance/python-standards-parser.js.map +1 -0
- package/dist/src/core/living-docs/governance/standards-generator.d.ts +38 -0
- package/dist/src/core/living-docs/governance/standards-generator.d.ts.map +1 -0
- package/dist/src/core/living-docs/governance/standards-generator.js +476 -0
- package/dist/src/core/living-docs/governance/standards-generator.js.map +1 -0
- package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.d.ts.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.js +54 -2
- package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.js.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.d.ts +5 -1
- package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.d.ts.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js +358 -30
- package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js.map +1 -1
- package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts +44 -0
- package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.d.ts +7 -3
- package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.js +94 -10
- package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
- package/dist/src/core/living-docs/module-analyzer.d.ts +22 -0
- package/dist/src/core/living-docs/module-analyzer.d.ts.map +1 -1
- package/dist/src/core/living-docs/module-analyzer.js +123 -19
- package/dist/src/core/living-docs/module-analyzer.js.map +1 -1
- package/dist/src/core/living-docs/sync-helpers/generators.d.ts +8 -1
- package/dist/src/core/living-docs/sync-helpers/generators.d.ts.map +1 -1
- package/dist/src/core/living-docs/sync-helpers/generators.js +18 -1
- package/dist/src/core/living-docs/sync-helpers/generators.js.map +1 -1
- package/dist/src/core/living-docs/sync-helpers/index.d.ts +1 -1
- package/dist/src/core/living-docs/sync-helpers/index.d.ts.map +1 -1
- package/dist/src/core/living-docs/sync-helpers/index.js.map +1 -1
- package/dist/src/core/living-docs/sync-helpers/parsers.d.ts +3 -1
- package/dist/src/core/living-docs/sync-helpers/parsers.d.ts.map +1 -1
- package/dist/src/core/living-docs/sync-helpers/parsers.js +24 -2
- package/dist/src/core/living-docs/sync-helpers/parsers.js.map +1 -1
- package/dist/src/core/living-docs/types.d.ts +6 -0
- package/dist/src/core/living-docs/types.d.ts.map +1 -1
- package/dist/src/core/living-docs/validators/index.d.ts +7 -0
- package/dist/src/core/living-docs/validators/index.d.ts.map +1 -0
- package/dist/src/core/living-docs/validators/index.js +7 -0
- package/dist/src/core/living-docs/validators/index.js.map +1 -0
- package/dist/src/core/living-docs/validators/project-validator.d.ts +92 -0
- package/dist/src/core/living-docs/validators/project-validator.d.ts.map +1 -0
- package/dist/src/core/living-docs/validators/project-validator.js +142 -0
- package/dist/src/core/living-docs/validators/project-validator.js.map +1 -0
- package/dist/src/core/llm/provider-factory.js +2 -2
- package/dist/src/core/llm/provider-factory.js.map +1 -1
- package/dist/src/core/llm/providers/anthropic-provider.js +1 -1
- package/dist/src/core/llm/providers/bedrock-provider.d.ts.map +1 -1
- package/dist/src/core/llm/providers/bedrock-provider.js +8 -4
- package/dist/src/core/llm/providers/bedrock-provider.js.map +1 -1
- package/dist/src/core/project/project-manager.d.ts.map +1 -1
- package/dist/src/core/project/project-manager.js +19 -17
- package/dist/src/core/project/project-manager.js.map +1 -1
- package/dist/src/core/types/config.d.ts +4 -2
- package/dist/src/core/types/config.d.ts.map +1 -1
- package/dist/src/core/types/config.js.map +1 -1
- package/dist/src/core/types/increment-metadata.d.ts +34 -0
- package/dist/src/core/types/increment-metadata.d.ts.map +1 -1
- package/dist/src/importers/jira-importer.d.ts +14 -0
- package/dist/src/importers/jira-importer.d.ts.map +1 -1
- package/dist/src/importers/jira-importer.js +75 -0
- package/dist/src/importers/jira-importer.js.map +1 -1
- package/dist/src/integrations/jira/jira-token-provider.d.ts +93 -0
- package/dist/src/integrations/jira/jira-token-provider.d.ts.map +1 -0
- package/dist/src/integrations/jira/jira-token-provider.js +160 -0
- package/dist/src/integrations/jira/jira-token-provider.js.map +1 -0
- package/dist/src/sync/ado-reconciler.d.ts +92 -0
- package/dist/src/sync/ado-reconciler.d.ts.map +1 -0
- package/dist/src/sync/ado-reconciler.js +335 -0
- package/dist/src/sync/ado-reconciler.js.map +1 -0
- package/dist/src/sync/jira-reconciler.d.ts +106 -0
- package/dist/src/sync/jira-reconciler.d.ts.map +1 -0
- package/dist/src/sync/jira-reconciler.js +405 -0
- package/dist/src/sync/jira-reconciler.js.map +1 -0
- package/dist/src/types/model-selection.d.ts +6 -4
- package/dist/src/types/model-selection.d.ts.map +1 -1
- package/dist/src/types/model-selection.js +3 -1
- package/dist/src/types/model-selection.js.map +1 -1
- package/dist/src/utils/cross-cutting-detector.d.ts +66 -0
- package/dist/src/utils/cross-cutting-detector.d.ts.map +1 -0
- package/dist/src/utils/cross-cutting-detector.js +179 -0
- package/dist/src/utils/cross-cutting-detector.js.map +1 -0
- package/dist/src/utils/external-tool-drift-detector.d.ts +1 -1
- package/dist/src/utils/external-tool-drift-detector.d.ts.map +1 -1
- package/dist/src/utils/external-tool-drift-detector.js +5 -4
- package/dist/src/utils/external-tool-drift-detector.js.map +1 -1
- package/dist/src/utils/feature-id-derivation.d.ts +8 -3
- package/dist/src/utils/feature-id-derivation.d.ts.map +1 -1
- package/dist/src/utils/feature-id-derivation.js +14 -6
- package/dist/src/utils/feature-id-derivation.js.map +1 -1
- package/dist/src/utils/model-selection.d.ts +3 -4
- package/dist/src/utils/model-selection.d.ts.map +1 -1
- package/dist/src/utils/model-selection.js +3 -4
- package/dist/src/utils/model-selection.js.map +1 -1
- package/dist/src/utils/project-detection.d.ts +12 -8
- package/dist/src/utils/project-detection.d.ts.map +1 -1
- package/dist/src/utils/project-detection.js +13 -19
- package/dist/src/utils/project-detection.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/agents/code-standards-detective/AGENT.md +48 -0
- package/plugins/specweave/commands/specweave-costs.md +4 -4
- package/plugins/specweave/commands/specweave-do.md +9 -9
- package/plugins/specweave/commands/specweave-done.md +13 -0
- package/plugins/specweave/commands/specweave-status.md +64 -0
- package/plugins/specweave/commands/specweave-validate.md +27 -1
- package/plugins/specweave/hooks/hooks.json +11 -1
- package/plugins/specweave/hooks/spec-project-validator.sh +81 -25
- package/plugins/specweave/hooks/v2/guards/increment-duplicate-guard.sh +135 -0
- package/plugins/specweave/lib/vendor/core/types/increment-metadata.d.ts +34 -0
- package/plugins/specweave/scripts/read-costs.sh +3 -3
- package/plugins/specweave/skills/code-standards-analyzer/SKILL.md +58 -6
- package/plugins/specweave/skills/increment-planner/SKILL.md +135 -29
- package/plugins/specweave/skills/increment-planner/templates/spec-multi-project.md +4 -2
- package/plugins/specweave/skills/increment-planner/templates/spec-single-project.md +2 -1
- package/plugins/specweave/skills/increment-planner/templates/tasks-multi-project.md +1 -1
- package/plugins/specweave/skills/increment-planner/templates/tasks-single-project.md +1 -1
- package/plugins/specweave/skills/spec-generator/SKILL.md +78 -7
- package/plugins/specweave-ado/commands/cleanup-duplicates.md +212 -0
- package/plugins/specweave-ado/commands/reconcile.md +120 -0
- package/plugins/specweave-ado/lib/ado-duplicate-detector.js +279 -0
- package/plugins/specweave-ado/lib/ado-duplicate-detector.ts +407 -0
- package/plugins/specweave-github/agents/github-manager/AGENT.md +2 -2
- package/plugins/specweave-infrastructure/skills/hetzner-provisioner/README.md +1 -1
- package/plugins/specweave-jira/agents/jira-manager/AGENT.md +1 -1
- package/plugins/specweave-jira/agents/jira-multi-project-mapper/AGENT.md +530 -0
- package/plugins/specweave-jira/agents/jira-sync-judge/AGENT.md +438 -0
- package/plugins/specweave-jira/commands/cleanup-duplicates.md +219 -0
- package/plugins/specweave-jira/commands/close.md +297 -0
- package/plugins/specweave-jira/commands/create.md +198 -0
- package/plugins/specweave-jira/commands/reconcile.md +123 -0
- package/plugins/specweave-jira/commands/status.md +215 -0
- package/plugins/specweave-jira/lib/jira-duplicate-detector.js +296 -0
- package/plugins/specweave-jira/lib/jira-duplicate-detector.ts +434 -0
- package/plugins/specweave-jira/lib/jira-permission-gate.js +160 -0
- package/plugins/specweave-jira/lib/jira-permission-gate.ts +276 -0
- package/plugins/specweave-jira/lib/jira-profile-resolver.js +222 -0
- package/plugins/specweave-jira/lib/jira-profile-resolver.ts +427 -0
- package/plugins/specweave-jira/reference/jira-specweave-mapping.md +16 -11
- package/plugins/specweave-release/commands/specweave-release-npm.md +140 -14
- package/plugins/specweave/commands/specweave-switch-project.md +0 -168
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: specweave-ado:reconcile
|
|
3
|
+
description: Reconcile Azure DevOps work item states with increment statuses. Fixes drift by closing work items for completed increments and reactivating work items for resumed increments.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Azure DevOps Status Reconciliation
|
|
7
|
+
|
|
8
|
+
Scan all increments and fix any drift between local metadata.json status and ADO work item states.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
/specweave-ado:reconcile [options]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Options
|
|
17
|
+
|
|
18
|
+
- `--dry-run`: Preview changes without making them
|
|
19
|
+
- `--verbose`: Show detailed output for each work item checked
|
|
20
|
+
|
|
21
|
+
## What It Does
|
|
22
|
+
|
|
23
|
+
1. **Scans** all non-archived increments
|
|
24
|
+
2. **Compares** metadata.json status with ADO work item state
|
|
25
|
+
3. **Fixes** mismatches:
|
|
26
|
+
- `metadata=completed` + `ADO=Active` → **Close** the work item
|
|
27
|
+
- `metadata=in-progress` + `ADO=Closed` → **Reactivate** the work item
|
|
28
|
+
|
|
29
|
+
## When to Use
|
|
30
|
+
|
|
31
|
+
- After manual metadata.json edits
|
|
32
|
+
- After git pulls that changed increment statuses
|
|
33
|
+
- When you notice active work items for completed work
|
|
34
|
+
- As a periodic health check
|
|
35
|
+
|
|
36
|
+
## Implementation
|
|
37
|
+
|
|
38
|
+
Run the reconciliation using the AdoReconciler:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { AdoReconciler } from '../../../src/sync/ado-reconciler.js';
|
|
42
|
+
|
|
43
|
+
const reconciler = new AdoReconciler({
|
|
44
|
+
projectRoot: process.cwd(),
|
|
45
|
+
dryRun: args.includes('--dry-run'),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const result = await reconciler.reconcile();
|
|
49
|
+
|
|
50
|
+
// Report results
|
|
51
|
+
console.log(`\nReconciliation complete:`);
|
|
52
|
+
console.log(` Scanned: ${result.scanned} increments`);
|
|
53
|
+
console.log(` Fixed: ${result.closed} closed, ${result.reopened} reopened`);
|
|
54
|
+
if (result.errors.length > 0) {
|
|
55
|
+
console.log(` Errors: ${result.errors.length}`);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Example Output
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
📊 Scanning 5 increment(s) for ADO state drift...
|
|
63
|
+
|
|
64
|
+
✅ Work Item #1234 (0056-plugin-fix/US-001): State matches (Active)
|
|
65
|
+
❌ Work Item #1240 (0066-import-wizard/US-003): Active but should be CLOSED (status=completed)
|
|
66
|
+
✅ Closed work item #1240
|
|
67
|
+
❌ Work Item #1238 (0063-multi-repo/US-001): CLOSED but should be ACTIVE (status=in-progress)
|
|
68
|
+
✅ Reactivated work item #1238
|
|
69
|
+
|
|
70
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
71
|
+
📊 ADO RECONCILIATION SUMMARY
|
|
72
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
73
|
+
Increments scanned: 5
|
|
74
|
+
Mismatches found: 2
|
|
75
|
+
Work items closed: 1
|
|
76
|
+
Work items reopened: 1
|
|
77
|
+
Errors: 0
|
|
78
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Dry Run Mode
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
/specweave-ado:reconcile --dry-run
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Shows what would be changed without making any modifications:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
❌ Work Item #1240 (0066-import-wizard/US-003): Active but should be CLOSED
|
|
91
|
+
[DRY RUN] Would close work item #1240
|
|
92
|
+
|
|
93
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
94
|
+
📊 ADO RECONCILIATION SUMMARY
|
|
95
|
+
⚠️ DRY RUN - No changes were made
|
|
96
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Requirements
|
|
100
|
+
|
|
101
|
+
- Azure DevOps PAT configured (`AZURE_DEVOPS_PAT`)
|
|
102
|
+
- `sync.ado.enabled = true` in config.json
|
|
103
|
+
- `sync.settings.canUpdateExternalItems = true` in config.json
|
|
104
|
+
|
|
105
|
+
## ADO Status Mapping
|
|
106
|
+
|
|
107
|
+
| SpecWeave Status | Expected ADO State |
|
|
108
|
+
|-----------------|-------------------|
|
|
109
|
+
| `completed` | Closed, Done, Resolved |
|
|
110
|
+
| `abandoned` | Closed, Removed |
|
|
111
|
+
| `in-progress` | Active, In Progress |
|
|
112
|
+
| `active` | Active, New |
|
|
113
|
+
| `planning` | New |
|
|
114
|
+
|
|
115
|
+
## Related Commands
|
|
116
|
+
|
|
117
|
+
- `/specweave-ado:status`: View sync status for increments
|
|
118
|
+
- `/specweave-ado:sync`: Manual sync to ADO
|
|
119
|
+
- `/specweave:done`: Close increment (triggers auto-close)
|
|
120
|
+
- `/specweave:resume`: Resume increment (now triggers auto-reopen)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { consoleLogger } from "../../../src/utils/logger.js";
|
|
2
|
+
class AdoDuplicateDetector {
|
|
3
|
+
constructor(options = {}) {
|
|
4
|
+
this.org = options.org || process.env.AZURE_DEVOPS_ORG || "";
|
|
5
|
+
this.project = options.project || process.env.AZURE_DEVOPS_PROJECT || "";
|
|
6
|
+
this.pat = options.pat || process.env.AZURE_DEVOPS_PAT || process.env.AZURE_DEVOPS_TOKEN || "";
|
|
7
|
+
this.logger = options.logger || consoleLogger;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Phase 1: Check if work item exists before creating
|
|
11
|
+
*/
|
|
12
|
+
async checkBeforeCreate(titlePattern, incrementId) {
|
|
13
|
+
try {
|
|
14
|
+
const items = await this.searchWorkItems(titlePattern);
|
|
15
|
+
if (items.length > 0) {
|
|
16
|
+
return {
|
|
17
|
+
found: true,
|
|
18
|
+
existingItem: items[0],
|
|
19
|
+
count: items.length
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return { found: false, count: 0 };
|
|
23
|
+
} catch (error) {
|
|
24
|
+
this.logger.log(`\u26A0\uFE0F Detection check failed: ${error.message}`);
|
|
25
|
+
return { found: false, count: 0 };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Phase 2: Verify count after creation
|
|
30
|
+
*/
|
|
31
|
+
async verifyAfterCreate(titlePattern, expectedCount = 1) {
|
|
32
|
+
try {
|
|
33
|
+
const items = await this.searchWorkItems(titlePattern);
|
|
34
|
+
if (items.length > expectedCount) {
|
|
35
|
+
const sorted = items.sort(
|
|
36
|
+
(a, b) => a.id - b.id
|
|
37
|
+
// Sort by ID (lowest = oldest)
|
|
38
|
+
);
|
|
39
|
+
return {
|
|
40
|
+
success: false,
|
|
41
|
+
expectedCount,
|
|
42
|
+
actualCount: items.length,
|
|
43
|
+
duplicates: sorted.slice(expectedCount)
|
|
44
|
+
// All items after expected count
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
success: true,
|
|
49
|
+
expectedCount,
|
|
50
|
+
actualCount: items.length,
|
|
51
|
+
duplicates: []
|
|
52
|
+
};
|
|
53
|
+
} catch (error) {
|
|
54
|
+
this.logger.log(`\u26A0\uFE0F Verification check failed: ${error.message}`);
|
|
55
|
+
return {
|
|
56
|
+
success: true,
|
|
57
|
+
// Assume success on error
|
|
58
|
+
expectedCount,
|
|
59
|
+
actualCount: expectedCount,
|
|
60
|
+
duplicates: []
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Phase 3: Auto-close duplicates
|
|
66
|
+
*/
|
|
67
|
+
async closeDuplicates(duplicates, keepItemId) {
|
|
68
|
+
const result = {
|
|
69
|
+
closedCount: 0,
|
|
70
|
+
keptCount: 1,
|
|
71
|
+
errors: []
|
|
72
|
+
};
|
|
73
|
+
for (const item of duplicates) {
|
|
74
|
+
try {
|
|
75
|
+
await this.closeWorkItem(item.id, keepItemId);
|
|
76
|
+
result.closedCount++;
|
|
77
|
+
this.logger.log(` \u2705 Closed #${item.id} (duplicate of #${keepItemId})`);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
result.errors.push(`#${item.id}: ${error.message}`);
|
|
80
|
+
this.logger.log(` \u274C Failed to close #${item.id}: ${error.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Full cleanup: Find and close all duplicates for a feature
|
|
87
|
+
*/
|
|
88
|
+
async cleanupFeatureDuplicates(featureId, dryRun = false) {
|
|
89
|
+
const searchPattern = `[${featureId}]`;
|
|
90
|
+
const items = await this.searchWorkItems(searchPattern);
|
|
91
|
+
this.logger.log(`
|
|
92
|
+
\u{1F50D} Scanning for duplicates in Feature ${featureId}...`);
|
|
93
|
+
this.logger.log(` Found ${items.length} total work items`);
|
|
94
|
+
const groups = this.groupByTitle(items);
|
|
95
|
+
const duplicateGroups = groups.filter((g) => g.duplicates.length > 0);
|
|
96
|
+
if (duplicateGroups.length === 0) {
|
|
97
|
+
this.logger.log(` \u2705 No duplicates found!`);
|
|
98
|
+
return {
|
|
99
|
+
groups: [],
|
|
100
|
+
totalItems: items.length,
|
|
101
|
+
duplicateCount: 0,
|
|
102
|
+
closedCount: 0
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
this.logger.log(` Detected ${duplicateGroups.length} duplicate groups:
|
|
106
|
+
`);
|
|
107
|
+
for (let i = 0; i < duplicateGroups.length; i++) {
|
|
108
|
+
const group = duplicateGroups[i];
|
|
109
|
+
this.logger.log(` \u{1F4CB} Group ${i + 1}: "${group.title.substring(0, 50)}..."`);
|
|
110
|
+
this.logger.log(` - #${group.keepItem.id} (KEEP) - Created ${group.keepItem.createdDate.split("T")[0]}`);
|
|
111
|
+
for (const dup of group.duplicates) {
|
|
112
|
+
this.logger.log(` - #${dup.id} (CLOSE) - Created ${dup.createdDate.split("T")[0]} - DUPLICATE`);
|
|
113
|
+
}
|
|
114
|
+
this.logger.log("");
|
|
115
|
+
}
|
|
116
|
+
const totalDuplicates = duplicateGroups.reduce((sum, g) => sum + g.duplicates.length, 0);
|
|
117
|
+
if (dryRun) {
|
|
118
|
+
this.logger.log(`
|
|
119
|
+
\u2705 Dry run complete!`);
|
|
120
|
+
this.logger.log(` Total work items: ${items.length}`);
|
|
121
|
+
this.logger.log(` Duplicate groups: ${duplicateGroups.length}`);
|
|
122
|
+
this.logger.log(` Work items to close: ${totalDuplicates}`);
|
|
123
|
+
this.logger.log(`
|
|
124
|
+
\u26A0\uFE0F This was a DRY RUN - no changes made.`);
|
|
125
|
+
return {
|
|
126
|
+
groups: duplicateGroups,
|
|
127
|
+
totalItems: items.length,
|
|
128
|
+
duplicateCount: totalDuplicates,
|
|
129
|
+
closedCount: 0
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
let closedCount = 0;
|
|
133
|
+
this.logger.log(`\u{1F5D1}\uFE0F Closing duplicates...`);
|
|
134
|
+
for (const group of duplicateGroups) {
|
|
135
|
+
const result = await this.closeDuplicates(group.duplicates, group.keepItem.id);
|
|
136
|
+
closedCount += result.closedCount;
|
|
137
|
+
}
|
|
138
|
+
this.logger.log(`
|
|
139
|
+
\u2705 Cleanup complete!`);
|
|
140
|
+
this.logger.log(` Closed: ${closedCount} duplicates`);
|
|
141
|
+
this.logger.log(` Kept: ${duplicateGroups.length} original work items`);
|
|
142
|
+
return {
|
|
143
|
+
groups: duplicateGroups,
|
|
144
|
+
totalItems: items.length,
|
|
145
|
+
duplicateCount: totalDuplicates,
|
|
146
|
+
closedCount
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Group work items by title
|
|
151
|
+
*/
|
|
152
|
+
groupByTitle(items) {
|
|
153
|
+
const titleMap = /* @__PURE__ */ new Map();
|
|
154
|
+
for (const item of items) {
|
|
155
|
+
const existing = titleMap.get(item.title) || [];
|
|
156
|
+
existing.push(item);
|
|
157
|
+
titleMap.set(item.title, existing);
|
|
158
|
+
}
|
|
159
|
+
const groups = [];
|
|
160
|
+
for (const [title, groupItems] of titleMap) {
|
|
161
|
+
const sorted = groupItems.sort((a, b) => a.id - b.id);
|
|
162
|
+
groups.push({
|
|
163
|
+
title,
|
|
164
|
+
items: sorted,
|
|
165
|
+
keepItem: sorted[0],
|
|
166
|
+
duplicates: sorted.slice(1)
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return groups;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Search for work items using WIQL
|
|
173
|
+
*/
|
|
174
|
+
async searchWorkItems(titlePattern) {
|
|
175
|
+
if (!this.org || !this.pat) {
|
|
176
|
+
throw new Error("ADO credentials not configured");
|
|
177
|
+
}
|
|
178
|
+
const wiql = {
|
|
179
|
+
query: `SELECT [System.Id], [System.Title], [System.State], [System.CreatedDate]
|
|
180
|
+
FROM WorkItems
|
|
181
|
+
WHERE [System.TeamProject] = '${this.project}'
|
|
182
|
+
AND [System.Title] CONTAINS '${titlePattern}'
|
|
183
|
+
ORDER BY [System.Id] ASC`
|
|
184
|
+
};
|
|
185
|
+
const auth = Buffer.from(`:${this.pat}`).toString("base64");
|
|
186
|
+
const url = `https://dev.azure.com/${this.org}/${this.project}/_apis/wit/wiql?api-version=7.1`;
|
|
187
|
+
const response = await fetch(url, {
|
|
188
|
+
method: "POST",
|
|
189
|
+
headers: {
|
|
190
|
+
Authorization: `Basic ${auth}`,
|
|
191
|
+
"Content-Type": "application/json"
|
|
192
|
+
},
|
|
193
|
+
body: JSON.stringify(wiql)
|
|
194
|
+
});
|
|
195
|
+
if (!response.ok) {
|
|
196
|
+
throw new Error(`WIQL query failed: ${response.status}`);
|
|
197
|
+
}
|
|
198
|
+
const data = await response.json();
|
|
199
|
+
const workItemIds = data.workItems?.map((wi) => wi.id) || [];
|
|
200
|
+
if (workItemIds.length === 0) {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
return this.getWorkItemDetails(workItemIds);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Get work item details by IDs
|
|
207
|
+
*/
|
|
208
|
+
async getWorkItemDetails(ids) {
|
|
209
|
+
if (ids.length === 0) return [];
|
|
210
|
+
const auth = Buffer.from(`:${this.pat}`).toString("base64");
|
|
211
|
+
const url = `https://dev.azure.com/${this.org}/_apis/wit/workitems?ids=${ids.join(",")}&api-version=7.1`;
|
|
212
|
+
const response = await fetch(url, {
|
|
213
|
+
headers: {
|
|
214
|
+
Authorization: `Basic ${auth}`,
|
|
215
|
+
"Content-Type": "application/json"
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
throw new Error(`Failed to get work items: ${response.status}`);
|
|
220
|
+
}
|
|
221
|
+
const data = await response.json();
|
|
222
|
+
return (data.value || []).map((wi) => ({
|
|
223
|
+
id: wi.id,
|
|
224
|
+
title: wi.fields["System.Title"],
|
|
225
|
+
state: wi.fields["System.State"],
|
|
226
|
+
createdDate: wi.fields["System.CreatedDate"],
|
|
227
|
+
url: wi._links?.html?.href
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Close a work item with duplicate comment
|
|
232
|
+
*/
|
|
233
|
+
async closeWorkItem(workItemId, originalId) {
|
|
234
|
+
const auth = Buffer.from(`:${this.pat}`).toString("base64");
|
|
235
|
+
const url = `https://dev.azure.com/${this.org}/_apis/wit/workitems/${workItemId}?api-version=7.1`;
|
|
236
|
+
const comment = `## Duplicate of #${originalId}
|
|
237
|
+
|
|
238
|
+
This work item was automatically closed by SpecWeave cleanup because it is a duplicate.
|
|
239
|
+
|
|
240
|
+
The original work item (#${originalId}) contains the same content and should be used for tracking instead.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
\u{1F916} Auto-closed by SpecWeave Duplicate Cleanup`;
|
|
244
|
+
const operations = [
|
|
245
|
+
{
|
|
246
|
+
op: "add",
|
|
247
|
+
path: "/fields/System.State",
|
|
248
|
+
value: "Closed"
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
op: "add",
|
|
252
|
+
path: "/fields/System.History",
|
|
253
|
+
value: comment
|
|
254
|
+
}
|
|
255
|
+
];
|
|
256
|
+
const response = await fetch(url, {
|
|
257
|
+
method: "PATCH",
|
|
258
|
+
headers: {
|
|
259
|
+
Authorization: `Basic ${auth}`,
|
|
260
|
+
"Content-Type": "application/json-patch+json"
|
|
261
|
+
},
|
|
262
|
+
body: JSON.stringify(operations)
|
|
263
|
+
});
|
|
264
|
+
if (!response.ok) {
|
|
265
|
+
const error = await response.text();
|
|
266
|
+
throw new Error(`Failed to close work item: ${response.status} - ${error}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async function cleanupAdoDuplicates(featureId, dryRun = false) {
|
|
271
|
+
const detector = new AdoDuplicateDetector();
|
|
272
|
+
return detector.cleanupFeatureDuplicates(featureId, dryRun);
|
|
273
|
+
}
|
|
274
|
+
var ado_duplicate_detector_default = AdoDuplicateDetector;
|
|
275
|
+
export {
|
|
276
|
+
AdoDuplicateDetector,
|
|
277
|
+
cleanupAdoDuplicates,
|
|
278
|
+
ado_duplicate_detector_default as default
|
|
279
|
+
};
|