specweave 1.0.318 → 1.0.320
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/sync/sync-coordinator.d.ts +4 -66
- package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
- package/dist/src/sync/sync-coordinator.js +9 -454
- package/dist/src/sync/sync-coordinator.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/hooks/v2/handlers/universal-auto-create-dispatcher.sh +51 -12
- package/plugins/specweave/lib/vendor/core/universal-auto-create.d.ts +64 -0
- package/plugins/specweave/lib/vendor/core/universal-auto-create.js +228 -0
- package/plugins/specweave/lib/vendor/core/universal-auto-create.js.map +1 -0
- package/plugins/specweave/lib/vendor/utils/feature-id-derivation.d.ts +63 -0
- package/plugins/specweave/lib/vendor/utils/feature-id-derivation.js +85 -0
- package/plugins/specweave/lib/vendor/utils/feature-id-derivation.js.map +1 -0
- package/plugins/specweave/skills/increment/SKILL.md +4 -0
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* High-level coordinator that integrates FormatPreservationSyncService
|
|
5
5
|
* with living docs sync workflow. Called by post-task-completion hook.
|
|
6
6
|
*/
|
|
7
|
-
import { GitHubIssue } from '../../plugins/specweave-github/lib/types.js';
|
|
8
7
|
import { Logger } from '../utils/logger.js';
|
|
9
8
|
import { ResolvedAdoProfile } from '../integrations/ado/ado-client-factory.js';
|
|
10
9
|
import { ClosureMetrics } from './closure-metrics.js';
|
|
@@ -48,36 +47,13 @@ export declare class SyncCoordinator {
|
|
|
48
47
|
*/
|
|
49
48
|
getFormattedClosureMetrics(): string;
|
|
50
49
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* This is the AUTOMATIC GitHub sync that runs when increment completes.
|
|
54
|
-
* Creates issues for User Stories that don't have GitHub issues yet.
|
|
55
|
-
*
|
|
56
|
-
* Uses 3-layer idempotency:
|
|
57
|
-
* - Layer 1: Check user story frontmatter (fastest, <1ms)
|
|
58
|
-
* - Layer 2: Check increment metadata.json (fast, <5ms)
|
|
59
|
-
* - Layer 3: Query GitHub API (slow but authoritative, 500-2000ms)
|
|
60
|
-
*
|
|
61
|
-
* @param config - Project configuration
|
|
62
|
-
* @returns Array of created issues
|
|
50
|
+
* @deprecated (0348) GitHub issue creation now handled by GitHubFeatureSync via LivingDocsSync chain.
|
|
63
51
|
*/
|
|
64
|
-
createGitHubIssuesForUserStories(
|
|
52
|
+
createGitHubIssuesForUserStories(_config: SpecWeaveConfig): Promise<any[]>;
|
|
65
53
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* This is the CRITICAL missing feature that was causing GitHub issues to remain
|
|
69
|
-
* open after increments were closed.
|
|
70
|
-
*
|
|
71
|
-
* Called by: syncIncrementClosure() when increment status changes to "completed"
|
|
72
|
-
*
|
|
73
|
-
* Flow:
|
|
74
|
-
* 1. Get feature ID from increment spec
|
|
75
|
-
* 2. Load all user stories for that feature
|
|
76
|
-
* 3. For each user story with a GitHub issue, close it with completion comment
|
|
77
|
-
*
|
|
78
|
-
* @param config - Project configuration
|
|
79
|
-
* @returns Array of closed issue numbers
|
|
54
|
+
* @deprecated (0348) GitHub closure now handled by GitHubFeatureSync via LivingDocsSync chain.
|
|
80
55
|
*/
|
|
56
|
+
closeGitHubIssuesForUserStories(_config: SpecWeaveConfig): Promise<number[]>;
|
|
81
57
|
/**
|
|
82
58
|
* Close JIRA issues for completed user stories
|
|
83
59
|
*
|
|
@@ -114,31 +90,6 @@ export declare class SyncCoordinator {
|
|
|
114
90
|
syncIncrementClosure(): Promise<SyncResult & {
|
|
115
91
|
closedIssues: number[];
|
|
116
92
|
}>;
|
|
117
|
-
/**
|
|
118
|
-
* Detect duplicate issues with different feature ID formats
|
|
119
|
-
*
|
|
120
|
-
* Searches for issues that match the user story but have a different feature ID format.
|
|
121
|
-
* This prevents creating duplicates like:
|
|
122
|
-
* - [FS-0128][US-001] (old bug format with leading zeros)
|
|
123
|
-
* - [FS-128][US-001] (correct format)
|
|
124
|
-
*
|
|
125
|
-
* The search uses a regex pattern that matches any FS-XXX format for the same US-XXX.
|
|
126
|
-
*
|
|
127
|
-
* @param client - GitHub client
|
|
128
|
-
* @param featureId - Current feature ID (e.g., "FS-128")
|
|
129
|
-
* @param userStoryId - User story ID (e.g., "US-001")
|
|
130
|
-
* @returns Duplicate info if found, null otherwise
|
|
131
|
-
*/
|
|
132
|
-
private detectDuplicateIssue;
|
|
133
|
-
/**
|
|
134
|
-
* Update issue if it has placeholder content
|
|
135
|
-
* Fetches issue from GitHub, checks for placeholder, and updates with rich content
|
|
136
|
-
*/
|
|
137
|
-
private updateIssueIfPlaceholder;
|
|
138
|
-
/**
|
|
139
|
-
* Format user story content for GitHub issue body
|
|
140
|
-
*/
|
|
141
|
-
private formatUserStoryBody;
|
|
142
93
|
/**
|
|
143
94
|
* Sync increment completion to external tools using format preservation
|
|
144
95
|
*/
|
|
@@ -182,18 +133,5 @@ export declare class SyncCoordinator {
|
|
|
182
133
|
* Emoji checkmarks work in both plain text and ADF contexts.
|
|
183
134
|
*/
|
|
184
135
|
private formatJiraCompletionComment;
|
|
185
|
-
/**
|
|
186
|
-
* Sync AC checkbox status to GitHub issues
|
|
187
|
-
*
|
|
188
|
-
* @deprecated (0348) Delegates to GitHubACCheckboxSync in the GitHub plugin.
|
|
189
|
-
* Kept for backward compatibility with callers that haven't migrated yet.
|
|
190
|
-
*/
|
|
191
|
-
syncACCheckboxesToGitHub(config: SpecWeaveConfig, options?: {
|
|
192
|
-
addComment?: boolean;
|
|
193
|
-
}): Promise<{
|
|
194
|
-
success: boolean;
|
|
195
|
-
updated: number;
|
|
196
|
-
issues: number[];
|
|
197
|
-
}>;
|
|
198
136
|
}
|
|
199
137
|
//# sourceMappingURL=sync-coordinator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-coordinator.d.ts","sourceRoot":"","sources":["../../../src/sync/sync-coordinator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"sync-coordinator.d.ts","sourceRoot":"","sources":["../../../src/sync/sync-coordinator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,OAAO,EAAE,MAAM,EAAiB,MAAM,oBAAoB,CAAC;AAI3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2CAA2C,CAAC;AAG/E,OAAO,EAAE,cAAc,EAAwB,MAAM,sBAAsB,CAAC;AAE5E,OAAO,KAAK,EACV,eAAe,EAEhB,MAAM,yBAAyB,CAAC;AAcjC,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAWvD,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,UAAU,CAAC,EAAE,kBAAkB,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,cAAc,GAAG,WAAW,GAAG,WAAW,GAAG,aAAa,GAAG,kBAAkB,GAAG,mBAAmB,CAAC;IAChH,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,UAAU,CAAC,CAAqB;IACxC,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,cAAc,CAAiB;gBAE3B,OAAO,EAAE,sBAAsB;IAkB3C;;;;;OAKG;IACH,iBAAiB,IAAI,UAAU,CAAC,cAAc,CAAC,YAAY,CAAC,CAAC;IAI7D;;OAEG;IACH,0BAA0B,IAAI,MAAM;IAKpC;;OAEG;IACG,gCAAgC,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAKhF;;OAEG;IACG,+BAA+B,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAKlF;;;;;;;;OAQG;IACG,6BAA6B,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC;IA+E7E;;;;;;;;OAQG;IACG,+BAA+B,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC;IA0G/E;;;;;;;;;;;;OAYG;IACG,oBAAoB,IAAI,OAAO,CAAC,UAAU,GAAG;QAAE,YAAY,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IA2J9E;;OAEG;IACG,uBAAuB,IAAI,OAAO,CAAC,UAAU,CAAC;IAuHpD;;OAEG;YACW,aAAa;IA6L3B;;OAEG;YACW,kBAAkB;IAmEhC;;OAEG;YACW,2BAA2B;IAyGzC;;;;OAIG;IACH,OAAO,CAAC,wBAAwB;IA0BhC;;OAEG;YACW,UAAU;IAWxB;;;OAGG;YACW,gBAAgB;IAI9B;;OAEG;IACH,OAAO,CAAC,0BAA0B;IA0ClC;;;;;;OAMG;IACH,OAAO,CAAC,2BAA2B;CA0CpC"}
|
|
@@ -13,7 +13,6 @@ import { GitHubClientV2 } from '../../plugins/specweave-github/lib/github-client
|
|
|
13
13
|
import { consoleLogger } from '../utils/logger.js';
|
|
14
14
|
import { FrontmatterUpdater } from './frontmatter-updater.js';
|
|
15
15
|
import { autoDetectProjectIdSync } from '../utils/project-detection.js';
|
|
16
|
-
import { UserStoryContentBuilder } from '../../plugins/specweave-github/lib/user-story-content-builder.js';
|
|
17
16
|
import { AdoClient } from '../integrations/ado/ado-client.js';
|
|
18
17
|
import { getAdoPat } from '../integrations/ado/ado-pat-provider.js';
|
|
19
18
|
import { deriveFeatureId } from '../utils/feature-id-derivation.js';
|
|
@@ -58,333 +57,19 @@ export class SyncCoordinator {
|
|
|
58
57
|
return this.metrics.formatSummary();
|
|
59
58
|
}
|
|
60
59
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* This is the AUTOMATIC GitHub sync that runs when increment completes.
|
|
64
|
-
* Creates issues for User Stories that don't have GitHub issues yet.
|
|
65
|
-
*
|
|
66
|
-
* Uses 3-layer idempotency:
|
|
67
|
-
* - Layer 1: Check user story frontmatter (fastest, <1ms)
|
|
68
|
-
* - Layer 2: Check increment metadata.json (fast, <5ms)
|
|
69
|
-
* - Layer 3: Query GitHub API (slow but authoritative, 500-2000ms)
|
|
70
|
-
*
|
|
71
|
-
* @param config - Project configuration
|
|
72
|
-
* @returns Array of created issues
|
|
60
|
+
* @deprecated (0348) GitHub issue creation now handled by GitHubFeatureSync via LivingDocsSync chain.
|
|
73
61
|
*/
|
|
74
|
-
async createGitHubIssuesForUserStories(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// Load user stories for this increment
|
|
78
|
-
const userStories = await this.loadUserStoriesForIncrement();
|
|
79
|
-
if (userStories.length === 0) {
|
|
80
|
-
this.logger.log('📚 No user stories found for this increment');
|
|
81
|
-
return createdIssues;
|
|
82
|
-
}
|
|
83
|
-
this.logger.log(`📚 Found ${userStories.length} user story/stories for GitHub sync`);
|
|
84
|
-
// Get GitHub config
|
|
85
|
-
const githubConfig = config.sync?.github || {};
|
|
86
|
-
const repoInfo = await this.detectGitHubRepo(githubConfig);
|
|
87
|
-
if (!repoInfo) {
|
|
88
|
-
throw new Error('GitHub repository not configured');
|
|
89
|
-
}
|
|
90
|
-
const client = GitHubClientV2.fromRepo(repoInfo.owner, repoInfo.repo);
|
|
91
|
-
// Get feature ID from increment spec
|
|
92
|
-
const specFile = path.join(this.projectRoot, '.specweave/increments', this.incrementId, 'spec.md');
|
|
93
|
-
let featureId = '';
|
|
94
|
-
if (existsSync(specFile)) {
|
|
95
|
-
const content = await fs.readFile(specFile, 'utf-8');
|
|
96
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
97
|
-
if (frontmatterMatch) {
|
|
98
|
-
const frontmatter = yaml.parse(frontmatterMatch[1]);
|
|
99
|
-
featureId = frontmatter.feature_id || frontmatter.epic || frontmatter.feature || '';
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (!featureId) {
|
|
103
|
-
// AUTO-GENERATE feature ID using deriveFeatureId() (ADR-0139)
|
|
104
|
-
// CRITICAL FIX: Must use deriveFeatureId() to get correct format (FS-128, not FS-0128)
|
|
105
|
-
// The old code used raw regex match which preserved leading zeros, causing duplicates
|
|
106
|
-
try {
|
|
107
|
-
featureId = deriveFeatureId(this.incrementId);
|
|
108
|
-
this.logger.log(`📝 Auto-generated feature ID: ${featureId} (no epic/feature_id in spec.md)`);
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
this.logger.log('⚠️ No feature ID found and could not auto-generate - skipping GitHub sync');
|
|
112
|
-
this.logger.log(' 💡 Add epic: FS-XXX or feature_id: FS-XXX to spec.md frontmatter');
|
|
113
|
-
return createdIssues;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
// Try to find or create milestone for the feature
|
|
117
|
-
let milestoneNumber = null;
|
|
118
|
-
try {
|
|
119
|
-
const milestone = await client.createOrGetMilestone(`${featureId}: Automatic GitHub Sync`, `Feature milestone for ${featureId}`, 30 // 30 days from now
|
|
120
|
-
);
|
|
121
|
-
milestoneNumber = milestone.number;
|
|
122
|
-
this.logger.log(`🎯 Using milestone: ${milestone.title} (#${milestone.number})`);
|
|
123
|
-
}
|
|
124
|
-
catch (error) {
|
|
125
|
-
this.logger.log('⚠️ Could not create/get milestone, continuing without it');
|
|
126
|
-
}
|
|
127
|
-
// Create issue for each user story (with idempotency + atomic locking)
|
|
128
|
-
// Ensure locks directory exists (once per batch, not per user story)
|
|
129
|
-
const locksDir = path.join(this.projectRoot, '.specweave/state/.locks');
|
|
130
|
-
if (!existsSync(locksDir)) {
|
|
131
|
-
await fs.mkdir(locksDir, { recursive: true });
|
|
132
|
-
}
|
|
133
|
-
for (const usFile of userStories) {
|
|
134
|
-
// ATOMIC LOCK: Prevent race conditions when multiple syncs run concurrently
|
|
135
|
-
// Lock is per-user-story to allow parallel processing of different stories
|
|
136
|
-
const lockDir = path.join(locksDir, `github-issue-${featureId}-${usFile.id}`.toLowerCase().replace(/[^a-z0-9-]/g, '-'));
|
|
137
|
-
const lockManager = new LockManager(lockDir, 60, { logger: this.logger }); // 60s stale threshold
|
|
138
|
-
let lockAcquired = false;
|
|
139
|
-
try {
|
|
140
|
-
lockAcquired = await lockManager.acquire();
|
|
141
|
-
if (!lockAcquired) {
|
|
142
|
-
this.logger.log(` ⏳ ${usFile.id} - Another sync in progress, skipping (lock not acquired)`);
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
// LAYER 1: Check user story frontmatter for existing GitHub issue
|
|
146
|
-
const cachedIssue = await this.frontmatterUpdater.getGitHubIssueFromFrontmatter(this.projectRoot, featureId, usFile.id);
|
|
147
|
-
if (cachedIssue) {
|
|
148
|
-
this.logger.log(` 🔍 ${usFile.id} - Issue #${cachedIssue.number} exists (cached)`);
|
|
149
|
-
// Check if issue needs content update (has placeholder body)
|
|
150
|
-
await this.updateIssueIfPlaceholder(cachedIssue.number, usFile.id, featureId, client, repoInfo);
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
// LAYER 2: Check increment metadata.json
|
|
154
|
-
const metadataFile = path.join(this.projectRoot, '.specweave/increments', this.incrementId, 'metadata.json');
|
|
155
|
-
let existingIssue = null;
|
|
156
|
-
if (existsSync(metadataFile)) {
|
|
157
|
-
const metadata = JSON.parse(await fs.readFile(metadataFile, 'utf-8'));
|
|
158
|
-
// Check OLD format (metadata.github.issues[])
|
|
159
|
-
const githubIssues = metadata.github?.issues || [];
|
|
160
|
-
let found = githubIssues.find((i) => i.userStory === usFile.id);
|
|
161
|
-
// v1.0.240 FIX: Also check NEW format (externalLinks.github.issues{})
|
|
162
|
-
if (!found) {
|
|
163
|
-
const extLinks = metadata.externalLinks?.github?.issues;
|
|
164
|
-
if (extLinks && extLinks[usFile.id]?.issueNumber) {
|
|
165
|
-
found = {
|
|
166
|
-
userStory: usFile.id,
|
|
167
|
-
number: extLinks[usFile.id].issueNumber,
|
|
168
|
-
url: extLinks[usFile.id].issueUrl || '',
|
|
169
|
-
createdAt: new Date().toISOString(),
|
|
170
|
-
};
|
|
171
|
-
this.logger.log(` 🔍 ${usFile.id} - Issue #${found.number} exists (externalLinks)`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
if (found) {
|
|
175
|
-
this.logger.log(` 🔍 ${usFile.id} - Issue #${found.number} exists (metadata)`);
|
|
176
|
-
// Check if issue needs content update
|
|
177
|
-
await this.updateIssueIfPlaceholder(found.number, usFile.id, featureId, client, repoInfo);
|
|
178
|
-
existingIssue = found.number;
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
// LAYER 3: Query GitHub API to check for existing issue
|
|
183
|
-
const searchTitle = `[${featureId}][${usFile.id}]`;
|
|
184
|
-
const existingOnGitHub = await client.searchIssueByTitle(searchTitle);
|
|
185
|
-
if (existingOnGitHub) {
|
|
186
|
-
this.logger.log(` 🔍 ${usFile.id} - Issue #${existingOnGitHub.number} already exists on GitHub`);
|
|
187
|
-
// CHECK: Does issue have placeholder content that needs updating?
|
|
188
|
-
const hasPlaceholderBody = existingOnGitHub.body?.includes('This issue was auto-created by SpecWeave')
|
|
189
|
-
&& !existingOnGitHub.body?.includes('## Acceptance Criteria');
|
|
190
|
-
if (hasPlaceholderBody) {
|
|
191
|
-
this.logger.log(` 📝 Issue has placeholder content - updating with rich body...`);
|
|
192
|
-
try {
|
|
193
|
-
// Find the user story file in living docs
|
|
194
|
-
const featurePath = path.join(this.projectRoot, '.specweave/docs/internal/specs', this.projectId, featureId);
|
|
195
|
-
const usIdNumber = usFile.id.toLowerCase().replace('us-', '');
|
|
196
|
-
const featureFiles = existsSync(featurePath) ? await fs.readdir(featurePath) : [];
|
|
197
|
-
const usFileName = featureFiles.find(f => f.startsWith(`us-${usIdNumber}-`) && f.endsWith('.md'));
|
|
198
|
-
if (usFileName) {
|
|
199
|
-
const usFilePath = path.join(featurePath, usFileName);
|
|
200
|
-
const contentBuilder = new UserStoryContentBuilder(usFilePath, this.projectRoot);
|
|
201
|
-
const richBody = await contentBuilder.buildIssueBody(`${repoInfo.owner}/${repoInfo.repo}`);
|
|
202
|
-
// Update the issue body via gh CLI
|
|
203
|
-
await client.updateIssueBody(existingOnGitHub.number, richBody);
|
|
204
|
-
this.logger.log(` ✅ Updated issue #${existingOnGitHub.number} with rich content (ACs, tasks)`);
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
this.logger.log(` ⚠️ User story file not found - keeping placeholder content`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
catch (error) {
|
|
211
|
-
this.logger.log(` ⚠️ Failed to update issue body: ${error}`);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
215
|
-
this.logger.log(` ⏭️ Issue already has rich content - skipping update`);
|
|
216
|
-
}
|
|
217
|
-
// Backfill Layer 1 (frontmatter) and Layer 2 (metadata)
|
|
218
|
-
await this.frontmatterUpdater.updateUserStoryFrontmatter({
|
|
219
|
-
projectRoot: this.projectRoot,
|
|
220
|
-
featureId,
|
|
221
|
-
userStoryId: usFile.id,
|
|
222
|
-
githubIssue: {
|
|
223
|
-
number: existingOnGitHub.number,
|
|
224
|
-
url: existingOnGitHub.html_url,
|
|
225
|
-
createdAt: new Date().toISOString(),
|
|
226
|
-
},
|
|
227
|
-
});
|
|
228
|
-
// Backfill Layer 2 (metadata.json)
|
|
229
|
-
if (existsSync(metadataFile)) {
|
|
230
|
-
const metadata = JSON.parse(await fs.readFile(metadataFile, 'utf-8'));
|
|
231
|
-
if (!metadata.github) {
|
|
232
|
-
metadata.github = {};
|
|
233
|
-
}
|
|
234
|
-
if (!metadata.github.issues) {
|
|
235
|
-
metadata.github.issues = [];
|
|
236
|
-
}
|
|
237
|
-
// Check if not already in metadata
|
|
238
|
-
const existsInMetadata = metadata.github.issues.find((i) => i.userStory === usFile.id);
|
|
239
|
-
if (!existsInMetadata) {
|
|
240
|
-
metadata.github.issues.push({
|
|
241
|
-
userStory: usFile.id,
|
|
242
|
-
number: existingOnGitHub.number,
|
|
243
|
-
url: existingOnGitHub.html_url,
|
|
244
|
-
createdAt: new Date().toISOString(),
|
|
245
|
-
});
|
|
246
|
-
metadata.github.lastSync = new Date().toISOString();
|
|
247
|
-
await fs.writeFile(metadataFile, JSON.stringify(metadata, null, 2), 'utf-8');
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
// All 3 layers miss - but check for DUPLICATES with wrong format first!
|
|
253
|
-
// ========================================================================
|
|
254
|
-
// DUPLICATE DETECTION: Prevent FS-0128 vs FS-128 duplicates
|
|
255
|
-
// ========================================================================
|
|
256
|
-
// Before creating, search for issues with similar titles but different feature ID formats.
|
|
257
|
-
// This catches cases where an old bug created issues with leading zeros (FS-0128)
|
|
258
|
-
// but the correct format is without (FS-128).
|
|
259
|
-
const duplicateCheck = await this.detectDuplicateIssue(client, featureId, usFile.id);
|
|
260
|
-
if (duplicateCheck) {
|
|
261
|
-
this.logger.log(` ⚠️ DUPLICATE DETECTED: Issue #${duplicateCheck.number} exists with format "${duplicateCheck.format}"`);
|
|
262
|
-
this.logger.log(` Current format: [${featureId}][${usFile.id}]`);
|
|
263
|
-
this.logger.log(` Existing issue: ${duplicateCheck.title}`);
|
|
264
|
-
this.logger.log(` ⏭️ Skipping creation to avoid duplicate. Use existing issue #${duplicateCheck.number}`);
|
|
265
|
-
// Backfill metadata with the existing issue (even if wrong format)
|
|
266
|
-
await this.frontmatterUpdater.updateUserStoryFrontmatter({
|
|
267
|
-
projectRoot: this.projectRoot,
|
|
268
|
-
featureId,
|
|
269
|
-
userStoryId: usFile.id,
|
|
270
|
-
githubIssue: {
|
|
271
|
-
number: duplicateCheck.number,
|
|
272
|
-
url: duplicateCheck.url,
|
|
273
|
-
createdAt: new Date().toISOString(),
|
|
274
|
-
},
|
|
275
|
-
});
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
278
|
-
this.logger.log(` 📝 Creating GitHub issue for ${usFile.id}...`);
|
|
279
|
-
// Format issue body - use UserStoryContentBuilder for rich content with ACs
|
|
280
|
-
let issueBody;
|
|
281
|
-
try {
|
|
282
|
-
// Find the user story file in living docs
|
|
283
|
-
const featurePath = path.join(this.projectRoot, '.specweave/docs/internal/specs', this.projectId, featureId);
|
|
284
|
-
// Search for file matching us-{number}-*.md pattern
|
|
285
|
-
const usIdNumber = usFile.id.toLowerCase().replace('us-', '');
|
|
286
|
-
const featureFiles = existsSync(featurePath) ? await fs.readdir(featurePath) : [];
|
|
287
|
-
const usFileName = featureFiles.find(f => f.startsWith(`us-${usIdNumber}-`) && f.endsWith('.md'));
|
|
288
|
-
if (usFileName) {
|
|
289
|
-
const usFilePath = path.join(featurePath, usFileName);
|
|
290
|
-
const contentBuilder = new UserStoryContentBuilder(usFilePath, this.projectRoot);
|
|
291
|
-
issueBody = await contentBuilder.buildIssueBody(`${repoInfo.owner}/${repoInfo.repo}`);
|
|
292
|
-
this.logger.log(` 📄 Built rich issue body with ACs from ${usFileName}`);
|
|
293
|
-
}
|
|
294
|
-
else {
|
|
295
|
-
// Fallback to basic body if file not found
|
|
296
|
-
this.logger.log(` ⚠️ User story file not found for ${usFile.id}, using basic body`);
|
|
297
|
-
issueBody = this.formatUserStoryBody(usFile);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
catch (error) {
|
|
301
|
-
// Fallback to basic body on any error
|
|
302
|
-
this.logger.log(` ⚠️ Failed to build rich body: ${error}, using fallback`);
|
|
303
|
-
issueBody = this.formatUserStoryBody(usFile);
|
|
304
|
-
}
|
|
305
|
-
// Create issue
|
|
306
|
-
const issue = await client.createUserStoryIssue({
|
|
307
|
-
featureId,
|
|
308
|
-
userStoryId: usFile.id,
|
|
309
|
-
title: usFile.title,
|
|
310
|
-
body: issueBody,
|
|
311
|
-
labels: [],
|
|
312
|
-
milestone: milestoneNumber,
|
|
313
|
-
});
|
|
314
|
-
this.logger.log(` ✅ Created issue #${issue.number}: ${issue.html_url}`);
|
|
315
|
-
createdIssues.push(issue);
|
|
316
|
-
// Update increment metadata.json (Layer 2)
|
|
317
|
-
if (existsSync(metadataFile)) {
|
|
318
|
-
const metadata = JSON.parse(await fs.readFile(metadataFile, 'utf-8'));
|
|
319
|
-
if (!metadata.github) {
|
|
320
|
-
metadata.github = {};
|
|
321
|
-
}
|
|
322
|
-
if (!metadata.github.issues) {
|
|
323
|
-
metadata.github.issues = [];
|
|
324
|
-
}
|
|
325
|
-
metadata.github.issues.push({
|
|
326
|
-
userStory: usFile.id,
|
|
327
|
-
number: issue.number,
|
|
328
|
-
url: issue.html_url,
|
|
329
|
-
createdAt: new Date().toISOString(),
|
|
330
|
-
});
|
|
331
|
-
metadata.github.lastSync = new Date().toISOString();
|
|
332
|
-
await fs.writeFile(metadataFile, JSON.stringify(metadata, null, 2), 'utf-8');
|
|
333
|
-
}
|
|
334
|
-
// Update user story frontmatter (Layer 1 backfill)
|
|
335
|
-
await this.frontmatterUpdater.updateUserStoryFrontmatter({
|
|
336
|
-
projectRoot: this.projectRoot,
|
|
337
|
-
featureId,
|
|
338
|
-
userStoryId: usFile.id,
|
|
339
|
-
githubIssue: {
|
|
340
|
-
number: issue.number,
|
|
341
|
-
url: issue.html_url,
|
|
342
|
-
createdAt: new Date().toISOString(),
|
|
343
|
-
},
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
catch (error) {
|
|
347
|
-
this.logger.error(` ❌ Failed to create issue for ${usFile.id}:`, error);
|
|
348
|
-
}
|
|
349
|
-
finally {
|
|
350
|
-
// ALWAYS release lock, even on error or early continue
|
|
351
|
-
if (lockAcquired) {
|
|
352
|
-
await lockManager.release();
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
if (createdIssues.length > 0) {
|
|
357
|
-
this.logger.log(`\n✅ Created ${createdIssues.length} GitHub issue(s) for ${featureId}`);
|
|
358
|
-
createdIssues.forEach(issue => {
|
|
359
|
-
this.logger.log(` - Issue #${issue.number}: ${issue.html_url}`);
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
else {
|
|
363
|
-
this.logger.log('\n✅ All GitHub issues already exist (0 new issues created)');
|
|
364
|
-
}
|
|
365
|
-
return createdIssues;
|
|
366
|
-
}
|
|
367
|
-
catch (error) {
|
|
368
|
-
this.logger.error('❌ Failed to create GitHub issues:', error);
|
|
369
|
-
throw error;
|
|
370
|
-
}
|
|
62
|
+
async createGitHubIssuesForUserStories(_config) {
|
|
63
|
+
this.logger.log('GitHub issue creation handled by GitHubFeatureSync pipeline');
|
|
64
|
+
return [];
|
|
371
65
|
}
|
|
372
66
|
/**
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
* This is the CRITICAL missing feature that was causing GitHub issues to remain
|
|
376
|
-
* open after increments were closed.
|
|
377
|
-
*
|
|
378
|
-
* Called by: syncIncrementClosure() when increment status changes to "completed"
|
|
379
|
-
*
|
|
380
|
-
* Flow:
|
|
381
|
-
* 1. Get feature ID from increment spec
|
|
382
|
-
* 2. Load all user stories for that feature
|
|
383
|
-
* 3. For each user story with a GitHub issue, close it with completion comment
|
|
384
|
-
*
|
|
385
|
-
* @param config - Project configuration
|
|
386
|
-
* @returns Array of closed issue numbers
|
|
67
|
+
* @deprecated (0348) GitHub closure now handled by GitHubFeatureSync via LivingDocsSync chain.
|
|
387
68
|
*/
|
|
69
|
+
async closeGitHubIssuesForUserStories(_config) {
|
|
70
|
+
this.logger.log('GitHub closure handled by GitHubFeatureSync pipeline');
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
388
73
|
/**
|
|
389
74
|
* Close JIRA issues for completed user stories
|
|
390
75
|
*
|
|
@@ -712,121 +397,6 @@ export class SyncCoordinator {
|
|
|
712
397
|
}
|
|
713
398
|
}
|
|
714
399
|
}
|
|
715
|
-
/**
|
|
716
|
-
* Detect duplicate issues with different feature ID formats
|
|
717
|
-
*
|
|
718
|
-
* Searches for issues that match the user story but have a different feature ID format.
|
|
719
|
-
* This prevents creating duplicates like:
|
|
720
|
-
* - [FS-0128][US-001] (old bug format with leading zeros)
|
|
721
|
-
* - [FS-128][US-001] (correct format)
|
|
722
|
-
*
|
|
723
|
-
* The search uses a regex pattern that matches any FS-XXX format for the same US-XXX.
|
|
724
|
-
*
|
|
725
|
-
* @param client - GitHub client
|
|
726
|
-
* @param featureId - Current feature ID (e.g., "FS-128")
|
|
727
|
-
* @param userStoryId - User story ID (e.g., "US-001")
|
|
728
|
-
* @returns Duplicate info if found, null otherwise
|
|
729
|
-
*/
|
|
730
|
-
async detectDuplicateIssue(client, featureId, userStoryId) {
|
|
731
|
-
try {
|
|
732
|
-
// Extract the numeric part of the feature ID (e.g., "128" from "FS-128" or "FS-0128")
|
|
733
|
-
const featureNumMatch = featureId.match(/FS-0*(\d+)E?/i);
|
|
734
|
-
if (!featureNumMatch) {
|
|
735
|
-
return null;
|
|
736
|
-
}
|
|
737
|
-
const featureNum = featureNumMatch[1];
|
|
738
|
-
// Search for issues with ANY format of this feature ID + user story
|
|
739
|
-
// Patterns to check:
|
|
740
|
-
// - [FS-128][US-001] (correct, no leading zeros)
|
|
741
|
-
// - [FS-0128][US-001] (bug format, with leading zeros)
|
|
742
|
-
// - [FS-00128][US-001] (edge case, multiple leading zeros)
|
|
743
|
-
const searchPatterns = [
|
|
744
|
-
`[FS-${featureNum}][${userStoryId}]`, // FS-128
|
|
745
|
-
`[FS-0${featureNum}][${userStoryId}]`, // FS-0128
|
|
746
|
-
`[FS-00${featureNum}][${userStoryId}]`, // FS-00128
|
|
747
|
-
`[FS-${featureNum}E][${userStoryId}]`, // FS-128E (external)
|
|
748
|
-
`[FS-0${featureNum}E][${userStoryId}]`, // FS-0128E
|
|
749
|
-
];
|
|
750
|
-
// Filter out the exact current format (we already checked that)
|
|
751
|
-
const currentFormat = `[${featureId}][${userStoryId}]`;
|
|
752
|
-
const alternatePatterns = searchPatterns.filter(p => p !== currentFormat);
|
|
753
|
-
for (const pattern of alternatePatterns) {
|
|
754
|
-
const existingIssue = await client.searchIssueByTitle(pattern, true); // Include closed
|
|
755
|
-
if (existingIssue) {
|
|
756
|
-
// Extract the format from the issue title
|
|
757
|
-
const formatMatch = existingIssue.title.match(/\[(FS-\d+E?)\]/i);
|
|
758
|
-
const detectedFormat = formatMatch ? formatMatch[1] : 'unknown';
|
|
759
|
-
return {
|
|
760
|
-
number: existingIssue.number,
|
|
761
|
-
url: existingIssue.html_url,
|
|
762
|
-
title: existingIssue.title,
|
|
763
|
-
format: detectedFormat,
|
|
764
|
-
};
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
return null;
|
|
768
|
-
}
|
|
769
|
-
catch (error) {
|
|
770
|
-
// Don't block issue creation on duplicate detection failure
|
|
771
|
-
this.logger.log(` ⚠️ Duplicate detection failed (non-blocking): ${error}`);
|
|
772
|
-
return null;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
/**
|
|
776
|
-
* Update issue if it has placeholder content
|
|
777
|
-
* Fetches issue from GitHub, checks for placeholder, and updates with rich content
|
|
778
|
-
*/
|
|
779
|
-
async updateIssueIfPlaceholder(issueNumber, userStoryId, featureId, client, repoInfo) {
|
|
780
|
-
try {
|
|
781
|
-
// Fetch issue from GitHub to check body content
|
|
782
|
-
const issue = await client.getIssue(issueNumber);
|
|
783
|
-
if (!issue) {
|
|
784
|
-
this.logger.log(` ⚠️ Could not fetch issue #${issueNumber}`);
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
// Check if issue has placeholder content
|
|
788
|
-
const hasPlaceholderBody = issue.body?.includes('This issue was auto-created by SpecWeave')
|
|
789
|
-
&& !issue.body?.includes('## Acceptance Criteria');
|
|
790
|
-
if (!hasPlaceholderBody) {
|
|
791
|
-
this.logger.log(` ✓ Issue already has rich content`);
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
this.logger.log(` 📝 Updating placeholder content with rich body...`);
|
|
795
|
-
// Find the user story file in living docs
|
|
796
|
-
const featurePath = path.join(this.projectRoot, '.specweave/docs/internal/specs', this.projectId, featureId);
|
|
797
|
-
const usIdNumber = userStoryId.toLowerCase().replace('us-', '');
|
|
798
|
-
const featureFiles = existsSync(featurePath) ? await fs.readdir(featurePath) : [];
|
|
799
|
-
const usFileName = featureFiles.find(f => f.startsWith(`us-${usIdNumber}-`) && f.endsWith('.md'));
|
|
800
|
-
if (!usFileName) {
|
|
801
|
-
this.logger.log(` ⚠️ User story file not found - keeping placeholder`);
|
|
802
|
-
return;
|
|
803
|
-
}
|
|
804
|
-
const usFilePath = path.join(featurePath, usFileName);
|
|
805
|
-
const contentBuilder = new UserStoryContentBuilder(usFilePath, this.projectRoot);
|
|
806
|
-
const richBody = await contentBuilder.buildIssueBody(`${repoInfo.owner}/${repoInfo.repo}`);
|
|
807
|
-
// Update the issue body
|
|
808
|
-
await client.updateIssueBody(issueNumber, richBody);
|
|
809
|
-
this.logger.log(` ✅ Updated issue #${issueNumber} with ACs and tasks`);
|
|
810
|
-
}
|
|
811
|
-
catch (error) {
|
|
812
|
-
this.logger.log(` ⚠️ Failed to update: ${error}`);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
/**
|
|
816
|
-
* Format user story content for GitHub issue body
|
|
817
|
-
*/
|
|
818
|
-
formatUserStoryBody(usFile) {
|
|
819
|
-
// Simple body for now - can be enhanced later
|
|
820
|
-
const parts = [];
|
|
821
|
-
parts.push(`## User Story: ${usFile.id}`);
|
|
822
|
-
parts.push('');
|
|
823
|
-
if (usFile.external_title) {
|
|
824
|
-
parts.push(`**Original Title**: ${usFile.external_title}`);
|
|
825
|
-
parts.push('');
|
|
826
|
-
}
|
|
827
|
-
parts.push('For detailed acceptance criteria, tasks, and technical specifications, see the living docs in the repository.');
|
|
828
|
-
return parts.join('\n');
|
|
829
|
-
}
|
|
830
400
|
/**
|
|
831
401
|
* Sync increment completion to external tools using format preservation
|
|
832
402
|
*/
|
|
@@ -1350,20 +920,5 @@ export class SyncCoordinator {
|
|
|
1350
920
|
lines.push(`🤖 Auto-synced by SpecWeave`);
|
|
1351
921
|
return lines.join('\n');
|
|
1352
922
|
}
|
|
1353
|
-
/**
|
|
1354
|
-
* Sync AC checkbox status to GitHub issues
|
|
1355
|
-
*
|
|
1356
|
-
* @deprecated (0348) Delegates to GitHubACCheckboxSync in the GitHub plugin.
|
|
1357
|
-
* Kept for backward compatibility with callers that haven't migrated yet.
|
|
1358
|
-
*/
|
|
1359
|
-
async syncACCheckboxesToGitHub(config, options = {}) {
|
|
1360
|
-
const { GitHubACCheckboxSync } = await import('../../plugins/specweave-github/lib/github-ac-checkbox-sync.js');
|
|
1361
|
-
const sync = new GitHubACCheckboxSync({
|
|
1362
|
-
projectRoot: this.projectRoot,
|
|
1363
|
-
incrementId: this.incrementId,
|
|
1364
|
-
logger: this.logger,
|
|
1365
|
-
});
|
|
1366
|
-
return sync.syncACCheckboxesToGitHub(config, options);
|
|
1367
|
-
}
|
|
1368
923
|
}
|
|
1369
924
|
//# sourceMappingURL=sync-coordinator.js.map
|