specweave 0.28.19 → 0.28.20

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.
Files changed (85) hide show
  1. package/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts +16 -0
  2. package/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts.map +1 -1
  3. package/dist/plugins/specweave-ado/lib/ado-spec-sync.js +63 -3
  4. package/dist/plugins/specweave-ado/lib/ado-spec-sync.js.map +1 -1
  5. package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts +12 -3
  6. package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts.map +1 -1
  7. package/dist/plugins/specweave-ado/lib/ado-status-sync.js +37 -3
  8. package/dist/plugins/specweave-ado/lib/ado-status-sync.js.map +1 -1
  9. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts +8 -6
  10. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts.map +1 -1
  11. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js +230 -165
  12. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js.map +1 -1
  13. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts +10 -0
  14. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts.map +1 -1
  15. package/dist/plugins/specweave-github/lib/github-status-sync.js +40 -2
  16. package/dist/plugins/specweave-github/lib/github-status-sync.js.map +1 -1
  17. package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts +2 -0
  18. package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts.map +1 -1
  19. package/dist/plugins/specweave-github/lib/increment-issue-builder.js +25 -5
  20. package/dist/plugins/specweave-github/lib/increment-issue-builder.js.map +1 -1
  21. package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts +12 -0
  22. package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts.map +1 -1
  23. package/dist/plugins/specweave-jira/lib/jira-spec-sync.js +57 -5
  24. package/dist/plugins/specweave-jira/lib/jira-spec-sync.js.map +1 -1
  25. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +5 -1
  26. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -1
  27. package/dist/plugins/specweave-jira/lib/jira-status-sync.js +12 -4
  28. package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -1
  29. package/dist/src/cli/helpers/init/external-import.d.ts.map +1 -1
  30. package/dist/src/cli/helpers/init/external-import.js +186 -19
  31. package/dist/src/cli/helpers/init/external-import.js.map +1 -1
  32. package/dist/src/cli/helpers/init/jira-ado-auto-detect.d.ts +115 -0
  33. package/dist/src/cli/helpers/init/jira-ado-auto-detect.d.ts.map +1 -0
  34. package/dist/src/cli/helpers/init/jira-ado-auto-detect.js +590 -0
  35. package/dist/src/cli/helpers/init/jira-ado-auto-detect.js.map +1 -0
  36. package/dist/src/config/types.d.ts +6 -6
  37. package/dist/src/core/background/index.d.ts +11 -0
  38. package/dist/src/core/background/index.d.ts.map +1 -0
  39. package/dist/src/core/background/index.js +11 -0
  40. package/dist/src/core/background/index.js.map +1 -0
  41. package/dist/src/core/background/job-manager.d.ts +65 -0
  42. package/dist/src/core/background/job-manager.d.ts.map +1 -0
  43. package/dist/src/core/background/job-manager.js +192 -0
  44. package/dist/src/core/background/job-manager.js.map +1 -0
  45. package/dist/src/core/background/types.d.ts +59 -0
  46. package/dist/src/core/background/types.d.ts.map +1 -0
  47. package/dist/src/core/background/types.js +8 -0
  48. package/dist/src/core/background/types.js.map +1 -0
  49. package/dist/src/core/repo-structure/multi-repo-configurator.d.ts +25 -0
  50. package/dist/src/core/repo-structure/multi-repo-configurator.d.ts.map +1 -0
  51. package/dist/src/core/repo-structure/multi-repo-configurator.js +614 -0
  52. package/dist/src/core/repo-structure/multi-repo-configurator.js.map +1 -0
  53. package/dist/src/core/repo-structure/repo-initializer.d.ts +40 -0
  54. package/dist/src/core/repo-structure/repo-initializer.d.ts.map +1 -0
  55. package/dist/src/core/repo-structure/repo-initializer.js +252 -0
  56. package/dist/src/core/repo-structure/repo-initializer.js.map +1 -0
  57. package/dist/src/core/repo-structure/repo-structure-manager.d.ts +3 -37
  58. package/dist/src/core/repo-structure/repo-structure-manager.d.ts.map +1 -1
  59. package/dist/src/core/repo-structure/repo-structure-manager.js +23 -803
  60. package/dist/src/core/repo-structure/repo-structure-manager.js.map +1 -1
  61. package/dist/src/core/types/spec-metadata.d.ts +2 -0
  62. package/dist/src/core/types/spec-metadata.d.ts.map +1 -1
  63. package/dist/src/importers/import-coordinator.d.ts +20 -0
  64. package/dist/src/importers/import-coordinator.d.ts.map +1 -1
  65. package/dist/src/importers/import-coordinator.js.map +1 -1
  66. package/dist/src/init/architecture/types.d.ts +2 -2
  67. package/dist/src/init/compliance/types.d.ts +1 -1
  68. package/package.json +1 -1
  69. package/plugins/specweave/commands/specweave-jobs.md +160 -0
  70. package/plugins/specweave-ado/lib/ado-spec-sync.js +59 -3
  71. package/plugins/specweave-ado/lib/ado-spec-sync.ts +72 -3
  72. package/plugins/specweave-ado/lib/ado-status-sync.js +35 -3
  73. package/plugins/specweave-ado/lib/ado-status-sync.ts +48 -4
  74. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +6 -0
  75. package/plugins/specweave-github/lib/github-increment-sync-cli.js +268 -155
  76. package/plugins/specweave-github/lib/github-increment-sync-cli.ts +313 -209
  77. package/plugins/specweave-github/lib/github-status-sync.js +37 -1
  78. package/plugins/specweave-github/lib/github-status-sync.ts +60 -4
  79. package/plugins/specweave-github/lib/increment-issue-builder.js +26 -5
  80. package/plugins/specweave-github/lib/increment-issue-builder.ts +36 -5
  81. package/plugins/specweave-jira/lib/jira-spec-sync.js +53 -5
  82. package/plugins/specweave-jira/lib/jira-spec-sync.ts +87 -7
  83. package/plugins/specweave-jira/lib/jira-status-sync.js +9 -3
  84. package/plugins/specweave-jira/lib/jira-status-sync.ts +15 -6
  85. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +9 -0
@@ -79,32 +79,205 @@ async function findIncrementFolder(incrementId) {
79
79
  }
80
80
  return null;
81
81
  }
82
- async function findExistingIssue(owner, repo, featureId) {
83
- const result = await execFileNoThrow("gh", [
84
- "search",
85
- "issues",
86
- `repo:${owner}/${repo}`,
87
- `"[${featureId}]" in:title`,
88
- "is:open",
89
- "--json",
90
- "number,title",
91
- "--limit",
92
- "5"
93
- ]);
94
- if (result.exitCode !== 0 || !result.stdout.trim()) {
95
- return null;
82
+ function loadExistingGitHubLinks(incrementPath) {
83
+ const metadataPath = path.join(incrementPath, "metadata.json");
84
+ if (!existsSync(metadataPath)) {
85
+ return { userStoryIssues: {} };
96
86
  }
97
87
  try {
98
- const issues = JSON.parse(result.stdout);
99
- if (issues.length > 0) {
100
- return issues[0].number;
101
- }
88
+ const metadata = JSON.parse(readFileSync(metadataPath, "utf-8"));
89
+ return {
90
+ milestone: metadata.github?.milestone,
91
+ userStoryIssues: metadata.github?.userStoryIssues || {}
92
+ };
102
93
  } catch {
94
+ return { userStoryIssues: {} };
103
95
  }
104
- return null;
105
96
  }
106
- async function createGitHubIssue(owner, repo, title, body, labels) {
107
- const args = [
97
+ async function updateIncrementMetadata(incrementPath, milestoneNumber, userStoryIssues) {
98
+ const metadataPath = path.join(incrementPath, "metadata.json");
99
+ let metadata = {};
100
+ if (existsSync(metadataPath)) {
101
+ try {
102
+ metadata = JSON.parse(readFileSync(metadataPath, "utf-8"));
103
+ } catch {
104
+ }
105
+ }
106
+ metadata.github = {
107
+ milestone: milestoneNumber,
108
+ userStoryIssues,
109
+ lastSync: (/* @__PURE__ */ new Date()).toISOString()
110
+ };
111
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2) + "\n");
112
+ }
113
+ async function createOrGetMilestone(owner, repo, featureId, title, existingMilestone) {
114
+ if (existingMilestone) {
115
+ const verifyResult = await execFileNoThrow("gh", [
116
+ "api",
117
+ `repos/${owner}/${repo}/milestones/${existingMilestone}`,
118
+ "--jq",
119
+ ".number"
120
+ ]);
121
+ if (verifyResult.exitCode === 0) {
122
+ return {
123
+ number: existingMilestone,
124
+ url: `https://github.com/${owner}/${repo}/milestone/${existingMilestone}`
125
+ };
126
+ }
127
+ }
128
+ const searchResult = await execFileNoThrow("gh", [
129
+ "api",
130
+ `repos/${owner}/${repo}/milestones`,
131
+ "--jq",
132
+ `.[] | select(.title | contains("${featureId}")) | .number`
133
+ ]);
134
+ if (searchResult.exitCode === 0 && searchResult.stdout.trim()) {
135
+ const milestoneNumber2 = parseInt(searchResult.stdout.trim().split("\n")[0], 10);
136
+ return {
137
+ number: milestoneNumber2,
138
+ url: `https://github.com/${owner}/${repo}/milestone/${milestoneNumber2}`
139
+ };
140
+ }
141
+ const milestoneTitle = `[${featureId}] ${title}`;
142
+ const createResult = await execFileNoThrow("gh", [
143
+ "api",
144
+ `repos/${owner}/${repo}/milestones`,
145
+ "-X",
146
+ "POST",
147
+ "-f",
148
+ `title=${milestoneTitle}`,
149
+ "-f",
150
+ `description=Feature milestone for ${featureId}`,
151
+ "--jq",
152
+ ".number"
153
+ ]);
154
+ if (createResult.exitCode !== 0) {
155
+ throw new Error(`Failed to create milestone: ${createResult.stderr || createResult.stdout}`);
156
+ }
157
+ const milestoneNumber = parseInt(createResult.stdout.trim(), 10);
158
+ return {
159
+ number: milestoneNumber,
160
+ url: `https://github.com/${owner}/${repo}/milestone/${milestoneNumber}`
161
+ };
162
+ }
163
+ function buildUserStoryIssueBody(story, tasks, incrementData, githubRepo) {
164
+ const incrementId = incrementData.frontmatter.increment;
165
+ let body = "";
166
+ body += `**Feature**: ${incrementData.frontmatter.feature_id || "N/A"}
167
+ `;
168
+ body += `**Status**: ${story.acceptanceCriteria.every((ac) => ac.completed) ? "complete" : "in-progress"}
169
+ `;
170
+ body += `**Priority**: ${story.priority || incrementData.frontmatter.priority || "P2"}
171
+ `;
172
+ body += `
173
+ ---
174
+
175
+ `;
176
+ body += `## User Story
177
+
178
+ `;
179
+ if (story.asA && story.iWant && story.soThat) {
180
+ body += `**As a** ${story.asA}
181
+ `;
182
+ body += `**I want** ${story.iWant}
183
+ `;
184
+ body += `**So that** ${story.soThat}
185
+
186
+ `;
187
+ } else {
188
+ body += `${story.title}
189
+
190
+ `;
191
+ }
192
+ body += `---
193
+
194
+ `;
195
+ body += `## Acceptance Criteria
196
+
197
+ `;
198
+ if (story.acceptanceCriteria.length > 0) {
199
+ const completed = story.acceptanceCriteria.filter((ac) => ac.completed).length;
200
+ const total = story.acceptanceCriteria.length;
201
+ const percentage = total > 0 ? Math.round(completed / total * 100) : 0;
202
+ body += `Progress: ${completed}/${total} criteria met (${percentage}%)
203
+
204
+ `;
205
+ for (const ac of story.acceptanceCriteria) {
206
+ const checkbox = ac.completed ? "[x]" : "[ ]";
207
+ body += `- ${checkbox} **${ac.id}**: ${ac.description}
208
+ `;
209
+ }
210
+ body += "\n";
211
+ } else {
212
+ body += `*No acceptance criteria defined*
213
+
214
+ `;
215
+ }
216
+ body += `---
217
+
218
+ `;
219
+ const storyTasks = tasks.filter(
220
+ (t) => t.userStories.includes(story.id) || t.userStories.some((us) => us.includes(story.id.replace("US-", "")))
221
+ );
222
+ if (storyTasks.length > 0) {
223
+ body += `## Implementation Tasks
224
+
225
+ `;
226
+ const completedTasks = storyTasks.filter((t) => t.completed).length;
227
+ const totalTasks = storyTasks.length;
228
+ const taskPercentage = totalTasks > 0 ? Math.round(completedTasks / totalTasks * 100) : 0;
229
+ body += `Progress: ${completedTasks}/${totalTasks} tasks (${taskPercentage}%)
230
+
231
+ `;
232
+ for (const task of storyTasks) {
233
+ const checkbox = task.completed ? "[x]" : "[ ]";
234
+ body += `- ${checkbox} **${task.id}**: ${task.title}
235
+ `;
236
+ }
237
+ body += "\n";
238
+ body += `---
239
+
240
+ `;
241
+ }
242
+ body += `## SpecWeave Increment
243
+
244
+ `;
245
+ body += `**Increment**: [${incrementId}](https://github.com/${githubRepo}/tree/develop/.specweave/increments/${incrementId})
246
+
247
+ `;
248
+ body += `---
249
+
250
+ `;
251
+ body += `\u{1F916} Auto-synced by SpecWeave`;
252
+ return body;
253
+ }
254
+ async function syncUserStoryIssue(owner, repo, featureId, story, tasks, incrementData, milestoneNumber, existingIssueNumber) {
255
+ const title = `[${featureId}][${story.id}] ${story.title}`;
256
+ const body = buildUserStoryIssueBody(story, tasks, incrementData, `${owner}/${repo}`);
257
+ const labels = ["specweave", "user-story"];
258
+ const priority = story.priority?.toLowerCase() || incrementData.frontmatter.priority?.toLowerCase() || "p2";
259
+ labels.push(priority);
260
+ if (existingIssueNumber) {
261
+ const updateResult = await execFileNoThrow("gh", [
262
+ "issue",
263
+ "edit",
264
+ String(existingIssueNumber),
265
+ "--repo",
266
+ `${owner}/${repo}`,
267
+ "--title",
268
+ title,
269
+ "--body",
270
+ body
271
+ ]);
272
+ if (updateResult.exitCode !== 0) {
273
+ throw new Error(`Failed to update issue #${existingIssueNumber}: ${updateResult.stderr}`);
274
+ }
275
+ return {
276
+ number: existingIssueNumber,
277
+ url: `https://github.com/${owner}/${repo}/issues/${existingIssueNumber}`
278
+ };
279
+ }
280
+ const createArgs = [
108
281
  "issue",
109
282
  "create",
110
283
  "--repo",
@@ -112,16 +285,18 @@ async function createGitHubIssue(owner, repo, title, body, labels) {
112
285
  "--title",
113
286
  title,
114
287
  "--body",
115
- body
288
+ body,
289
+ "--milestone",
290
+ String(milestoneNumber)
116
291
  ];
117
292
  if (labels.length > 0) {
118
- args.push("--label", labels.join(","));
293
+ createArgs.push("--label", labels.join(","));
119
294
  }
120
- const result = await execFileNoThrow("gh", args);
121
- if (result.exitCode !== 0) {
122
- throw new Error(`Failed to create issue: ${result.stderr || result.stdout}`);
295
+ const createResult = await execFileNoThrow("gh", createArgs);
296
+ if (createResult.exitCode !== 0) {
297
+ throw new Error(`Failed to create issue: ${createResult.stderr || createResult.stdout}`);
123
298
  }
124
- const urlMatch = result.stdout.match(/https:\/\/github\.com\/[^\s]+\/issues\/(\d+)/);
299
+ const urlMatch = createResult.stdout.match(/https:\/\/github\.com\/[^\s]+\/issues\/(\d+)/);
125
300
  if (!urlMatch) {
126
301
  throw new Error("Could not parse issue URL from gh output");
127
302
  }
@@ -130,84 +305,32 @@ async function createGitHubIssue(owner, repo, title, body, labels) {
130
305
  url: urlMatch[0]
131
306
  };
132
307
  }
133
- async function updateGitHubIssue(owner, repo, issueNumber, body) {
134
- const result = await execFileNoThrow("gh", [
135
- "issue",
136
- "edit",
137
- String(issueNumber),
138
- "--repo",
139
- `${owner}/${repo}`,
140
- "--body",
141
- body
142
- ]);
143
- if (result.exitCode !== 0) {
144
- throw new Error(`Failed to update issue: ${result.stderr || result.stdout}`);
145
- }
146
- }
147
- function loadExistingGitHubLink(incrementPath) {
148
- const metadataPath = path.join(incrementPath, "metadata.json");
149
- if (!existsSync(metadataPath)) {
150
- return null;
151
- }
152
- try {
153
- const metadata = JSON.parse(readFileSync(metadataPath, "utf-8"));
154
- if (metadata.github?.issue) {
155
- return {
156
- issue: metadata.github.issue,
157
- url: metadata.github.url
158
- };
159
- }
160
- if (metadata.sync?.issueNumber) {
161
- return {
162
- issue: metadata.sync.issueNumber,
163
- url: metadata.sync.issueUrl
164
- };
165
- }
166
- return null;
167
- } catch {
168
- return null;
169
- }
170
- }
171
- async function updateIncrementMetadata(incrementPath, issueNumber, issueUrl) {
172
- const metadataPath = path.join(incrementPath, "metadata.json");
173
- let metadata = {};
174
- if (existsSync(metadataPath)) {
175
- try {
176
- metadata = JSON.parse(readFileSync(metadataPath, "utf-8"));
177
- } catch {
178
- }
179
- }
180
- metadata.github = {
181
- issue: issueNumber,
182
- url: issueUrl,
183
- lastSync: (/* @__PURE__ */ new Date()).toISOString()
184
- };
185
- await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2) + "\n");
186
- }
187
308
  async function main() {
188
309
  const args = process.argv.slice(2);
189
310
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
190
311
  console.log("Usage: node github-increment-sync-cli.js <increment-id> [options]");
191
312
  console.log("");
313
+ console.log("Creates GitHub issues with CORRECT format:");
314
+ console.log(" - Milestone: [FS-XXX] Feature Title");
315
+ console.log(" - Issues: [FS-XXX][US-YYY] User Story Title (one per US)");
316
+ console.log("");
192
317
  console.log("Arguments:");
193
- console.log(" increment-id Increment ID (e.g., 0063 or 0063-fix-external-import)");
318
+ console.log(" increment-id Increment ID (e.g., 0002 or 0002-thumbnail-mvp)");
194
319
  console.log("");
195
320
  console.log("Options:");
196
- console.log(" --force Force create even if issue exists");
197
- console.log(" --dry-run Preview issue without creating");
321
+ console.log(" --dry-run Preview issues without creating");
198
322
  console.log("");
199
323
  console.log("Environment:");
200
324
  console.log(" GITHUB_TOKEN Required - GitHub personal access token");
201
325
  console.log("");
202
326
  console.log("Example:");
203
- console.log(" GITHUB_TOKEN=ghp_xxx node github-increment-sync-cli.js 0063");
327
+ console.log(" GITHUB_TOKEN=ghp_xxx node github-increment-sync-cli.js 0002");
204
328
  process.exit(args.length === 0 ? 1 : 0);
205
329
  }
206
330
  const incrementId = args[0];
207
- const force = args.includes("--force");
208
331
  const dryRun = args.includes("--dry-run");
209
332
  console.log(`
210
- \u{1F419} GitHub Increment Sync CLI`);
333
+ \u{1F419} GitHub Increment Sync CLI (Per-User-Story Mode)`);
211
334
  console.log(` Increment: ${incrementId}`);
212
335
  const incrementPath = await findIncrementFolder(incrementId);
213
336
  if (!incrementPath) {
@@ -238,6 +361,8 @@ async function main() {
238
361
  console.log(`
239
362
  \u{1F504} Parsing increment spec.md...`);
240
363
  const incrementData = await builder.parse();
364
+ const featureId = incrementData.frontmatter.feature_id || `FS-${fullIncrementId.match(/^(\d+)/)?.[1]?.padStart(3, "0") || "UNKNOWN"}`;
365
+ console.log(` \u{1F4E6} Feature: ${featureId}`);
241
366
  console.log(` \u{1F4E6} Title: ${incrementData.title}`);
242
367
  console.log(` \u{1F4DD} User Stories: ${incrementData.userStories.length}`);
243
368
  const totalACs = incrementData.userStories.reduce(
@@ -245,91 +370,79 @@ async function main() {
245
370
  0
246
371
  );
247
372
  console.log(` \u2713 Acceptance Criteria: ${totalACs}`);
248
- const githubRepo = config ? `${config.owner}/${config.repo}` : void 0;
249
- const issue = builder.buildIncrementIssue(incrementData, githubRepo);
373
+ console.log(` \u{1F527} Tasks: ${incrementData.tasks.length}`);
374
+ if (incrementData.userStories.length === 0) {
375
+ console.error(`
376
+ \u274C No user stories found in spec.md`);
377
+ console.error(" Ensure spec.md has ### US-XXX: Title sections");
378
+ process.exit(1);
379
+ }
250
380
  console.log(`
251
- \u{1F4CB} Issue Preview:`);
252
- console.log(` Title: ${issue.title}`);
253
- console.log(` Labels: ${issue.labels.join(", ")}`);
381
+ \u{1F4CB} Issues to Create/Update:`);
382
+ console.log(` \u{1F3AF} Milestone: [${featureId}] ${incrementData.title}`);
383
+ for (const story of incrementData.userStories) {
384
+ const storyTasks = incrementData.tasks.filter(
385
+ (t) => t.userStories.includes(story.id) || t.userStories.some((us) => us.includes(story.id.replace("US-", "")))
386
+ );
387
+ console.log(` \u{1F4DD} [${featureId}][${story.id}] ${story.title}`);
388
+ console.log(` \u2514\u2500 ${story.acceptanceCriteria.length} ACs, ${storyTasks.length} tasks`);
389
+ }
254
390
  if (dryRun) {
255
391
  console.log(`
256
- \u{1F4C4} Issue Body Preview:
392
+ \u{1F4C4} Sample Issue Body (${incrementData.userStories[0].id}):
257
393
  `);
258
- console.log(issue.body);
394
+ const sampleBody = buildUserStoryIssueBody(
395
+ incrementData.userStories[0],
396
+ incrementData.tasks,
397
+ incrementData,
398
+ config ? `${config.owner}/${config.repo}` : "owner/repo"
399
+ );
400
+ console.log(sampleBody);
259
401
  console.log(`
260
- \u2705 Dry run complete (no issue created)`);
402
+ \u2705 Dry run complete (no issues created)`);
261
403
  process.exit(0);
262
404
  }
263
405
  if (!config) {
264
406
  console.error("\u274C GitHub config not available");
265
407
  process.exit(1);
266
408
  }
267
- const metadataLink = loadExistingGitHubLink(incrementPath);
268
- if (metadataLink) {
269
- console.log(`
270
- \u{1F4CE} Found existing issue link in metadata: #${metadataLink.issue}`);
271
- console.log(`\u{1F504} Updating issue #${metadataLink.issue} with new format...`);
272
- await updateGitHubIssue(config.owner, config.repo, metadataLink.issue, issue.body);
273
- console.log(` \u2705 Body updated with User Stories and ACs`);
274
- const updateTitleResult = await execFileNoThrow("gh", [
275
- "issue",
276
- "edit",
277
- String(metadataLink.issue),
278
- "--repo",
279
- `${config.owner}/${config.repo}`,
280
- "--title",
281
- issue.title
282
- ]);
283
- if (updateTitleResult.exitCode === 0) {
284
- console.log(` \u2705 Title updated to: ${issue.title}`);
285
- } else {
286
- console.log(` \u26A0\uFE0F Could not update title (may need permissions)`);
287
- }
288
- await updateIncrementMetadata(
289
- incrementPath,
290
- metadataLink.issue,
291
- `https://github.com/${config.owner}/${config.repo}/issues/${metadataLink.issue}`
292
- );
293
- console.log(`
294
- \u2705 Sync complete!`);
295
- console.log(` \u{1F517} https://github.com/${config.owner}/${config.repo}/issues/${metadataLink.issue}`);
296
- process.exit(0);
297
- }
298
- const featureId = incrementData.frontmatter.feature_id || `FS-${fullIncrementId.match(/^(\d+)/)?.[1]?.padStart(3, "0") || "UNKNOWN"}`;
299
- console.log(`
300
- \u{1F50D} Searching GitHub for existing issue [${featureId}]...`);
301
- const existingIssue = await findExistingIssue(config.owner, config.repo, featureId);
302
- if (existingIssue) {
303
- console.log(` Found existing issue: #${existingIssue}`);
304
- console.log(`\u{1F504} Updating issue #${existingIssue}...`);
305
- await updateGitHubIssue(config.owner, config.repo, existingIssue, issue.body);
306
- console.log(` \u2705 Issue #${existingIssue} updated`);
307
- await updateIncrementMetadata(
308
- incrementPath,
309
- existingIssue,
310
- `https://github.com/${config.owner}/${config.repo}/issues/${existingIssue}`
311
- );
312
- console.log(`
313
- \u2705 Sync complete!`);
314
- console.log(` \u{1F517} https://github.com/${config.owner}/${config.repo}/issues/${existingIssue}`);
315
- process.exit(0);
316
- }
317
- console.log(` No existing issue found`);
409
+ const existingLinks = loadExistingGitHubLinks(incrementPath);
318
410
  console.log(`
319
- \u{1F680} Creating GitHub issue...`);
320
- const created = await createGitHubIssue(
411
+ \u{1F3AF} Creating/updating milestone...`);
412
+ const milestone = await createOrGetMilestone(
321
413
  config.owner,
322
414
  config.repo,
323
- issue.title,
324
- issue.body,
325
- issue.labels
415
+ featureId,
416
+ incrementData.title,
417
+ existingLinks.milestone
326
418
  );
327
- console.log(` \u2705 Issue #${created.number} created`);
328
- await updateIncrementMetadata(incrementPath, created.number, created.url);
329
- console.log(` \u{1F4DD} Metadata updated`);
419
+ console.log(` \u2705 Milestone #${milestone.number}: [${featureId}] ${incrementData.title}`);
420
+ console.log(`
421
+ \u{1F4DD} Creating/updating user story issues...`);
422
+ const userStoryIssues = {};
423
+ for (const story of incrementData.userStories) {
424
+ const existingIssue = existingLinks.userStoryIssues[story.id];
425
+ const issue = await syncUserStoryIssue(
426
+ config.owner,
427
+ config.repo,
428
+ featureId,
429
+ story,
430
+ incrementData.tasks,
431
+ incrementData,
432
+ milestone.number,
433
+ existingIssue
434
+ );
435
+ userStoryIssues[story.id] = issue.number;
436
+ const action = existingIssue ? "\u267B\uFE0F Updated" : "\u2705 Created";
437
+ console.log(` ${action} #${issue.number}: [${featureId}][${story.id}] ${story.title}`);
438
+ }
439
+ await updateIncrementMetadata(incrementPath, milestone.number, userStoryIssues);
440
+ console.log(`
441
+ \u{1F4DD} Metadata updated`);
330
442
  console.log(`
331
443
  \u2705 Sync complete!`);
332
- console.log(` \u{1F517} ${created.url}`);
444
+ console.log(` \u{1F3AF} Milestone: ${milestone.url}`);
445
+ console.log(` \u{1F4DD} Issues: ${Object.keys(userStoryIssues).length} user stories synced`);
333
446
  process.exit(0);
334
447
  } catch (error) {
335
448
  console.error(`