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.
@@ -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
- * Create GitHub issues for all User Stories in the feature (NEW in v0.25.0)
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(config: SpecWeaveConfig): Promise<GitHubIssue[]>;
52
+ createGitHubIssuesForUserStories(_config: SpecWeaveConfig): Promise<any[]>;
65
53
  /**
66
- * Close GitHub issues for all User Stories when increment completes (NEW in v0.28.1)
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,WAAW,EAAE,MAAM,6CAA6C,CAAC;AAC1E,OAAO,EAAE,MAAM,EAAiB,MAAM,oBAAoB,CAAC;AAK3D,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;AAiCvD,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;IAIpC;;;;;;;;;;;;;OAaG;IACG,gCAAgC,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IA+WvF;;;;;;;;;;;;;;;OAeG;IACH;;;;;;;;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;IA0J9E;;;;;;;;;;;;;;OAcG;YACW,oBAAoB;IAsDlC;;;OAGG;YACW,wBAAwB;IA2DtC;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAiB3B;;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;IA0CnC;;;;;OAKG;IACG,wBAAwB,CAC5B,MAAM,EAAE,eAAe,EACvB,OAAO,GAAE;QAAE,UAAU,CAAC,EAAE,OAAO,CAAA;KAAO,GACrC,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;CAWpE"}
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
- * Create GitHub issues for all User Stories in the feature (NEW in v0.25.0)
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(config) {
75
- const createdIssues = [];
76
- try {
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
- * Close GitHub issues for all User Stories when increment completes (NEW in v0.28.1)
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