specweave 0.32.10 → 0.33.3

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 (224) hide show
  1. package/CLAUDE.md +162 -1
  2. package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts +120 -0
  3. package/dist/plugins/specweave-ado/lib/per-us-sync.d.ts.map +1 -0
  4. package/dist/plugins/specweave-ado/lib/per-us-sync.js +276 -0
  5. package/dist/plugins/specweave-ado/lib/per-us-sync.js.map +1 -0
  6. package/dist/plugins/specweave-github/lib/github-client-v2.d.ts +4 -1
  7. package/dist/plugins/specweave-github/lib/github-client-v2.d.ts.map +1 -1
  8. package/dist/plugins/specweave-github/lib/github-client-v2.js +13 -3
  9. package/dist/plugins/specweave-github/lib/github-client-v2.js.map +1 -1
  10. package/dist/plugins/specweave-github/lib/per-us-sync.d.ts +97 -0
  11. package/dist/plugins/specweave-github/lib/per-us-sync.d.ts.map +1 -0
  12. package/dist/plugins/specweave-github/lib/per-us-sync.js +274 -0
  13. package/dist/plugins/specweave-github/lib/per-us-sync.js.map +1 -0
  14. package/dist/plugins/specweave-jira/lib/per-us-sync.d.ts +113 -0
  15. package/dist/plugins/specweave-jira/lib/per-us-sync.d.ts.map +1 -0
  16. package/dist/plugins/specweave-jira/lib/per-us-sync.js +254 -0
  17. package/dist/plugins/specweave-jira/lib/per-us-sync.js.map +1 -0
  18. package/dist/src/cli/add-child-pid.d.ts +11 -0
  19. package/dist/src/cli/add-child-pid.d.ts.map +1 -0
  20. package/dist/src/cli/add-child-pid.js +42 -0
  21. package/dist/src/cli/add-child-pid.js.map +1 -0
  22. package/dist/src/cli/add-child-process.d.ts +15 -0
  23. package/dist/src/cli/add-child-process.d.ts.map +1 -0
  24. package/dist/src/cli/add-child-process.js +40 -0
  25. package/dist/src/cli/add-child-process.js.map +1 -0
  26. package/dist/src/cli/check-watchdog.d.ts +15 -0
  27. package/dist/src/cli/check-watchdog.d.ts.map +1 -0
  28. package/dist/src/cli/check-watchdog.js +47 -0
  29. package/dist/src/cli/check-watchdog.js.map +1 -0
  30. package/dist/src/cli/cleanup-zombies.d.ts +14 -0
  31. package/dist/src/cli/cleanup-zombies.d.ts.map +1 -0
  32. package/dist/src/cli/cleanup-zombies.js +268 -0
  33. package/dist/src/cli/cleanup-zombies.js.map +1 -0
  34. package/dist/src/cli/find-session-by-pid.d.ts +14 -0
  35. package/dist/src/cli/find-session-by-pid.d.ts.map +1 -0
  36. package/dist/src/cli/find-session-by-pid.js +45 -0
  37. package/dist/src/cli/find-session-by-pid.js.map +1 -0
  38. package/dist/src/cli/get-stale-sessions.d.ts +17 -0
  39. package/dist/src/cli/get-stale-sessions.d.ts.map +1 -0
  40. package/dist/src/cli/get-stale-sessions.js +36 -0
  41. package/dist/src/cli/get-stale-sessions.js.map +1 -0
  42. package/dist/src/cli/register-session.d.ts +16 -0
  43. package/dist/src/cli/register-session.d.ts.map +1 -0
  44. package/dist/src/cli/register-session.js +48 -0
  45. package/dist/src/cli/register-session.js.map +1 -0
  46. package/dist/src/cli/remove-session.d.ts +11 -0
  47. package/dist/src/cli/remove-session.d.ts.map +1 -0
  48. package/dist/src/cli/remove-session.js +36 -0
  49. package/dist/src/cli/remove-session.js.map +1 -0
  50. package/dist/src/cli/update-heartbeat.d.ts +11 -0
  51. package/dist/src/cli/update-heartbeat.d.ts.map +1 -0
  52. package/dist/src/cli/update-heartbeat.js +36 -0
  53. package/dist/src/cli/update-heartbeat.js.map +1 -0
  54. package/dist/src/config/types.d.ts +1208 -203
  55. package/dist/src/config/types.d.ts.map +1 -1
  56. package/dist/src/core/background/job-manager.d.ts +16 -0
  57. package/dist/src/core/background/job-manager.d.ts.map +1 -1
  58. package/dist/src/core/background/job-manager.js +110 -15
  59. package/dist/src/core/background/job-manager.js.map +1 -1
  60. package/dist/src/core/config/config-manager.d.ts.map +1 -1
  61. package/dist/src/core/config/config-manager.js +58 -0
  62. package/dist/src/core/config/config-manager.js.map +1 -1
  63. package/dist/src/core/config/types.d.ts +80 -0
  64. package/dist/src/core/config/types.d.ts.map +1 -1
  65. package/dist/src/core/config/types.js.map +1 -1
  66. package/dist/src/core/increment/increment-utils.d.ts +26 -1
  67. package/dist/src/core/increment/increment-utils.d.ts.map +1 -1
  68. package/dist/src/core/increment/increment-utils.js +66 -4
  69. package/dist/src/core/increment/increment-utils.js.map +1 -1
  70. package/dist/src/core/increment/status-change-sync-trigger.d.ts +3 -1
  71. package/dist/src/core/increment/status-change-sync-trigger.d.ts.map +1 -1
  72. package/dist/src/core/increment/status-change-sync-trigger.js +5 -2
  73. package/dist/src/core/increment/status-change-sync-trigger.js.map +1 -1
  74. package/dist/src/core/living-docs/cross-project-sync.d.ts +87 -15
  75. package/dist/src/core/living-docs/cross-project-sync.d.ts.map +1 -1
  76. package/dist/src/core/living-docs/cross-project-sync.js +147 -28
  77. package/dist/src/core/living-docs/cross-project-sync.js.map +1 -1
  78. package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.d.ts.map +1 -1
  79. package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.js +48 -12
  80. package/dist/src/core/living-docs/intelligent-analyzer/architecture-generator.js.map +1 -1
  81. package/dist/src/core/living-docs/intelligent-analyzer/cache-manager.d.ts +70 -0
  82. package/dist/src/core/living-docs/intelligent-analyzer/cache-manager.d.ts.map +1 -0
  83. package/dist/src/core/living-docs/intelligent-analyzer/cache-manager.js +188 -0
  84. package/dist/src/core/living-docs/intelligent-analyzer/cache-manager.js.map +1 -0
  85. package/dist/src/core/living-docs/intelligent-analyzer/dashboard-generator.d.ts +33 -0
  86. package/dist/src/core/living-docs/intelligent-analyzer/dashboard-generator.d.ts.map +1 -0
  87. package/dist/src/core/living-docs/intelligent-analyzer/dashboard-generator.js +290 -0
  88. package/dist/src/core/living-docs/intelligent-analyzer/dashboard-generator.js.map +1 -0
  89. package/dist/src/core/living-docs/intelligent-analyzer/deep-repo-analyzer.d.ts.map +1 -1
  90. package/dist/src/core/living-docs/intelligent-analyzer/deep-repo-analyzer.js +114 -11
  91. package/dist/src/core/living-docs/intelligent-analyzer/deep-repo-analyzer.js.map +1 -1
  92. package/dist/src/core/living-docs/intelligent-analyzer/graph-visualizer.d.ts +23 -0
  93. package/dist/src/core/living-docs/intelligent-analyzer/graph-visualizer.d.ts.map +1 -0
  94. package/dist/src/core/living-docs/intelligent-analyzer/graph-visualizer.js +283 -0
  95. package/dist/src/core/living-docs/intelligent-analyzer/graph-visualizer.js.map +1 -0
  96. package/dist/src/core/living-docs/intelligent-analyzer/mermaid-generator.d.ts +44 -0
  97. package/dist/src/core/living-docs/intelligent-analyzer/mermaid-generator.d.ts.map +1 -0
  98. package/dist/src/core/living-docs/intelligent-analyzer/mermaid-generator.js +61 -0
  99. package/dist/src/core/living-docs/intelligent-analyzer/mermaid-generator.js.map +1 -0
  100. package/dist/src/core/living-docs/intelligent-analyzer/orchestrator.d.ts +126 -0
  101. package/dist/src/core/living-docs/intelligent-analyzer/orchestrator.d.ts.map +1 -0
  102. package/dist/src/core/living-docs/intelligent-analyzer/orchestrator.js +378 -0
  103. package/dist/src/core/living-docs/intelligent-analyzer/orchestrator.js.map +1 -0
  104. package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.d.ts.map +1 -1
  105. package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js +57 -0
  106. package/dist/src/core/living-docs/intelligent-analyzer/organization-synthesizer.js.map +1 -1
  107. package/dist/src/core/living-docs/intelligent-analyzer/pattern-analyzer.d.ts +82 -0
  108. package/dist/src/core/living-docs/intelligent-analyzer/pattern-analyzer.d.ts.map +1 -0
  109. package/dist/src/core/living-docs/intelligent-analyzer/pattern-analyzer.js +430 -0
  110. package/dist/src/core/living-docs/intelligent-analyzer/pattern-analyzer.js.map +1 -0
  111. package/dist/src/core/living-docs/intelligent-analyzer/repo-scanner.d.ts +84 -0
  112. package/dist/src/core/living-docs/intelligent-analyzer/repo-scanner.d.ts.map +1 -0
  113. package/dist/src/core/living-docs/intelligent-analyzer/repo-scanner.js +387 -0
  114. package/dist/src/core/living-docs/intelligent-analyzer/repo-scanner.js.map +1 -0
  115. package/dist/src/core/living-docs/intelligent-analyzer/report-writer.d.ts +61 -0
  116. package/dist/src/core/living-docs/intelligent-analyzer/report-writer.d.ts.map +1 -0
  117. package/dist/src/core/living-docs/intelligent-analyzer/report-writer.js +174 -0
  118. package/dist/src/core/living-docs/intelligent-analyzer/report-writer.js.map +1 -0
  119. package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts +1 -1
  120. package/dist/src/core/living-docs/intelligent-analyzer/types.d.ts.map +1 -1
  121. package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
  122. package/dist/src/core/living-docs/living-docs-sync.js +26 -22
  123. package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
  124. package/dist/src/core/living-docs/module-analyzer.d.ts +3 -0
  125. package/dist/src/core/living-docs/module-analyzer.d.ts.map +1 -1
  126. package/dist/src/core/living-docs/module-analyzer.js +40 -1
  127. package/dist/src/core/living-docs/module-analyzer.js.map +1 -1
  128. package/dist/src/core/living-docs/types.d.ts +24 -3
  129. package/dist/src/core/living-docs/types.d.ts.map +1 -1
  130. package/dist/src/core/qa/qa-runner.js +1 -1
  131. package/dist/src/core/qa/qa-runner.js.map +1 -1
  132. package/dist/src/core/scheduler/session-sync-executor.js +1 -1
  133. package/dist/src/core/scheduler/session-sync-executor.js.map +1 -1
  134. package/dist/src/core/status-line/status-line-updater.d.ts +1 -1
  135. package/dist/src/core/status-line/status-line-updater.d.ts.map +1 -1
  136. package/dist/src/core/status-line/status-line-updater.js +4 -3
  137. package/dist/src/core/status-line/status-line-updater.js.map +1 -1
  138. package/dist/src/core/types/config.d.ts +79 -0
  139. package/dist/src/core/types/config.d.ts.map +1 -1
  140. package/dist/src/core/types/config.js.map +1 -1
  141. package/dist/src/importers/jira-importer.d.ts.map +1 -1
  142. package/dist/src/importers/jira-importer.js +18 -9
  143. package/dist/src/importers/jira-importer.js.map +1 -1
  144. package/dist/src/init/architecture/types.d.ts +140 -33
  145. package/dist/src/init/architecture/types.d.ts.map +1 -1
  146. package/dist/src/init/compliance/types.d.ts +27 -30
  147. package/dist/src/init/compliance/types.d.ts.map +1 -1
  148. package/dist/src/init/repo/types.d.ts +34 -11
  149. package/dist/src/init/repo/types.d.ts.map +1 -1
  150. package/dist/src/init/research/src/config/types.d.ts +82 -15
  151. package/dist/src/init/research/src/config/types.d.ts.map +1 -1
  152. package/dist/src/init/research/types.d.ts +93 -38
  153. package/dist/src/init/research/types.d.ts.map +1 -1
  154. package/dist/src/init/team/types.d.ts +42 -4
  155. package/dist/src/init/team/types.d.ts.map +1 -1
  156. package/dist/src/sync/ado-reconciler.js +1 -1
  157. package/dist/src/sync/ado-reconciler.js.map +1 -1
  158. package/dist/src/sync/github-reconciler.js +1 -1
  159. package/dist/src/sync/github-reconciler.js.map +1 -1
  160. package/dist/src/sync/jira-reconciler.js +1 -1
  161. package/dist/src/sync/jira-reconciler.js.map +1 -1
  162. package/dist/src/sync/sync-coordinator.d.ts +20 -0
  163. package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
  164. package/dist/src/sync/sync-coordinator.js +258 -33
  165. package/dist/src/sync/sync-coordinator.js.map +1 -1
  166. package/dist/src/types/session.d.ts +65 -0
  167. package/dist/src/types/session.d.ts.map +1 -0
  168. package/dist/src/types/session.js +8 -0
  169. package/dist/src/types/session.js.map +1 -0
  170. package/dist/src/utils/lock-manager.d.ts +48 -0
  171. package/dist/src/utils/lock-manager.d.ts.map +1 -0
  172. package/dist/src/utils/lock-manager.js +195 -0
  173. package/dist/src/utils/lock-manager.js.map +1 -0
  174. package/dist/src/utils/notification-manager.d.ts +45 -0
  175. package/dist/src/utils/notification-manager.d.ts.map +1 -0
  176. package/dist/src/utils/notification-manager.js +130 -0
  177. package/dist/src/utils/notification-manager.js.map +1 -0
  178. package/dist/src/utils/platform-utils.d.ts +136 -0
  179. package/dist/src/utils/platform-utils.d.ts.map +1 -0
  180. package/dist/src/utils/platform-utils.js +366 -0
  181. package/dist/src/utils/platform-utils.js.map +1 -0
  182. package/dist/src/utils/project-resolver.d.ts +156 -0
  183. package/dist/src/utils/project-resolver.d.ts.map +1 -0
  184. package/dist/src/utils/project-resolver.js +587 -0
  185. package/dist/src/utils/project-resolver.js.map +1 -0
  186. package/dist/src/utils/session-registry.d.ts +142 -0
  187. package/dist/src/utils/session-registry.d.ts.map +1 -0
  188. package/dist/src/utils/session-registry.js +480 -0
  189. package/dist/src/utils/session-registry.js.map +1 -0
  190. package/package.json +5 -2
  191. package/plugins/specweave/commands/specweave-living-docs.md +42 -0
  192. package/plugins/specweave/hooks/hooks.json +20 -0
  193. package/plugins/specweave/hooks/lib/update-active-increment.sh +2 -2
  194. package/plugins/specweave/hooks/lib/update-status-line.sh +1 -1
  195. package/plugins/specweave/hooks/post-increment-status-change.sh +3 -3
  196. package/plugins/specweave/hooks/post-metadata-change.sh +1 -1
  197. package/plugins/specweave/hooks/universal/hook-wrapper.cmd +26 -26
  198. package/plugins/specweave/hooks/universal/session-start.cmd +16 -16
  199. package/plugins/specweave/hooks/universal/session-start.ps1 +16 -16
  200. package/plugins/specweave/hooks/user-prompt-submit.sh +107 -5
  201. package/plugins/specweave/hooks/v2/guards/increment-root-guard.sh +61 -0
  202. package/plugins/specweave/hooks/v2/guards/per-us-project-validator.sh +281 -0
  203. package/plugins/specweave/hooks/v2/handlers/living-specs-handler.sh +29 -0
  204. package/plugins/specweave/hooks/v2/session-end.sh +69 -0
  205. package/plugins/specweave/hooks/v2/session-start.sh +81 -0
  206. package/plugins/specweave/lib/vendor/sync/github-reconciler.js +1 -1
  207. package/plugins/specweave/lib/vendor/sync/github-reconciler.js.map +1 -1
  208. package/plugins/specweave/scripts/heartbeat.sh +110 -0
  209. package/plugins/specweave/scripts/progress.js +34 -4
  210. package/plugins/specweave/scripts/read-jobs.sh +1 -1
  211. package/plugins/specweave/scripts/read-progress.sh +50 -5
  212. package/plugins/specweave/scripts/read-workflow.sh +1 -1
  213. package/plugins/specweave/scripts/session-watchdog.sh +65 -0
  214. package/plugins/specweave/scripts/status.js +28 -11
  215. package/plugins/specweave-ado/lib/per-us-sync.js +247 -0
  216. package/plugins/specweave-ado/lib/per-us-sync.ts +410 -0
  217. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +738 -0
  218. package/plugins/specweave-github/lib/github-client-v2.js +10 -3
  219. package/plugins/specweave-github/lib/github-client-v2.ts +15 -3
  220. package/plugins/specweave-github/lib/per-us-sync.js +241 -0
  221. package/plugins/specweave-github/lib/per-us-sync.ts +375 -0
  222. package/plugins/specweave-jira/lib/per-us-sync.js +224 -0
  223. package/plugins/specweave-jira/lib/per-us-sync.ts +366 -0
  224. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +1107 -0
@@ -291,10 +291,13 @@ ${body}`;
291
291
  * Search for issue by exact title match
292
292
  *
293
293
  * IDEMPOTENCY: Use this before creating issues to prevent duplicates
294
+ *
295
+ * @param title - Title pattern to search for (e.g., "[FS-136][US-001]")
296
+ * @param includeClosedIssues - If true, searches all issues (open+closed). Default: false (open only)
294
297
  */
295
- async searchIssueByTitle(title) {
298
+ async searchIssueByTitle(title, includeClosedIssues = false) {
296
299
  const escapedTitle = title.replace(/"/g, '\\"');
297
- const result = await execFileNoThrow("gh", [
300
+ const args = [
298
301
  "issue",
299
302
  "list",
300
303
  "--repo",
@@ -306,7 +309,11 @@ ${body}`;
306
309
  "--limit",
307
310
  "50"
308
311
  // ✅ FIX: Increased from 1 to 50 to catch duplicates (Issue #0047)
309
- ]);
312
+ ];
313
+ if (includeClosedIssues) {
314
+ args.push("--state", "all");
315
+ }
316
+ const result = await execFileNoThrow("gh", args);
310
317
  if (result.exitCode !== 0) {
311
318
  return null;
312
319
  }
@@ -392,12 +392,15 @@ export class GitHubClientV2 {
392
392
  * Search for issue by exact title match
393
393
  *
394
394
  * IDEMPOTENCY: Use this before creating issues to prevent duplicates
395
+ *
396
+ * @param title - Title pattern to search for (e.g., "[FS-136][US-001]")
397
+ * @param includeClosedIssues - If true, searches all issues (open+closed). Default: false (open only)
395
398
  */
396
- async searchIssueByTitle(title: string): Promise<GitHubIssue | null> {
399
+ async searchIssueByTitle(title: string, includeClosedIssues: boolean = false): Promise<GitHubIssue | null> {
397
400
  // Escape double quotes in title for gh search
398
401
  const escapedTitle = title.replace(/"/g, '\\"');
399
402
 
400
- const result = await execFileNoThrow('gh', [
403
+ const args = [
401
404
  'issue',
402
405
  'list',
403
406
  '--repo',
@@ -408,7 +411,16 @@ export class GitHubClientV2 {
408
411
  'number,title,state,url,labels',
409
412
  '--limit',
410
413
  '50', // ✅ FIX: Increased from 1 to 50 to catch duplicates (Issue #0047)
411
- ]);
414
+ ];
415
+
416
+ // v0.34.0: Add --state all to search closed issues too (for closure flow)
417
+ // Without this, closeGitHubIssuesForUserStories() can't find already-closed issues
418
+ // and reports "No GitHub issue found" instead of "already closed"
419
+ if (includeClosedIssues) {
420
+ args.push('--state', 'all');
421
+ }
422
+
423
+ const result = await execFileNoThrow('gh', args);
412
424
 
413
425
  if (result.exitCode !== 0) {
414
426
  // Search failed, return null (treat as not found)
@@ -0,0 +1,241 @@
1
+ import { Octokit } from "@octokit/rest";
2
+ import { consoleLogger } from "../../../src/utils/logger.js";
3
+ class PerUSGitHubSync {
4
+ constructor(token, projectMappings, options = {}) {
5
+ this.token = token;
6
+ this.projectMappings = projectMappings;
7
+ this.octokit = new Octokit({ auth: token });
8
+ this.logger = options.logger ?? consoleLogger;
9
+ }
10
+ /**
11
+ * Sync all user stories to their respective GitHub repos
12
+ *
13
+ * @param userStories - User stories with explicit project fields
14
+ * @param featureId - Feature ID (e.g., "FS-137")
15
+ * @param options - Sync options
16
+ */
17
+ async syncUserStories(userStories, featureId, options = {}) {
18
+ const synced = [];
19
+ const failed = [];
20
+ const externalRefs = {};
21
+ const groups = this.groupByProject(userStories, options.defaultProject);
22
+ this.logger.log(`\u{1F4E1} Per-US GitHub Sync: ${userStories.length} USs across ${groups.size} projects`);
23
+ for (const [projectId, stories] of groups) {
24
+ const mapping = this.projectMappings[projectId]?.github;
25
+ if (!mapping) {
26
+ this.logger.warn(` \u26A0\uFE0F No GitHub mapping for project "${projectId}" - skipping ${stories.length} USs`);
27
+ for (const story of stories) {
28
+ failed.push({
29
+ usId: story.id,
30
+ projectId,
31
+ repo: "N/A",
32
+ issueNumber: 0,
33
+ url: "",
34
+ action: "skipped",
35
+ error: `No GitHub mapping for project "${projectId}"`
36
+ });
37
+ }
38
+ continue;
39
+ }
40
+ for (const story of stories) {
41
+ try {
42
+ const result = await this.syncUserStory(story, mapping, featureId, options);
43
+ synced.push({
44
+ ...result,
45
+ projectId
46
+ });
47
+ if (!options.dryRun && result.action !== "skipped") {
48
+ externalRefs[story.id] = {
49
+ github: {
50
+ provider: "github",
51
+ issueNumber: result.issueNumber,
52
+ url: result.url,
53
+ targetProject: projectId,
54
+ lastSynced: (/* @__PURE__ */ new Date()).toISOString()
55
+ }
56
+ };
57
+ }
58
+ } catch (error) {
59
+ failed.push({
60
+ usId: story.id,
61
+ projectId,
62
+ repo: `${mapping.owner}/${mapping.repo}`,
63
+ issueNumber: 0,
64
+ url: "",
65
+ action: "skipped",
66
+ error: error instanceof Error ? error.message : String(error)
67
+ });
68
+ }
69
+ }
70
+ }
71
+ const created = synced.filter((r) => r.action === "created").length;
72
+ const updated = synced.filter((r) => r.action === "updated").length;
73
+ const skipped = synced.filter((r) => r.action === "skipped").length;
74
+ return {
75
+ success: failed.length === 0,
76
+ synced,
77
+ failed,
78
+ externalRefs,
79
+ summary: {
80
+ total: userStories.length,
81
+ created,
82
+ updated,
83
+ skipped,
84
+ failed: failed.length
85
+ }
86
+ };
87
+ }
88
+ /**
89
+ * Sync a single user story to GitHub
90
+ */
91
+ async syncUserStory(story, mapping, featureId, options) {
92
+ const title = `[${featureId}][${story.id}] ${story.title}`;
93
+ const body = this.buildIssueBody(story, featureId);
94
+ if (options.dryRun) {
95
+ this.logger.log(` \u{1F50D} [DRY-RUN] Would sync ${story.id} to ${mapping.owner}/${mapping.repo}`);
96
+ return {
97
+ usId: story.id,
98
+ projectId: story.project || "unknown",
99
+ repo: `${mapping.owner}/${mapping.repo}`,
100
+ issueNumber: 0,
101
+ url: "",
102
+ action: "skipped"
103
+ };
104
+ }
105
+ const existingIssue = await this.findExistingIssue(mapping, story.id);
106
+ if (existingIssue) {
107
+ const response = await this.octokit.issues.update({
108
+ owner: mapping.owner,
109
+ repo: mapping.repo,
110
+ issue_number: existingIssue.number,
111
+ title,
112
+ body
113
+ });
114
+ this.logger.log(` \u{1F504} Updated ${story.id} \u2192 ${mapping.owner}/${mapping.repo}#${response.data.number}`);
115
+ return {
116
+ usId: story.id,
117
+ projectId: story.project || "unknown",
118
+ repo: `${mapping.owner}/${mapping.repo}`,
119
+ issueNumber: response.data.number,
120
+ url: response.data.html_url,
121
+ action: "updated"
122
+ };
123
+ } else {
124
+ const response = await this.octokit.issues.create({
125
+ owner: mapping.owner,
126
+ repo: mapping.repo,
127
+ title,
128
+ body,
129
+ labels: ["specweave", "user-story"]
130
+ });
131
+ this.logger.log(` \u2705 Created ${story.id} \u2192 ${mapping.owner}/${mapping.repo}#${response.data.number}`);
132
+ return {
133
+ usId: story.id,
134
+ projectId: story.project || "unknown",
135
+ repo: `${mapping.owner}/${mapping.repo}`,
136
+ issueNumber: response.data.number,
137
+ url: response.data.html_url,
138
+ action: "created"
139
+ };
140
+ }
141
+ }
142
+ /**
143
+ * Find existing issue by US ID in title
144
+ */
145
+ async findExistingIssue(mapping, usId) {
146
+ try {
147
+ const response = await this.octokit.issues.listForRepo({
148
+ owner: mapping.owner,
149
+ repo: mapping.repo,
150
+ labels: "specweave",
151
+ state: "all",
152
+ per_page: 100
153
+ });
154
+ const existing = response.data.find(
155
+ (issue) => issue.title.includes(`[${usId}]`)
156
+ );
157
+ return existing ? { number: existing.number } : null;
158
+ } catch {
159
+ return null;
160
+ }
161
+ }
162
+ /**
163
+ * Build issue body from user story
164
+ */
165
+ buildIssueBody(story, featureId) {
166
+ const lines = [];
167
+ lines.push(`# ${story.title}`);
168
+ lines.push("");
169
+ if (story.description) {
170
+ lines.push(story.description);
171
+ lines.push("");
172
+ }
173
+ if (story.acceptanceCriteria && story.acceptanceCriteria.length > 0) {
174
+ lines.push("## Acceptance Criteria");
175
+ lines.push("");
176
+ for (const ac of story.acceptanceCriteria) {
177
+ lines.push(`- [ ] ${ac}`);
178
+ }
179
+ lines.push("");
180
+ }
181
+ lines.push("---");
182
+ lines.push("");
183
+ lines.push(`**Feature**: ${featureId}`);
184
+ lines.push(`**User Story**: ${story.id}`);
185
+ if (story.project) {
186
+ lines.push(`**Project**: ${story.project}`);
187
+ }
188
+ if (story.board) {
189
+ lines.push(`**Board**: ${story.board}`);
190
+ }
191
+ lines.push("");
192
+ lines.push("\u{1F916} Auto-generated by SpecWeave");
193
+ return lines.join("\n");
194
+ }
195
+ /**
196
+ * Group user stories by their explicit project field
197
+ */
198
+ groupByProject(userStories, defaultProject) {
199
+ const groups = /* @__PURE__ */ new Map();
200
+ for (const story of userStories) {
201
+ const project = story.project || defaultProject || "default";
202
+ if (!groups.has(project)) {
203
+ groups.set(project, []);
204
+ }
205
+ groups.get(project).push(story);
206
+ }
207
+ return groups;
208
+ }
209
+ }
210
+ function formatPerUSSyncResults(result) {
211
+ const lines = [];
212
+ lines.push("");
213
+ lines.push("\u{1F4CA} Per-US GitHub Sync Results");
214
+ lines.push("");
215
+ const byProject = /* @__PURE__ */ new Map();
216
+ for (const r of [...result.synced, ...result.failed]) {
217
+ const existing = byProject.get(r.projectId) || [];
218
+ existing.push(r);
219
+ byProject.set(r.projectId, existing);
220
+ }
221
+ for (const [projectId, results] of byProject) {
222
+ lines.push(`**${projectId}**:`);
223
+ for (const r of results) {
224
+ const icon = r.action === "created" ? "\u2705" : r.action === "updated" ? "\u{1F504}" : r.error ? "\u274C" : "\u23ED\uFE0F";
225
+ if (r.issueNumber > 0) {
226
+ lines.push(` ${icon} ${r.usId} \u2192 ${r.repo}#${r.issueNumber}`);
227
+ } else if (r.error) {
228
+ lines.push(` ${icon} ${r.usId}: ${r.error}`);
229
+ } else {
230
+ lines.push(` ${icon} ${r.usId} (${r.action})`);
231
+ }
232
+ }
233
+ lines.push("");
234
+ }
235
+ lines.push(`\u{1F4C8} Summary: ${result.summary.created} created, ${result.summary.updated} updated, ${result.summary.failed} failed`);
236
+ return lines.join("\n");
237
+ }
238
+ export {
239
+ PerUSGitHubSync,
240
+ formatPerUSSyncResults
241
+ };
@@ -0,0 +1,375 @@
1
+ /**
2
+ * Per-US GitHub Sync (v0.34.0+)
3
+ *
4
+ * Syncs each User Story to its explicitly declared project's GitHub repo.
5
+ * Uses the **Project**: field in spec.md (NOT keyword-based classification).
6
+ *
7
+ * Key difference from multi-project-sync:
8
+ * - Multi-project sync uses keyword/heuristic classification
9
+ * - Per-US sync uses EXPLICIT **Project**: field from spec.md
10
+ *
11
+ * @module per-us-sync
12
+ * @since v0.34.0
13
+ */
14
+
15
+ import { Octokit } from '@octokit/rest';
16
+ import type { UserStoryData } from '../../../src/core/living-docs/types.js';
17
+ import type { ProjectMappings, GitHubMapping } from '../../../src/core/types/config.js';
18
+ import type { USExternalRef, USExternalRefsMap } from '../../../src/core/types/increment-metadata.js';
19
+ import { Logger, consoleLogger } from '../../../src/utils/logger.js';
20
+
21
+ /**
22
+ * Result of syncing a single US to GitHub
23
+ */
24
+ export interface USSyncResult {
25
+ usId: string;
26
+ projectId: string;
27
+ repo: string;
28
+ issueNumber: number;
29
+ url: string;
30
+ action: 'created' | 'updated' | 'skipped';
31
+ error?: string;
32
+ }
33
+
34
+ /**
35
+ * Result of syncing all USs in an increment
36
+ */
37
+ export interface PerUSSyncResult {
38
+ success: boolean;
39
+ synced: USSyncResult[];
40
+ failed: USSyncResult[];
41
+ externalRefs: USExternalRefsMap;
42
+ summary: {
43
+ total: number;
44
+ created: number;
45
+ updated: number;
46
+ skipped: number;
47
+ failed: number;
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Options for per-US sync
53
+ */
54
+ export interface PerUSSyncOptions {
55
+ dryRun?: boolean;
56
+ force?: boolean;
57
+ defaultProject?: string;
58
+ logger?: Logger;
59
+ }
60
+
61
+ /**
62
+ * Per-US GitHub Sync
63
+ *
64
+ * Syncs each US to its declared project's GitHub repository.
65
+ */
66
+ export class PerUSGitHubSync {
67
+ private token: string;
68
+ private projectMappings: ProjectMappings;
69
+ private octokit: Octokit;
70
+ private logger: Logger;
71
+
72
+ constructor(
73
+ token: string,
74
+ projectMappings: ProjectMappings,
75
+ options: { logger?: Logger } = {}
76
+ ) {
77
+ this.token = token;
78
+ this.projectMappings = projectMappings;
79
+ this.octokit = new Octokit({ auth: token });
80
+ this.logger = options.logger ?? consoleLogger;
81
+ }
82
+
83
+ /**
84
+ * Sync all user stories to their respective GitHub repos
85
+ *
86
+ * @param userStories - User stories with explicit project fields
87
+ * @param featureId - Feature ID (e.g., "FS-137")
88
+ * @param options - Sync options
89
+ */
90
+ async syncUserStories(
91
+ userStories: UserStoryData[],
92
+ featureId: string,
93
+ options: PerUSSyncOptions = {}
94
+ ): Promise<PerUSSyncResult> {
95
+ const synced: USSyncResult[] = [];
96
+ const failed: USSyncResult[] = [];
97
+ const externalRefs: USExternalRefsMap = {};
98
+
99
+ // Group USs by their declared project
100
+ const groups = this.groupByProject(userStories, options.defaultProject);
101
+
102
+ this.logger.log(`📡 Per-US GitHub Sync: ${userStories.length} USs across ${groups.size} projects`);
103
+
104
+ for (const [projectId, stories] of groups) {
105
+ // Get GitHub mapping for this project
106
+ const mapping = this.projectMappings[projectId]?.github;
107
+
108
+ if (!mapping) {
109
+ // No GitHub mapping for this project
110
+ this.logger.warn(` ⚠️ No GitHub mapping for project "${projectId}" - skipping ${stories.length} USs`);
111
+ for (const story of stories) {
112
+ failed.push({
113
+ usId: story.id,
114
+ projectId,
115
+ repo: 'N/A',
116
+ issueNumber: 0,
117
+ url: '',
118
+ action: 'skipped',
119
+ error: `No GitHub mapping for project "${projectId}"`
120
+ });
121
+ }
122
+ continue;
123
+ }
124
+
125
+ // Sync each US to this project's repo
126
+ for (const story of stories) {
127
+ try {
128
+ const result = await this.syncUserStory(story, mapping, featureId, options);
129
+ synced.push({
130
+ ...result,
131
+ projectId
132
+ });
133
+
134
+ // Build external ref
135
+ if (!options.dryRun && result.action !== 'skipped') {
136
+ externalRefs[story.id] = {
137
+ github: {
138
+ provider: 'github',
139
+ issueNumber: result.issueNumber,
140
+ url: result.url,
141
+ targetProject: projectId,
142
+ lastSynced: new Date().toISOString()
143
+ }
144
+ };
145
+ }
146
+ } catch (error) {
147
+ failed.push({
148
+ usId: story.id,
149
+ projectId,
150
+ repo: `${mapping.owner}/${mapping.repo}`,
151
+ issueNumber: 0,
152
+ url: '',
153
+ action: 'skipped',
154
+ error: error instanceof Error ? error.message : String(error)
155
+ });
156
+ }
157
+ }
158
+ }
159
+
160
+ // Calculate summary
161
+ const created = synced.filter(r => r.action === 'created').length;
162
+ const updated = synced.filter(r => r.action === 'updated').length;
163
+ const skipped = synced.filter(r => r.action === 'skipped').length;
164
+
165
+ return {
166
+ success: failed.length === 0,
167
+ synced,
168
+ failed,
169
+ externalRefs,
170
+ summary: {
171
+ total: userStories.length,
172
+ created,
173
+ updated,
174
+ skipped,
175
+ failed: failed.length
176
+ }
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Sync a single user story to GitHub
182
+ */
183
+ private async syncUserStory(
184
+ story: UserStoryData,
185
+ mapping: GitHubMapping,
186
+ featureId: string,
187
+ options: PerUSSyncOptions
188
+ ): Promise<USSyncResult> {
189
+ const title = `[${featureId}][${story.id}] ${story.title}`;
190
+ const body = this.buildIssueBody(story, featureId);
191
+
192
+ if (options.dryRun) {
193
+ this.logger.log(` 🔍 [DRY-RUN] Would sync ${story.id} to ${mapping.owner}/${mapping.repo}`);
194
+ return {
195
+ usId: story.id,
196
+ projectId: story.project || 'unknown',
197
+ repo: `${mapping.owner}/${mapping.repo}`,
198
+ issueNumber: 0,
199
+ url: '',
200
+ action: 'skipped'
201
+ };
202
+ }
203
+
204
+ // Check for existing issue
205
+ const existingIssue = await this.findExistingIssue(mapping, story.id);
206
+
207
+ if (existingIssue) {
208
+ // Update existing issue
209
+ const response = await this.octokit.issues.update({
210
+ owner: mapping.owner,
211
+ repo: mapping.repo,
212
+ issue_number: existingIssue.number,
213
+ title,
214
+ body
215
+ });
216
+
217
+ this.logger.log(` 🔄 Updated ${story.id} → ${mapping.owner}/${mapping.repo}#${response.data.number}`);
218
+
219
+ return {
220
+ usId: story.id,
221
+ projectId: story.project || 'unknown',
222
+ repo: `${mapping.owner}/${mapping.repo}`,
223
+ issueNumber: response.data.number,
224
+ url: response.data.html_url,
225
+ action: 'updated'
226
+ };
227
+ } else {
228
+ // Create new issue
229
+ const response = await this.octokit.issues.create({
230
+ owner: mapping.owner,
231
+ repo: mapping.repo,
232
+ title,
233
+ body,
234
+ labels: ['specweave', 'user-story']
235
+ });
236
+
237
+ this.logger.log(` ✅ Created ${story.id} → ${mapping.owner}/${mapping.repo}#${response.data.number}`);
238
+
239
+ return {
240
+ usId: story.id,
241
+ projectId: story.project || 'unknown',
242
+ repo: `${mapping.owner}/${mapping.repo}`,
243
+ issueNumber: response.data.number,
244
+ url: response.data.html_url,
245
+ action: 'created'
246
+ };
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Find existing issue by US ID in title
252
+ */
253
+ private async findExistingIssue(
254
+ mapping: GitHubMapping,
255
+ usId: string
256
+ ): Promise<{ number: number } | null> {
257
+ try {
258
+ const response = await this.octokit.issues.listForRepo({
259
+ owner: mapping.owner,
260
+ repo: mapping.repo,
261
+ labels: 'specweave',
262
+ state: 'all',
263
+ per_page: 100
264
+ });
265
+
266
+ const existing = response.data.find(issue =>
267
+ issue.title.includes(`[${usId}]`)
268
+ );
269
+
270
+ return existing ? { number: existing.number } : null;
271
+ } catch {
272
+ return null;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Build issue body from user story
278
+ */
279
+ private buildIssueBody(story: UserStoryData, featureId: string): string {
280
+ const lines: string[] = [];
281
+
282
+ lines.push(`# ${story.title}`);
283
+ lines.push('');
284
+
285
+ if (story.description) {
286
+ lines.push(story.description);
287
+ lines.push('');
288
+ }
289
+
290
+ if (story.acceptanceCriteria && story.acceptanceCriteria.length > 0) {
291
+ lines.push('## Acceptance Criteria');
292
+ lines.push('');
293
+ for (const ac of story.acceptanceCriteria) {
294
+ lines.push(`- [ ] ${ac}`);
295
+ }
296
+ lines.push('');
297
+ }
298
+
299
+ lines.push('---');
300
+ lines.push('');
301
+ lines.push(`**Feature**: ${featureId}`);
302
+ lines.push(`**User Story**: ${story.id}`);
303
+ if (story.project) {
304
+ lines.push(`**Project**: ${story.project}`);
305
+ }
306
+ if (story.board) {
307
+ lines.push(`**Board**: ${story.board}`);
308
+ }
309
+ lines.push('');
310
+ lines.push('🤖 Auto-generated by SpecWeave');
311
+
312
+ return lines.join('\n');
313
+ }
314
+
315
+ /**
316
+ * Group user stories by their explicit project field
317
+ */
318
+ private groupByProject(
319
+ userStories: UserStoryData[],
320
+ defaultProject?: string
321
+ ): Map<string, UserStoryData[]> {
322
+ const groups = new Map<string, UserStoryData[]>();
323
+
324
+ for (const story of userStories) {
325
+ const project = story.project || defaultProject || 'default';
326
+
327
+ if (!groups.has(project)) {
328
+ groups.set(project, []);
329
+ }
330
+ groups.get(project)!.push(story);
331
+ }
332
+
333
+ return groups;
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Format per-US sync results for display
339
+ */
340
+ export function formatPerUSSyncResults(result: PerUSSyncResult): string {
341
+ const lines: string[] = [];
342
+
343
+ lines.push('');
344
+ lines.push('📊 Per-US GitHub Sync Results');
345
+ lines.push('');
346
+
347
+ // Group by project
348
+ const byProject = new Map<string, USSyncResult[]>();
349
+ for (const r of [...result.synced, ...result.failed]) {
350
+ const existing = byProject.get(r.projectId) || [];
351
+ existing.push(r);
352
+ byProject.set(r.projectId, existing);
353
+ }
354
+
355
+ for (const [projectId, results] of byProject) {
356
+ lines.push(`**${projectId}**:`);
357
+ for (const r of results) {
358
+ const icon = r.action === 'created' ? '✅' :
359
+ r.action === 'updated' ? '🔄' :
360
+ r.error ? '❌' : '⏭️';
361
+ if (r.issueNumber > 0) {
362
+ lines.push(` ${icon} ${r.usId} → ${r.repo}#${r.issueNumber}`);
363
+ } else if (r.error) {
364
+ lines.push(` ${icon} ${r.usId}: ${r.error}`);
365
+ } else {
366
+ lines.push(` ${icon} ${r.usId} (${r.action})`);
367
+ }
368
+ }
369
+ lines.push('');
370
+ }
371
+
372
+ lines.push(`📈 Summary: ${result.summary.created} created, ${result.summary.updated} updated, ${result.summary.failed} failed`);
373
+
374
+ return lines.join('\n');
375
+ }