claude-autopm 2.7.0 → 2.8.2

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 (281) hide show
  1. package/README.md +307 -56
  2. package/autopm/.claude/.env +158 -0
  3. package/autopm/.claude/settings.local.json +9 -0
  4. package/bin/autopm.js +11 -2
  5. package/bin/commands/epic.js +23 -3
  6. package/bin/commands/plugin.js +395 -0
  7. package/bin/commands/team.js +184 -10
  8. package/install/install.js +223 -4
  9. package/lib/cli/commands/issue.js +360 -20
  10. package/lib/plugins/PluginManager.js +1328 -0
  11. package/lib/plugins/PluginManager.old.js +400 -0
  12. package/lib/providers/AzureDevOpsProvider.js +575 -0
  13. package/lib/providers/GitHubProvider.js +475 -0
  14. package/lib/services/EpicService.js +1092 -3
  15. package/lib/services/IssueService.js +991 -0
  16. package/package.json +9 -1
  17. package/scripts/publish-plugins.sh +166 -0
  18. package/autopm/.claude/agents/cloud/README.md +0 -55
  19. package/autopm/.claude/agents/cloud/aws-cloud-architect.md +0 -521
  20. package/autopm/.claude/agents/cloud/azure-cloud-architect.md +0 -436
  21. package/autopm/.claude/agents/cloud/gcp-cloud-architect.md +0 -385
  22. package/autopm/.claude/agents/cloud/gcp-cloud-functions-engineer.md +0 -306
  23. package/autopm/.claude/agents/cloud/gemini-api-expert.md +0 -880
  24. package/autopm/.claude/agents/cloud/kubernetes-orchestrator.md +0 -566
  25. package/autopm/.claude/agents/cloud/openai-python-expert.md +0 -1087
  26. package/autopm/.claude/agents/cloud/terraform-infrastructure-expert.md +0 -454
  27. package/autopm/.claude/agents/core/agent-manager.md +0 -296
  28. package/autopm/.claude/agents/core/code-analyzer.md +0 -131
  29. package/autopm/.claude/agents/core/file-analyzer.md +0 -162
  30. package/autopm/.claude/agents/core/test-runner.md +0 -200
  31. package/autopm/.claude/agents/data/airflow-orchestration-expert.md +0 -52
  32. package/autopm/.claude/agents/data/kedro-pipeline-expert.md +0 -50
  33. package/autopm/.claude/agents/data/langgraph-workflow-expert.md +0 -520
  34. package/autopm/.claude/agents/databases/README.md +0 -50
  35. package/autopm/.claude/agents/databases/bigquery-expert.md +0 -392
  36. package/autopm/.claude/agents/databases/cosmosdb-expert.md +0 -368
  37. package/autopm/.claude/agents/databases/mongodb-expert.md +0 -398
  38. package/autopm/.claude/agents/databases/postgresql-expert.md +0 -321
  39. package/autopm/.claude/agents/databases/redis-expert.md +0 -52
  40. package/autopm/.claude/agents/devops/README.md +0 -52
  41. package/autopm/.claude/agents/devops/azure-devops-specialist.md +0 -308
  42. package/autopm/.claude/agents/devops/docker-containerization-expert.md +0 -298
  43. package/autopm/.claude/agents/devops/github-operations-specialist.md +0 -335
  44. package/autopm/.claude/agents/devops/mcp-context-manager.md +0 -319
  45. package/autopm/.claude/agents/devops/observability-engineer.md +0 -574
  46. package/autopm/.claude/agents/devops/ssh-operations-expert.md +0 -1093
  47. package/autopm/.claude/agents/devops/traefik-proxy-expert.md +0 -444
  48. package/autopm/.claude/agents/frameworks/README.md +0 -64
  49. package/autopm/.claude/agents/frameworks/e2e-test-engineer.md +0 -360
  50. package/autopm/.claude/agents/frameworks/nats-messaging-expert.md +0 -254
  51. package/autopm/.claude/agents/frameworks/react-frontend-engineer.md +0 -217
  52. package/autopm/.claude/agents/frameworks/react-ui-expert.md +0 -226
  53. package/autopm/.claude/agents/frameworks/tailwindcss-expert.md +0 -770
  54. package/autopm/.claude/agents/frameworks/ux-design-expert.md +0 -244
  55. package/autopm/.claude/agents/integration/message-queue-engineer.md +0 -794
  56. package/autopm/.claude/agents/languages/README.md +0 -50
  57. package/autopm/.claude/agents/languages/bash-scripting-expert.md +0 -541
  58. package/autopm/.claude/agents/languages/javascript-frontend-engineer.md +0 -197
  59. package/autopm/.claude/agents/languages/nodejs-backend-engineer.md +0 -226
  60. package/autopm/.claude/agents/languages/python-backend-engineer.md +0 -214
  61. package/autopm/.claude/agents/languages/python-backend-expert.md +0 -289
  62. package/autopm/.claude/agents/testing/frontend-testing-engineer.md +0 -395
  63. package/autopm/.claude/commands/ai/langgraph-workflow.md +0 -65
  64. package/autopm/.claude/commands/ai/openai-chat.md +0 -65
  65. package/autopm/.claude/commands/azure/COMMANDS.md +0 -107
  66. package/autopm/.claude/commands/azure/COMMAND_MAPPING.md +0 -252
  67. package/autopm/.claude/commands/azure/INTEGRATION_FIX.md +0 -103
  68. package/autopm/.claude/commands/azure/README.md +0 -246
  69. package/autopm/.claude/commands/azure/active-work.md +0 -198
  70. package/autopm/.claude/commands/azure/aliases.md +0 -143
  71. package/autopm/.claude/commands/azure/blocked-items.md +0 -287
  72. package/autopm/.claude/commands/azure/clean.md +0 -93
  73. package/autopm/.claude/commands/azure/docs-query.md +0 -48
  74. package/autopm/.claude/commands/azure/feature-decompose.md +0 -380
  75. package/autopm/.claude/commands/azure/feature-list.md +0 -61
  76. package/autopm/.claude/commands/azure/feature-new.md +0 -115
  77. package/autopm/.claude/commands/azure/feature-show.md +0 -205
  78. package/autopm/.claude/commands/azure/feature-start.md +0 -130
  79. package/autopm/.claude/commands/azure/fix-integration-example.md +0 -93
  80. package/autopm/.claude/commands/azure/help.md +0 -150
  81. package/autopm/.claude/commands/azure/import-us.md +0 -269
  82. package/autopm/.claude/commands/azure/init.md +0 -211
  83. package/autopm/.claude/commands/azure/next-task.md +0 -262
  84. package/autopm/.claude/commands/azure/search.md +0 -160
  85. package/autopm/.claude/commands/azure/sprint-status.md +0 -235
  86. package/autopm/.claude/commands/azure/standup.md +0 -260
  87. package/autopm/.claude/commands/azure/sync-all.md +0 -99
  88. package/autopm/.claude/commands/azure/task-analyze.md +0 -186
  89. package/autopm/.claude/commands/azure/task-close.md +0 -329
  90. package/autopm/.claude/commands/azure/task-edit.md +0 -145
  91. package/autopm/.claude/commands/azure/task-list.md +0 -263
  92. package/autopm/.claude/commands/azure/task-new.md +0 -84
  93. package/autopm/.claude/commands/azure/task-reopen.md +0 -79
  94. package/autopm/.claude/commands/azure/task-show.md +0 -126
  95. package/autopm/.claude/commands/azure/task-start.md +0 -301
  96. package/autopm/.claude/commands/azure/task-status.md +0 -65
  97. package/autopm/.claude/commands/azure/task-sync.md +0 -67
  98. package/autopm/.claude/commands/azure/us-edit.md +0 -164
  99. package/autopm/.claude/commands/azure/us-list.md +0 -202
  100. package/autopm/.claude/commands/azure/us-new.md +0 -265
  101. package/autopm/.claude/commands/azure/us-parse.md +0 -253
  102. package/autopm/.claude/commands/azure/us-show.md +0 -188
  103. package/autopm/.claude/commands/azure/us-status.md +0 -320
  104. package/autopm/.claude/commands/azure/validate.md +0 -86
  105. package/autopm/.claude/commands/azure/work-item-sync.md +0 -47
  106. package/autopm/.claude/commands/cloud/infra-deploy.md +0 -38
  107. package/autopm/.claude/commands/github/workflow-create.md +0 -42
  108. package/autopm/.claude/commands/infrastructure/ssh-security.md +0 -65
  109. package/autopm/.claude/commands/infrastructure/traefik-setup.md +0 -65
  110. package/autopm/.claude/commands/kubernetes/deploy.md +0 -37
  111. package/autopm/.claude/commands/playwright/test-scaffold.md +0 -38
  112. package/autopm/.claude/commands/pm/blocked.md +0 -28
  113. package/autopm/.claude/commands/pm/clean.md +0 -119
  114. package/autopm/.claude/commands/pm/context-create.md +0 -136
  115. package/autopm/.claude/commands/pm/context-prime.md +0 -170
  116. package/autopm/.claude/commands/pm/context-update.md +0 -292
  117. package/autopm/.claude/commands/pm/context.md +0 -28
  118. package/autopm/.claude/commands/pm/epic-close.md +0 -86
  119. package/autopm/.claude/commands/pm/epic-decompose.md +0 -370
  120. package/autopm/.claude/commands/pm/epic-edit.md +0 -83
  121. package/autopm/.claude/commands/pm/epic-list.md +0 -30
  122. package/autopm/.claude/commands/pm/epic-merge.md +0 -222
  123. package/autopm/.claude/commands/pm/epic-oneshot.md +0 -119
  124. package/autopm/.claude/commands/pm/epic-refresh.md +0 -119
  125. package/autopm/.claude/commands/pm/epic-show.md +0 -28
  126. package/autopm/.claude/commands/pm/epic-split.md +0 -120
  127. package/autopm/.claude/commands/pm/epic-start.md +0 -195
  128. package/autopm/.claude/commands/pm/epic-status.md +0 -28
  129. package/autopm/.claude/commands/pm/epic-sync-modular.md +0 -338
  130. package/autopm/.claude/commands/pm/epic-sync-original.md +0 -473
  131. package/autopm/.claude/commands/pm/epic-sync.md +0 -486
  132. package/autopm/.claude/commands/pm/help.md +0 -28
  133. package/autopm/.claude/commands/pm/import.md +0 -115
  134. package/autopm/.claude/commands/pm/in-progress.md +0 -28
  135. package/autopm/.claude/commands/pm/init.md +0 -28
  136. package/autopm/.claude/commands/pm/issue-analyze.md +0 -202
  137. package/autopm/.claude/commands/pm/issue-close.md +0 -119
  138. package/autopm/.claude/commands/pm/issue-edit.md +0 -93
  139. package/autopm/.claude/commands/pm/issue-reopen.md +0 -87
  140. package/autopm/.claude/commands/pm/issue-show.md +0 -41
  141. package/autopm/.claude/commands/pm/issue-start.md +0 -234
  142. package/autopm/.claude/commands/pm/issue-status.md +0 -95
  143. package/autopm/.claude/commands/pm/issue-sync.md +0 -411
  144. package/autopm/.claude/commands/pm/next.md +0 -28
  145. package/autopm/.claude/commands/pm/prd-edit.md +0 -82
  146. package/autopm/.claude/commands/pm/prd-list.md +0 -28
  147. package/autopm/.claude/commands/pm/prd-new.md +0 -55
  148. package/autopm/.claude/commands/pm/prd-parse.md +0 -42
  149. package/autopm/.claude/commands/pm/prd-status.md +0 -28
  150. package/autopm/.claude/commands/pm/search.md +0 -28
  151. package/autopm/.claude/commands/pm/standup.md +0 -28
  152. package/autopm/.claude/commands/pm/status.md +0 -28
  153. package/autopm/.claude/commands/pm/sync.md +0 -99
  154. package/autopm/.claude/commands/pm/test-reference-update.md +0 -151
  155. package/autopm/.claude/commands/pm/validate.md +0 -28
  156. package/autopm/.claude/commands/pm/what-next.md +0 -28
  157. package/autopm/.claude/commands/python/api-scaffold.md +0 -50
  158. package/autopm/.claude/commands/python/docs-query.md +0 -48
  159. package/autopm/.claude/commands/react/app-scaffold.md +0 -50
  160. package/autopm/.claude/commands/testing/prime.md +0 -314
  161. package/autopm/.claude/commands/testing/run.md +0 -125
  162. package/autopm/.claude/commands/ui/bootstrap-scaffold.md +0 -65
  163. package/autopm/.claude/commands/ui/tailwind-system.md +0 -64
  164. package/autopm/.claude/rules/ai-integration-patterns.md +0 -219
  165. package/autopm/.claude/rules/ci-cd-kubernetes-strategy.md +0 -25
  166. package/autopm/.claude/rules/database-management-strategy.md +0 -17
  167. package/autopm/.claude/rules/database-pipeline.md +0 -94
  168. package/autopm/.claude/rules/devops-troubleshooting-playbook.md +0 -450
  169. package/autopm/.claude/rules/docker-first-development.md +0 -404
  170. package/autopm/.claude/rules/infrastructure-pipeline.md +0 -128
  171. package/autopm/.claude/rules/performance-guidelines.md +0 -403
  172. package/autopm/.claude/rules/ui-development-standards.md +0 -281
  173. package/autopm/.claude/rules/ui-framework-rules.md +0 -151
  174. package/autopm/.claude/rules/ux-design-rules.md +0 -209
  175. package/autopm/.claude/rules/visual-testing.md +0 -223
  176. package/autopm/.claude/scripts/azure/README.md +0 -192
  177. package/autopm/.claude/scripts/azure/active-work.js +0 -524
  178. package/autopm/.claude/scripts/azure/active-work.sh +0 -20
  179. package/autopm/.claude/scripts/azure/blocked.js +0 -520
  180. package/autopm/.claude/scripts/azure/blocked.sh +0 -20
  181. package/autopm/.claude/scripts/azure/daily.js +0 -533
  182. package/autopm/.claude/scripts/azure/daily.sh +0 -20
  183. package/autopm/.claude/scripts/azure/dashboard.js +0 -970
  184. package/autopm/.claude/scripts/azure/dashboard.sh +0 -20
  185. package/autopm/.claude/scripts/azure/feature-list.js +0 -254
  186. package/autopm/.claude/scripts/azure/feature-list.sh +0 -20
  187. package/autopm/.claude/scripts/azure/feature-show.js +0 -7
  188. package/autopm/.claude/scripts/azure/feature-show.sh +0 -20
  189. package/autopm/.claude/scripts/azure/feature-status.js +0 -604
  190. package/autopm/.claude/scripts/azure/feature-status.sh +0 -20
  191. package/autopm/.claude/scripts/azure/help.js +0 -342
  192. package/autopm/.claude/scripts/azure/help.sh +0 -20
  193. package/autopm/.claude/scripts/azure/next-task.js +0 -508
  194. package/autopm/.claude/scripts/azure/next-task.sh +0 -20
  195. package/autopm/.claude/scripts/azure/search.js +0 -469
  196. package/autopm/.claude/scripts/azure/search.sh +0 -20
  197. package/autopm/.claude/scripts/azure/setup.js +0 -745
  198. package/autopm/.claude/scripts/azure/setup.sh +0 -20
  199. package/autopm/.claude/scripts/azure/sprint-report.js +0 -1012
  200. package/autopm/.claude/scripts/azure/sprint-report.sh +0 -20
  201. package/autopm/.claude/scripts/azure/sync.js +0 -563
  202. package/autopm/.claude/scripts/azure/sync.sh +0 -20
  203. package/autopm/.claude/scripts/azure/us-list.js +0 -210
  204. package/autopm/.claude/scripts/azure/us-list.sh +0 -20
  205. package/autopm/.claude/scripts/azure/us-status.js +0 -238
  206. package/autopm/.claude/scripts/azure/us-status.sh +0 -20
  207. package/autopm/.claude/scripts/azure/validate.js +0 -626
  208. package/autopm/.claude/scripts/azure/validate.sh +0 -20
  209. package/autopm/.claude/scripts/azure/wrapper-template.sh +0 -20
  210. package/autopm/.claude/scripts/github/dependency-tracker.js +0 -554
  211. package/autopm/.claude/scripts/github/dependency-validator.js +0 -545
  212. package/autopm/.claude/scripts/github/dependency-visualizer.js +0 -477
  213. package/autopm/.claude/scripts/pm/analytics.js +0 -425
  214. package/autopm/.claude/scripts/pm/blocked.js +0 -164
  215. package/autopm/.claude/scripts/pm/blocked.sh +0 -78
  216. package/autopm/.claude/scripts/pm/clean.js +0 -464
  217. package/autopm/.claude/scripts/pm/context-create.js +0 -216
  218. package/autopm/.claude/scripts/pm/context-prime.js +0 -335
  219. package/autopm/.claude/scripts/pm/context-update.js +0 -344
  220. package/autopm/.claude/scripts/pm/context.js +0 -338
  221. package/autopm/.claude/scripts/pm/epic-close.js +0 -347
  222. package/autopm/.claude/scripts/pm/epic-edit.js +0 -382
  223. package/autopm/.claude/scripts/pm/epic-list.js +0 -273
  224. package/autopm/.claude/scripts/pm/epic-list.sh +0 -109
  225. package/autopm/.claude/scripts/pm/epic-show.js +0 -291
  226. package/autopm/.claude/scripts/pm/epic-show.sh +0 -105
  227. package/autopm/.claude/scripts/pm/epic-split.js +0 -522
  228. package/autopm/.claude/scripts/pm/epic-start/epic-start.js +0 -183
  229. package/autopm/.claude/scripts/pm/epic-start/epic-start.sh +0 -94
  230. package/autopm/.claude/scripts/pm/epic-status.js +0 -291
  231. package/autopm/.claude/scripts/pm/epic-status.sh +0 -104
  232. package/autopm/.claude/scripts/pm/epic-sync/README.md +0 -208
  233. package/autopm/.claude/scripts/pm/epic-sync/create-epic-issue.sh +0 -77
  234. package/autopm/.claude/scripts/pm/epic-sync/create-task-issues.sh +0 -86
  235. package/autopm/.claude/scripts/pm/epic-sync/update-epic-file.sh +0 -79
  236. package/autopm/.claude/scripts/pm/epic-sync/update-references.sh +0 -89
  237. package/autopm/.claude/scripts/pm/epic-sync.sh +0 -137
  238. package/autopm/.claude/scripts/pm/help.js +0 -92
  239. package/autopm/.claude/scripts/pm/help.sh +0 -90
  240. package/autopm/.claude/scripts/pm/in-progress.js +0 -178
  241. package/autopm/.claude/scripts/pm/in-progress.sh +0 -93
  242. package/autopm/.claude/scripts/pm/init.js +0 -321
  243. package/autopm/.claude/scripts/pm/init.sh +0 -178
  244. package/autopm/.claude/scripts/pm/issue-close.js +0 -232
  245. package/autopm/.claude/scripts/pm/issue-edit.js +0 -310
  246. package/autopm/.claude/scripts/pm/issue-show.js +0 -272
  247. package/autopm/.claude/scripts/pm/issue-start.js +0 -181
  248. package/autopm/.claude/scripts/pm/issue-sync/format-comment.sh +0 -468
  249. package/autopm/.claude/scripts/pm/issue-sync/gather-updates.sh +0 -460
  250. package/autopm/.claude/scripts/pm/issue-sync/post-comment.sh +0 -330
  251. package/autopm/.claude/scripts/pm/issue-sync/preflight-validation.sh +0 -348
  252. package/autopm/.claude/scripts/pm/issue-sync/update-frontmatter.sh +0 -387
  253. package/autopm/.claude/scripts/pm/lib/README.md +0 -85
  254. package/autopm/.claude/scripts/pm/lib/epic-discovery.js +0 -119
  255. package/autopm/.claude/scripts/pm/lib/logger.js +0 -78
  256. package/autopm/.claude/scripts/pm/next.js +0 -189
  257. package/autopm/.claude/scripts/pm/next.sh +0 -72
  258. package/autopm/.claude/scripts/pm/optimize.js +0 -407
  259. package/autopm/.claude/scripts/pm/pr-create.js +0 -337
  260. package/autopm/.claude/scripts/pm/pr-list.js +0 -257
  261. package/autopm/.claude/scripts/pm/prd-list.js +0 -242
  262. package/autopm/.claude/scripts/pm/prd-list.sh +0 -103
  263. package/autopm/.claude/scripts/pm/prd-new.js +0 -684
  264. package/autopm/.claude/scripts/pm/prd-parse.js +0 -547
  265. package/autopm/.claude/scripts/pm/prd-status.js +0 -152
  266. package/autopm/.claude/scripts/pm/prd-status.sh +0 -63
  267. package/autopm/.claude/scripts/pm/release.js +0 -460
  268. package/autopm/.claude/scripts/pm/search.js +0 -192
  269. package/autopm/.claude/scripts/pm/search.sh +0 -89
  270. package/autopm/.claude/scripts/pm/standup.js +0 -362
  271. package/autopm/.claude/scripts/pm/standup.sh +0 -95
  272. package/autopm/.claude/scripts/pm/status.js +0 -148
  273. package/autopm/.claude/scripts/pm/status.sh +0 -59
  274. package/autopm/.claude/scripts/pm/sync-batch.js +0 -337
  275. package/autopm/.claude/scripts/pm/sync.js +0 -343
  276. package/autopm/.claude/scripts/pm/template-list.js +0 -141
  277. package/autopm/.claude/scripts/pm/template-new.js +0 -366
  278. package/autopm/.claude/scripts/pm/validate.js +0 -274
  279. package/autopm/.claude/scripts/pm/validate.sh +0 -106
  280. package/autopm/.claude/scripts/pm/what-next.js +0 -660
  281. package/bin/node/azure-feature-show.js +0 -7
@@ -31,12 +31,24 @@
31
31
  * - getIssuePath: Get file path for issue
32
32
  * - formatIssueDuration: Format time duration
33
33
  *
34
+ * 6. GitHub Sync Methods (8 methods):
35
+ * - syncToGitHub: Enhanced push with conflict detection
36
+ * - syncFromGitHub: Enhanced pull with merge
37
+ * - syncBidirectional: Full bidirectional sync
38
+ * - createGitHubIssue: Create new issue on GitHub
39
+ * - updateGitHubIssue: Update existing GitHub issue
40
+ * - detectConflict: Detect sync conflicts
41
+ * - resolveConflict: Resolve sync conflict with strategy
42
+ * - getSyncStatus: Get sync status for issue
43
+ *
34
44
  * Documentation Queries:
35
45
  * - GitHub Issues API v3 best practices (2025)
36
46
  * - Azure DevOps work items REST API patterns
37
47
  * - Agile issue tracking workflow best practices
38
48
  * - mcp://context7/project-management/issue-tracking - Issue lifecycle management
39
49
  * - mcp://context7/markdown/frontmatter - YAML frontmatter patterns
50
+ * - mcp://context7/conflict-resolution/sync - Conflict resolution strategies
51
+ * - mcp://context7/github/sync-patterns - GitHub synchronization patterns
40
52
  */
41
53
 
42
54
  class IssueService {
@@ -586,6 +598,985 @@ ${issueData.description || ''}
586
598
  return `${minutes} minute${minutes > 1 ? 's' : ''}`;
587
599
  }
588
600
  }
601
+
602
+ // ==========================================
603
+ // 6. GITHUB SYNC METHODS (8 NEW METHODS)
604
+ // ==========================================
605
+
606
+ /**
607
+ * Sync local issue to GitHub (enhanced push with conflict detection)
608
+ *
609
+ * @param {number|string} issueNumber - Local issue number
610
+ * @param {Object} [options={}] - Sync options
611
+ * @param {boolean} [options.detectConflicts=false] - Enable conflict detection
612
+ * @returns {Promise<Object>} Result: { success, issueNumber, githubNumber, action, conflict? }
613
+ */
614
+ async syncToGitHub(issueNumber, options = {}) {
615
+ const syncMap = await this._loadSyncMap();
616
+ const localIssue = await this.getLocalIssue(issueNumber);
617
+ const githubNumber = syncMap['local-to-github'][String(issueNumber)];
618
+
619
+ let result;
620
+ let action;
621
+
622
+ if (githubNumber) {
623
+ // Check for conflicts if enabled
624
+ if (options.detectConflicts) {
625
+ const githubIssue = await this.provider.getIssue(githubNumber);
626
+ const conflict = this.detectConflict(localIssue, githubIssue);
627
+
628
+ if (conflict.hasConflict && conflict.remoteNewer) {
629
+ return {
630
+ success: false,
631
+ issueNumber: String(issueNumber),
632
+ githubNumber: String(githubNumber),
633
+ conflict
634
+ };
635
+ }
636
+ }
637
+
638
+ // Update existing GitHub issue
639
+ result = await this.updateGitHubIssue(githubNumber, localIssue);
640
+ action = 'updated';
641
+ } else {
642
+ // Create new GitHub issue
643
+ result = await this.createGitHubIssue(localIssue);
644
+ action = 'created';
645
+ }
646
+
647
+ // Update sync-map
648
+ await this._updateSyncMap(String(issueNumber), String(result.number));
649
+
650
+ return {
651
+ success: true,
652
+ issueNumber: String(issueNumber),
653
+ githubNumber: String(result.number),
654
+ action
655
+ };
656
+ }
657
+
658
+ /**
659
+ * Sync GitHub issue to local (enhanced pull with merge)
660
+ *
661
+ * @param {number|string} githubNumber - GitHub issue number
662
+ * @param {Object} [options={}] - Sync options
663
+ * @param {boolean} [options.detectConflicts=false] - Enable conflict detection
664
+ * @param {string} [options.conflictStrategy='newest'] - Conflict resolution strategy
665
+ * @returns {Promise<Object>} Result: { success, localNumber, githubNumber, action, conflict? }
666
+ */
667
+ async syncFromGitHub(githubNumber, options = {}) {
668
+ const fs = require('fs-extra');
669
+
670
+ const githubIssue = await this.provider.getIssue(githubNumber);
671
+ const syncMap = await this._loadSyncMap();
672
+ const localNumber = syncMap['github-to-local'][String(githubNumber)];
673
+
674
+ let action;
675
+ let finalLocalNumber = localNumber;
676
+
677
+ if (localNumber) {
678
+ // Check for conflicts if enabled
679
+ if (options.detectConflicts) {
680
+ const localIssue = await this.getLocalIssue(localNumber);
681
+ const conflict = this.detectConflict(localIssue, githubIssue);
682
+
683
+ if (conflict.hasConflict && conflict.localNewer) {
684
+ return {
685
+ success: false,
686
+ localNumber: String(localNumber),
687
+ githubNumber: String(githubNumber),
688
+ conflict
689
+ };
690
+ }
691
+ }
692
+
693
+ // Update existing local issue
694
+ action = 'updated';
695
+ } else {
696
+ // Create new local issue - find next available number
697
+ const issues = await this.listIssues();
698
+ const maxNumber = issues.reduce((max, issue) => {
699
+ const num = parseInt(issue.id || '0');
700
+ return num > max ? num : max;
701
+ }, 0);
702
+ finalLocalNumber = String(maxNumber + 1);
703
+ action = 'created';
704
+ }
705
+
706
+ // Build issue content with frontmatter
707
+ const labels = githubIssue.labels ? githubIssue.labels.map(l => l.name || l).join(', ') : '';
708
+ const assignee = githubIssue.assignees && githubIssue.assignees.length > 0
709
+ ? githubIssue.assignees[0].login || githubIssue.assignees[0]
710
+ : '';
711
+
712
+ const frontmatter = `---
713
+ id: ${finalLocalNumber}
714
+ title: ${githubIssue.title}
715
+ status: ${githubIssue.state === 'closed' ? 'closed' : 'open'}
716
+ ${assignee ? `assignee: ${assignee}` : ''}
717
+ ${labels ? `labels: ${labels}` : ''}
718
+ created: ${githubIssue.created_at}
719
+ updated: ${githubIssue.updated_at}
720
+ github_number: ${githubNumber}
721
+ ---
722
+
723
+ # ${githubIssue.title}
724
+
725
+ ${githubIssue.body || ''}
726
+ `;
727
+
728
+ // Write to local file
729
+ const issuePath = this.getIssuePath(finalLocalNumber);
730
+ await fs.writeFile(issuePath, frontmatter);
731
+
732
+ // Update sync-map
733
+ await this._updateSyncMap(String(finalLocalNumber), String(githubNumber));
734
+
735
+ return {
736
+ success: true,
737
+ localNumber: String(finalLocalNumber),
738
+ githubNumber: String(githubNumber),
739
+ action
740
+ };
741
+ }
742
+
743
+ /**
744
+ * Bidirectional sync - sync in the direction of newer changes
745
+ *
746
+ * @param {number|string} issueNumber - Local issue number
747
+ * @param {Object} [options={}] - Sync options
748
+ * @param {string} [options.conflictStrategy='detect'] - How to handle conflicts
749
+ * @returns {Promise<Object>} Result: { success, direction, conflict? }
750
+ */
751
+ async syncBidirectional(issueNumber, options = {}) {
752
+ const syncMap = await this._loadSyncMap();
753
+ const githubNumber = syncMap['local-to-github'][String(issueNumber)];
754
+
755
+ if (!githubNumber) {
756
+ // No GitHub mapping, push to GitHub
757
+ const result = await this.syncToGitHub(issueNumber);
758
+ return { ...result, direction: 'to-github' };
759
+ }
760
+
761
+ const localIssue = await this.getLocalIssue(issueNumber);
762
+ const githubIssue = await this.provider.getIssue(githubNumber);
763
+
764
+ const conflict = this.detectConflict(localIssue, githubIssue);
765
+
766
+ if (conflict.hasConflict) {
767
+ if (options.conflictStrategy === 'detect') {
768
+ return {
769
+ success: false,
770
+ direction: 'conflict',
771
+ conflict
772
+ };
773
+ }
774
+
775
+ // Auto-resolve based on timestamps
776
+ if (conflict.localNewer) {
777
+ const result = await this.syncToGitHub(issueNumber);
778
+ return { ...result, direction: 'to-github' };
779
+ } else if (conflict.remoteNewer) {
780
+ const result = await this.syncFromGitHub(githubNumber);
781
+ return { ...result, direction: 'from-github' };
782
+ }
783
+ }
784
+
785
+ // No conflict or same timestamp - sync local to GitHub
786
+ const result = await this.syncToGitHub(issueNumber);
787
+ return { ...result, direction: 'to-github' };
788
+ }
789
+
790
+ /**
791
+ * Create new GitHub issue from local data
792
+ *
793
+ * @param {Object} issueData - Local issue data
794
+ * @returns {Promise<Object>} Created GitHub issue
795
+ */
796
+ async createGitHubIssue(issueData) {
797
+ const labels = issueData.labels ? issueData.labels.split(',').map(l => l.trim()) : [];
798
+ const assignees = issueData.assignee ? [issueData.assignee] : [];
799
+
800
+ const githubData = {
801
+ title: issueData.title,
802
+ body: issueData.content || '',
803
+ state: this._mapStatusToGitHub(issueData.status),
804
+ labels,
805
+ assignees
806
+ };
807
+
808
+ // Remove empty arrays
809
+ if (githubData.labels.length === 0) delete githubData.labels;
810
+ if (githubData.assignees.length === 0) delete githubData.assignees;
811
+
812
+ const result = await this.provider.createIssue(githubData);
813
+
814
+ // Update sync-map if we have an ID
815
+ if (issueData.id) {
816
+ await this._updateSyncMap(String(issueData.id), String(result.number));
817
+ }
818
+
819
+ return result;
820
+ }
821
+
822
+ /**
823
+ * Update existing GitHub issue with local data
824
+ *
825
+ * @param {number|string} githubNumber - GitHub issue number
826
+ * @param {Object} issueData - Local issue data
827
+ * @returns {Promise<Object>} Updated GitHub issue
828
+ */
829
+ async updateGitHubIssue(githubNumber, issueData) {
830
+ const updateData = {};
831
+
832
+ if (issueData.title) {
833
+ updateData.title = issueData.title;
834
+ }
835
+
836
+ if (issueData.content) {
837
+ updateData.body = issueData.content;
838
+ }
839
+
840
+ if (issueData.status) {
841
+ updateData.state = this._mapStatusToGitHub(issueData.status);
842
+ }
843
+
844
+ if (issueData.labels) {
845
+ updateData.labels = issueData.labels.split(',').map(l => l.trim());
846
+ }
847
+
848
+ if (issueData.assignee) {
849
+ updateData.assignees = [issueData.assignee];
850
+ }
851
+
852
+ return await this.provider.updateIssue(githubNumber, updateData);
853
+ }
854
+
855
+ /**
856
+ * Detect sync conflicts between local and GitHub issues
857
+ *
858
+ * @param {Object} localIssue - Local issue data
859
+ * @param {Object} githubIssue - GitHub issue data
860
+ * @returns {Object} Conflict info: { hasConflict, localNewer, remoteNewer, conflictFields }
861
+ */
862
+ detectConflict(localIssue, githubIssue) {
863
+ const localTime = new Date(localIssue.updated || localIssue.created || 0);
864
+ const githubTime = new Date(githubIssue.updated_at || githubIssue.created_at || 0);
865
+
866
+ const conflictFields = [];
867
+
868
+ // Check field differences
869
+ if (localIssue.title !== githubIssue.title) {
870
+ conflictFields.push('title');
871
+ }
872
+
873
+ const localStatus = this._mapStatusToGitHub(localIssue.status);
874
+ if (localStatus !== githubIssue.state) {
875
+ conflictFields.push('status');
876
+ }
877
+
878
+ const hasConflict = localTime.getTime() !== githubTime.getTime();
879
+ const localNewer = localTime > githubTime;
880
+ const remoteNewer = githubTime > localTime;
881
+
882
+ return {
883
+ hasConflict,
884
+ localNewer,
885
+ remoteNewer,
886
+ conflictFields
887
+ };
888
+ }
889
+
890
+ /**
891
+ * Resolve sync conflict using specified strategy
892
+ *
893
+ * @param {number|string} issueNumber - Local issue number
894
+ * @param {string} strategy - Resolution strategy (local|remote|newest|manual|merge)
895
+ * @returns {Promise<Object>} Resolution result: { resolved, appliedStrategy, result }
896
+ * @throws {Error} If strategy is invalid
897
+ */
898
+ async resolveConflict(issueNumber, strategy) {
899
+ const validStrategies = ['local', 'remote', 'newest', 'manual', 'merge'];
900
+
901
+ if (!validStrategies.includes(strategy)) {
902
+ throw new Error('Invalid conflict resolution strategy');
903
+ }
904
+
905
+ if (strategy === 'manual') {
906
+ return {
907
+ resolved: false,
908
+ appliedStrategy: 'manual',
909
+ requiresManualResolution: true
910
+ };
911
+ }
912
+
913
+ const syncMap = await this._loadSyncMap();
914
+ const githubNumber = syncMap['local-to-github'][String(issueNumber)];
915
+
916
+ if (strategy === 'local') {
917
+ // Use local version
918
+ const result = await this.syncToGitHub(issueNumber);
919
+ return {
920
+ resolved: true,
921
+ appliedStrategy: 'local',
922
+ result
923
+ };
924
+ }
925
+
926
+ if (strategy === 'remote') {
927
+ // Use remote version
928
+ const result = await this.syncFromGitHub(githubNumber);
929
+ return {
930
+ resolved: true,
931
+ appliedStrategy: 'remote',
932
+ result
933
+ };
934
+ }
935
+
936
+ if (strategy === 'newest') {
937
+ // Use newest version based on timestamp
938
+ const localIssue = await this.getLocalIssue(issueNumber);
939
+ const githubIssue = await this.provider.getIssue(githubNumber);
940
+
941
+ const localTime = new Date(localIssue.updated || localIssue.created || 0);
942
+ const githubTime = new Date(githubIssue.updated_at || githubIssue.created_at || 0);
943
+
944
+ if (localTime >= githubTime) {
945
+ const result = await this.syncToGitHub(issueNumber);
946
+ return {
947
+ resolved: true,
948
+ appliedStrategy: 'newest',
949
+ result
950
+ };
951
+ } else {
952
+ const result = await this.syncFromGitHub(githubNumber);
953
+ return {
954
+ resolved: true,
955
+ appliedStrategy: 'newest',
956
+ result
957
+ };
958
+ }
959
+ }
960
+
961
+ // merge strategy would go here (future enhancement)
962
+ return {
963
+ resolved: false,
964
+ appliedStrategy: strategy,
965
+ message: 'Strategy not yet implemented'
966
+ };
967
+ }
968
+
969
+ /**
970
+ * Get sync status for an issue
971
+ *
972
+ * @param {number|string} issueNumber - Local issue number
973
+ * @returns {Promise<Object>} Status: { synced, localNumber, githubNumber, lastSync, status }
974
+ */
975
+ async getSyncStatus(issueNumber) {
976
+ const syncMap = await this._loadSyncMap();
977
+ const githubNumber = syncMap['local-to-github'][String(issueNumber)];
978
+
979
+ if (!githubNumber) {
980
+ return {
981
+ synced: false,
982
+ localNumber: String(issueNumber),
983
+ githubNumber: null,
984
+ status: 'not-synced'
985
+ };
986
+ }
987
+
988
+ const metadata = syncMap.metadata[String(issueNumber)] || {};
989
+
990
+ // Check if out of sync
991
+ try {
992
+ const localIssue = await this.getLocalIssue(issueNumber);
993
+ const githubIssue = await this.provider.getIssue(githubNumber);
994
+
995
+ const localTime = new Date(localIssue.updated || localIssue.created || 0);
996
+ const githubTime = new Date(githubIssue.updated_at || githubIssue.created_at || 0);
997
+ const lastSyncTime = new Date(metadata.lastSync || 0);
998
+
999
+ const isOutOfSync = localTime > lastSyncTime || githubTime > lastSyncTime;
1000
+
1001
+ return {
1002
+ synced: !isOutOfSync,
1003
+ localNumber: String(issueNumber),
1004
+ githubNumber: String(githubNumber),
1005
+ lastSync: metadata.lastSync,
1006
+ status: isOutOfSync ? 'out-of-sync' : 'synced'
1007
+ };
1008
+ } catch (error) {
1009
+ return {
1010
+ synced: true,
1011
+ localNumber: String(issueNumber),
1012
+ githubNumber: String(githubNumber),
1013
+ lastSync: metadata.lastSync,
1014
+ status: 'synced'
1015
+ };
1016
+ }
1017
+ }
1018
+
1019
+ // ==========================================
1020
+ // PRIVATE HELPER METHODS FOR SYNC
1021
+ // ==========================================
1022
+
1023
+ /**
1024
+ * Load sync-map from file
1025
+ * @private
1026
+ */
1027
+ async _loadSyncMap() {
1028
+ const fs = require('fs-extra');
1029
+ const path = require('path');
1030
+ const syncMapPath = path.join(process.cwd(), '.claude/sync-map.json');
1031
+
1032
+ if (await fs.pathExists(syncMapPath)) {
1033
+ return await fs.readJSON(syncMapPath);
1034
+ }
1035
+
1036
+ return {
1037
+ 'local-to-github': {},
1038
+ 'github-to-local': {},
1039
+ 'metadata': {}
1040
+ };
1041
+ }
1042
+
1043
+ /**
1044
+ * Save sync-map to file
1045
+ * @private
1046
+ */
1047
+ async _saveSyncMap(syncMap) {
1048
+ const fs = require('fs-extra');
1049
+ const path = require('path');
1050
+ const syncMapPath = path.join(process.cwd(), '.claude/sync-map.json');
1051
+ await fs.writeJSON(syncMapPath, syncMap, { spaces: 2 });
1052
+ }
1053
+
1054
+ /**
1055
+ * Update sync-map with new mapping
1056
+ * @private
1057
+ */
1058
+ async _updateSyncMap(localNumber, githubNumber) {
1059
+ const syncMap = await this._loadSyncMap();
1060
+
1061
+ syncMap['local-to-github'][String(localNumber)] = String(githubNumber);
1062
+ syncMap['github-to-local'][String(githubNumber)] = String(localNumber);
1063
+ syncMap['metadata'][String(localNumber)] = {
1064
+ lastSync: new Date().toISOString(),
1065
+ githubNumber: String(githubNumber)
1066
+ };
1067
+
1068
+ await this._saveSyncMap(syncMap);
1069
+ }
1070
+
1071
+ /**
1072
+ * Map local status to GitHub state
1073
+ * @private
1074
+ */
1075
+ _mapStatusToGitHub(status) {
1076
+ if (!status) return 'open';
1077
+
1078
+ const lowerStatus = status.toLowerCase();
1079
+
1080
+ if (['closed', 'completed', 'done', 'resolved'].includes(lowerStatus)) {
1081
+ return 'closed';
1082
+ }
1083
+
1084
+ return 'open';
1085
+ }
1086
+
1087
+ // ==========================================
1088
+ // 7. AZURE DEVOPS SYNC METHODS (8 NEW METHODS)
1089
+ // ==========================================
1090
+
1091
+ /**
1092
+ * Sync local issue to Azure DevOps (enhanced push with conflict detection)
1093
+ *
1094
+ * @param {number|string} issueNumber - Local issue number
1095
+ * @param {Object} [options={}] - Sync options
1096
+ * @param {boolean} [options.detectConflicts=false] - Enable conflict detection
1097
+ * @returns {Promise<Object>} Result: { success, issueNumber, workItemId, action, conflict? }
1098
+ */
1099
+ async syncToAzure(issueNumber, options = {}) {
1100
+ const syncMap = await this._loadAzureSyncMap();
1101
+ const localIssue = await this.getLocalIssue(issueNumber);
1102
+ const workItemId = syncMap['local-to-azure'][String(issueNumber)];
1103
+
1104
+ let result;
1105
+ let action;
1106
+
1107
+ if (workItemId) {
1108
+ // Check for conflicts if enabled
1109
+ if (options.detectConflicts) {
1110
+ const azureWorkItem = await this.provider.getWorkItem(workItemId);
1111
+ const conflict = this.detectAzureConflict(localIssue, azureWorkItem);
1112
+
1113
+ if (conflict.hasConflict && conflict.remoteNewer) {
1114
+ return {
1115
+ success: false,
1116
+ issueNumber: String(issueNumber),
1117
+ workItemId: String(workItemId),
1118
+ conflict
1119
+ };
1120
+ }
1121
+ }
1122
+
1123
+ // Update existing Azure work item
1124
+ result = await this.updateAzureWorkItem(workItemId, localIssue);
1125
+ action = 'updated';
1126
+ } else {
1127
+ // Create new Azure work item
1128
+ result = await this.createAzureWorkItem(localIssue);
1129
+ action = 'created';
1130
+ }
1131
+
1132
+ // Update azure-sync-map
1133
+ const workItemType = result.fields['System.WorkItemType'] || 'User Story';
1134
+ await this._updateAzureSyncMap(String(issueNumber), String(result.id), workItemType);
1135
+
1136
+ return {
1137
+ success: true,
1138
+ issueNumber: String(issueNumber),
1139
+ workItemId: String(result.id),
1140
+ action
1141
+ };
1142
+ }
1143
+
1144
+ /**
1145
+ * Sync Azure work item to local (enhanced pull with merge)
1146
+ *
1147
+ * @param {number|string} workItemId - Azure work item ID
1148
+ * @param {Object} [options={}] - Sync options
1149
+ * @param {boolean} [options.detectConflicts=false] - Enable conflict detection
1150
+ * @returns {Promise<Object>} Result: { success, localNumber, workItemId, action, conflict? }
1151
+ */
1152
+ async syncFromAzure(workItemId, options = {}) {
1153
+ const fs = require('fs-extra');
1154
+
1155
+ const azureWorkItem = await this.provider.getWorkItem(workItemId);
1156
+ const syncMap = await this._loadAzureSyncMap();
1157
+ const localNumber = syncMap['azure-to-local'][String(workItemId)];
1158
+
1159
+ let action;
1160
+ let finalLocalNumber = localNumber;
1161
+
1162
+ if (localNumber) {
1163
+ // Check for conflicts if enabled
1164
+ if (options.detectConflicts) {
1165
+ const localIssue = await this.getLocalIssue(localNumber);
1166
+ const conflict = this.detectAzureConflict(localIssue, azureWorkItem);
1167
+
1168
+ if (conflict.hasConflict && conflict.localNewer) {
1169
+ return {
1170
+ success: false,
1171
+ localNumber: String(localNumber),
1172
+ workItemId: String(workItemId),
1173
+ conflict
1174
+ };
1175
+ }
1176
+ }
1177
+
1178
+ // Update existing local issue
1179
+ action = 'updated';
1180
+ } else {
1181
+ // Create new local issue - find next available number
1182
+ const issues = await this.listIssues();
1183
+ const maxNumber = issues.reduce((max, issue) => {
1184
+ const num = parseInt(issue.id || '0');
1185
+ return num > max ? num : max;
1186
+ }, 0);
1187
+ finalLocalNumber = String(maxNumber + 1);
1188
+ action = 'created';
1189
+ }
1190
+
1191
+ // Build issue content with frontmatter
1192
+ const tags = azureWorkItem.fields['System.Tags'] || '';
1193
+ const assignee = azureWorkItem.fields['System.AssignedTo']
1194
+ ? azureWorkItem.fields['System.AssignedTo'].displayName
1195
+ : '';
1196
+
1197
+ const frontmatter = `---
1198
+ id: ${finalLocalNumber}
1199
+ title: ${azureWorkItem.fields['System.Title']}
1200
+ status: ${this._mapAzureStateToLocal(azureWorkItem.fields['System.State'])}
1201
+ ${assignee ? `assignee: ${assignee}` : ''}
1202
+ ${tags ? `labels: ${tags}` : ''}
1203
+ created: ${azureWorkItem.fields['System.CreatedDate']}
1204
+ updated: ${azureWorkItem.fields['System.ChangedDate']}
1205
+ azure_work_item_id: ${workItemId}
1206
+ work_item_type: ${azureWorkItem.fields['System.WorkItemType']}
1207
+ ---
1208
+
1209
+ # ${azureWorkItem.fields['System.Title']}
1210
+
1211
+ ${azureWorkItem.fields['System.Description'] || ''}
1212
+ `;
1213
+
1214
+ // Write to local file
1215
+ const issuePath = this.getIssuePath(finalLocalNumber);
1216
+ await fs.writeFile(issuePath, frontmatter);
1217
+
1218
+ // Update azure-sync-map
1219
+ const workItemType = azureWorkItem.fields['System.WorkItemType'] || 'User Story';
1220
+ await this._updateAzureSyncMap(String(finalLocalNumber), String(workItemId), workItemType);
1221
+
1222
+ return {
1223
+ success: true,
1224
+ localNumber: String(finalLocalNumber),
1225
+ workItemId: String(workItemId),
1226
+ action
1227
+ };
1228
+ }
1229
+
1230
+ /**
1231
+ * Bidirectional Azure sync - sync in the direction of newer changes
1232
+ *
1233
+ * @param {number|string} issueNumber - Local issue number
1234
+ * @param {Object} [options={}] - Sync options
1235
+ * @param {string} [options.conflictStrategy='detect'] - How to handle conflicts
1236
+ * @returns {Promise<Object>} Result: { success, direction, conflict? }
1237
+ */
1238
+ async syncBidirectionalAzure(issueNumber, options = {}) {
1239
+ const syncMap = await this._loadAzureSyncMap();
1240
+ const workItemId = syncMap['local-to-azure'][String(issueNumber)];
1241
+
1242
+ if (!workItemId) {
1243
+ // No Azure mapping, push to Azure
1244
+ const result = await this.syncToAzure(issueNumber);
1245
+ return { ...result, direction: 'to-azure' };
1246
+ }
1247
+
1248
+ const localIssue = await this.getLocalIssue(issueNumber);
1249
+ const azureWorkItem = await this.provider.getWorkItem(workItemId);
1250
+
1251
+ const conflict = this.detectAzureConflict(localIssue, azureWorkItem);
1252
+
1253
+ if (conflict.hasConflict) {
1254
+ if (options.conflictStrategy === 'detect') {
1255
+ return {
1256
+ success: false,
1257
+ direction: 'conflict',
1258
+ conflict
1259
+ };
1260
+ }
1261
+
1262
+ // Auto-resolve based on timestamps
1263
+ if (conflict.localNewer) {
1264
+ const result = await this.syncToAzure(issueNumber);
1265
+ return { ...result, direction: 'to-azure' };
1266
+ } else if (conflict.remoteNewer) {
1267
+ const result = await this.syncFromAzure(workItemId);
1268
+ return { ...result, direction: 'from-azure' };
1269
+ }
1270
+ }
1271
+
1272
+ // No conflict or same timestamp - sync local to Azure
1273
+ const result = await this.syncToAzure(issueNumber);
1274
+ return { ...result, direction: 'to-azure' };
1275
+ }
1276
+
1277
+ /**
1278
+ * Create new Azure work item from local data
1279
+ *
1280
+ * @param {Object} issueData - Local issue data
1281
+ * @returns {Promise<Object>} Created Azure work item
1282
+ */
1283
+ async createAzureWorkItem(issueData) {
1284
+ const workItemType = issueData.work_item_type || 'User Story';
1285
+ const tags = issueData.labels || '';
1286
+
1287
+ const azureData = {
1288
+ title: issueData.title,
1289
+ description: issueData.content || '',
1290
+ state: this._mapStatusToAzure(issueData.status),
1291
+ tags
1292
+ };
1293
+
1294
+ const result = await this.provider.createWorkItem(workItemType, azureData);
1295
+
1296
+ // Update azure-sync-map if we have an ID
1297
+ if (issueData.id) {
1298
+ const resultWorkItemType = result.fields['System.WorkItemType'] || workItemType;
1299
+ await this._updateAzureSyncMap(String(issueData.id), String(result.id), resultWorkItemType);
1300
+ }
1301
+
1302
+ return result;
1303
+ }
1304
+
1305
+ /**
1306
+ * Update existing Azure work item with local data
1307
+ *
1308
+ * @param {number|string} workItemId - Azure work item ID
1309
+ * @param {Object} issueData - Local issue data
1310
+ * @returns {Promise<Object>} Updated Azure work item
1311
+ */
1312
+ async updateAzureWorkItem(workItemId, issueData) {
1313
+ const updateData = {};
1314
+
1315
+ if (issueData.title) {
1316
+ updateData.title = issueData.title;
1317
+ }
1318
+
1319
+ if (issueData.content) {
1320
+ updateData.description = issueData.content;
1321
+ }
1322
+
1323
+ if (issueData.status) {
1324
+ updateData.state = this._mapStatusToAzure(issueData.status);
1325
+ }
1326
+
1327
+ return await this.provider.updateWorkItem(workItemId, updateData);
1328
+ }
1329
+
1330
+ /**
1331
+ * Detect sync conflicts between local and Azure work items
1332
+ *
1333
+ * @param {Object} localIssue - Local issue data
1334
+ * @param {Object} azureWorkItem - Azure work item data
1335
+ * @returns {Object} Conflict info: { hasConflict, localNewer, remoteNewer, conflictFields }
1336
+ */
1337
+ detectAzureConflict(localIssue, azureWorkItem) {
1338
+ const localTime = new Date(localIssue.updated || localIssue.created || 0);
1339
+ const azureTime = new Date(azureWorkItem.fields['System.ChangedDate'] || azureWorkItem.fields['System.CreatedDate'] || 0);
1340
+
1341
+ const conflictFields = [];
1342
+
1343
+ // Check field differences
1344
+ if (localIssue.title !== azureWorkItem.fields['System.Title']) {
1345
+ conflictFields.push('title');
1346
+ }
1347
+
1348
+ const localStatus = this._mapStatusToAzure(localIssue.status);
1349
+ if (localStatus !== azureWorkItem.fields['System.State']) {
1350
+ conflictFields.push('status');
1351
+ }
1352
+
1353
+ const hasConflict = localTime.getTime() !== azureTime.getTime();
1354
+ const localNewer = localTime > azureTime;
1355
+ const remoteNewer = azureTime > localTime;
1356
+
1357
+ return {
1358
+ hasConflict,
1359
+ localNewer,
1360
+ remoteNewer,
1361
+ conflictFields
1362
+ };
1363
+ }
1364
+
1365
+ /**
1366
+ * Resolve Azure sync conflict using specified strategy
1367
+ *
1368
+ * @param {number|string} issueNumber - Local issue number
1369
+ * @param {string} strategy - Resolution strategy (local|remote|newest|manual|merge)
1370
+ * @returns {Promise<Object>} Resolution result: { resolved, appliedStrategy, result }
1371
+ * @throws {Error} If strategy is invalid
1372
+ */
1373
+ async resolveAzureConflict(issueNumber, strategy) {
1374
+ const validStrategies = ['local', 'remote', 'newest', 'manual', 'merge'];
1375
+
1376
+ if (!validStrategies.includes(strategy)) {
1377
+ throw new Error('Invalid conflict resolution strategy');
1378
+ }
1379
+
1380
+ if (strategy === 'manual') {
1381
+ return {
1382
+ resolved: false,
1383
+ appliedStrategy: 'manual',
1384
+ requiresManualResolution: true
1385
+ };
1386
+ }
1387
+
1388
+ const syncMap = await this._loadAzureSyncMap();
1389
+ const workItemId = syncMap['local-to-azure'][String(issueNumber)];
1390
+
1391
+ if (strategy === 'local') {
1392
+ // Use local version
1393
+ const result = await this.syncToAzure(issueNumber);
1394
+ return {
1395
+ resolved: true,
1396
+ appliedStrategy: 'local',
1397
+ result
1398
+ };
1399
+ }
1400
+
1401
+ if (strategy === 'remote') {
1402
+ // Use remote version
1403
+ const result = await this.syncFromAzure(workItemId);
1404
+ return {
1405
+ resolved: true,
1406
+ appliedStrategy: 'remote',
1407
+ result
1408
+ };
1409
+ }
1410
+
1411
+ if (strategy === 'newest') {
1412
+ // Use newest version based on timestamp
1413
+ const localIssue = await this.getLocalIssue(issueNumber);
1414
+ const azureWorkItem = await this.provider.getWorkItem(workItemId);
1415
+
1416
+ const localTime = new Date(localIssue.updated || localIssue.created || 0);
1417
+ const azureTime = new Date(azureWorkItem.fields['System.ChangedDate'] || azureWorkItem.fields['System.CreatedDate'] || 0);
1418
+
1419
+ if (localTime >= azureTime) {
1420
+ const result = await this.syncToAzure(issueNumber);
1421
+ return {
1422
+ resolved: true,
1423
+ appliedStrategy: 'newest',
1424
+ result
1425
+ };
1426
+ } else {
1427
+ const result = await this.syncFromAzure(workItemId);
1428
+ return {
1429
+ resolved: true,
1430
+ appliedStrategy: 'newest',
1431
+ result
1432
+ };
1433
+ }
1434
+ }
1435
+
1436
+ // merge strategy would go here (future enhancement)
1437
+ return {
1438
+ resolved: false,
1439
+ appliedStrategy: strategy,
1440
+ message: 'Strategy not yet implemented'
1441
+ };
1442
+ }
1443
+
1444
+ /**
1445
+ * Get Azure sync status for an issue
1446
+ *
1447
+ * @param {number|string} issueNumber - Local issue number
1448
+ * @returns {Promise<Object>} Status: { synced, localNumber, workItemId, lastSync, status }
1449
+ */
1450
+ async getAzureSyncStatus(issueNumber) {
1451
+ const syncMap = await this._loadAzureSyncMap();
1452
+ const workItemId = syncMap['local-to-azure'][String(issueNumber)];
1453
+
1454
+ if (!workItemId) {
1455
+ return {
1456
+ synced: false,
1457
+ localNumber: String(issueNumber),
1458
+ workItemId: null,
1459
+ status: 'not-synced'
1460
+ };
1461
+ }
1462
+
1463
+ const metadata = syncMap.metadata[String(issueNumber)] || {};
1464
+
1465
+ // Check if out of sync
1466
+ try {
1467
+ const localIssue = await this.getLocalIssue(issueNumber);
1468
+ const azureWorkItem = await this.provider.getWorkItem(workItemId);
1469
+
1470
+ const localTime = new Date(localIssue.updated || localIssue.created || 0);
1471
+ const azureTime = new Date(azureWorkItem.fields['System.ChangedDate'] || azureWorkItem.fields['System.CreatedDate'] || 0);
1472
+ const lastSyncTime = new Date(metadata.lastSync || 0);
1473
+
1474
+ const isOutOfSync = localTime > lastSyncTime || azureTime > lastSyncTime;
1475
+
1476
+ return {
1477
+ synced: !isOutOfSync,
1478
+ localNumber: String(issueNumber),
1479
+ workItemId: String(workItemId),
1480
+ lastSync: metadata.lastSync,
1481
+ status: isOutOfSync ? 'out-of-sync' : 'synced'
1482
+ };
1483
+ } catch (error) {
1484
+ return {
1485
+ synced: true,
1486
+ localNumber: String(issueNumber),
1487
+ workItemId: String(workItemId),
1488
+ lastSync: metadata.lastSync,
1489
+ status: 'synced'
1490
+ };
1491
+ }
1492
+ }
1493
+
1494
+ // ==========================================
1495
+ // PRIVATE HELPER METHODS FOR AZURE SYNC
1496
+ // ==========================================
1497
+
1498
+ /**
1499
+ * Load azure-sync-map from file
1500
+ * @private
1501
+ */
1502
+ async _loadAzureSyncMap() {
1503
+ const fs = require('fs-extra');
1504
+ const path = require('path');
1505
+ const syncMapPath = path.join(process.cwd(), '.claude/azure-sync-map.json');
1506
+
1507
+ if (await fs.pathExists(syncMapPath)) {
1508
+ return await fs.readJSON(syncMapPath);
1509
+ }
1510
+
1511
+ return {
1512
+ 'local-to-azure': {},
1513
+ 'azure-to-local': {},
1514
+ 'metadata': {}
1515
+ };
1516
+ }
1517
+
1518
+ /**
1519
+ * Save azure-sync-map to file
1520
+ * @private
1521
+ */
1522
+ async _saveAzureSyncMap(syncMap) {
1523
+ const fs = require('fs-extra');
1524
+ const path = require('path');
1525
+ const syncMapPath = path.join(process.cwd(), '.claude/azure-sync-map.json');
1526
+ await fs.writeJSON(syncMapPath, syncMap, { spaces: 2 });
1527
+ }
1528
+
1529
+ /**
1530
+ * Update azure-sync-map with new mapping
1531
+ * @private
1532
+ */
1533
+ async _updateAzureSyncMap(localNumber, workItemId, workItemType = 'User Story') {
1534
+ const syncMap = await this._loadAzureSyncMap();
1535
+
1536
+ syncMap['local-to-azure'][String(localNumber)] = String(workItemId);
1537
+ syncMap['azure-to-local'][String(workItemId)] = String(localNumber);
1538
+ syncMap['metadata'][String(localNumber)] = {
1539
+ lastSync: new Date().toISOString(),
1540
+ workItemId: String(workItemId),
1541
+ workItemType
1542
+ };
1543
+
1544
+ await this._saveAzureSyncMap(syncMap);
1545
+ }
1546
+
1547
+ /**
1548
+ * Map local status to Azure state
1549
+ * @private
1550
+ */
1551
+ _mapStatusToAzure(status) {
1552
+ if (!status) return 'New';
1553
+
1554
+ const lowerStatus = status.toLowerCase();
1555
+ const statusMap = {
1556
+ 'open': 'New',
1557
+ 'in-progress': 'Active',
1558
+ 'done': 'Resolved',
1559
+ 'completed': 'Resolved',
1560
+ 'closed': 'Closed'
1561
+ };
1562
+
1563
+ return statusMap[lowerStatus] || 'New';
1564
+ }
1565
+
1566
+ /**
1567
+ * Map Azure state to local status
1568
+ * @private
1569
+ */
1570
+ _mapAzureStateToLocal(state) {
1571
+ const stateMap = {
1572
+ 'New': 'open',
1573
+ 'Active': 'in-progress',
1574
+ 'Resolved': 'done',
1575
+ 'Closed': 'closed'
1576
+ };
1577
+
1578
+ return stateMap[state] || 'open';
1579
+ }
589
1580
  }
590
1581
 
591
1582
  module.exports = IssueService;