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
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * EpicService - Epic Management Service
3
3
  *
4
- * Pure service layer for epic operations without any I/O operations.
5
- * Follows 3-layer architecture: Service (logic) -> No direct I/O
4
+ * Pure service layer for epic operations following ClaudeAutoPM patterns.
5
+ * Follows 3-layer architecture: Service (logic) -> Provider (I/O) -> CLI (presentation)
6
6
  *
7
- * Provides 12 pure business logic methods:
7
+ * Provides comprehensive epic lifecycle management:
8
8
  *
9
9
  * 1. Status & Categorization (5 methods):
10
10
  * - categorizeStatus: Categorize epic status
@@ -26,11 +26,21 @@
26
26
  * - generateEpicContent: Build complete epic markdown
27
27
  * - buildTaskSection: Format tasks as markdown list
28
28
  *
29
+ * 5. GitHub Epic Sync Methods (6 methods):
30
+ * - syncEpicToGitHub: Push epic to GitHub with conflict detection
31
+ * - syncEpicFromGitHub: Pull GitHub epic to local
32
+ * - syncEpicBidirectional: Full bidirectional sync
33
+ * - createGitHubEpic: Create GitHub issue with "epic" label
34
+ * - updateGitHubEpic: Update GitHub epic
35
+ * - getEpicSyncStatus: Get sync status for epic
36
+ *
29
37
  * Documentation Queries:
30
38
  * - mcp://context7/agile/epic-management - Epic management best practices
31
39
  * - mcp://context7/agile/task-breakdown - Task breakdown patterns
32
40
  * - mcp://context7/project-management/dependencies - Dependency management
33
41
  * - mcp://context7/markdown/frontmatter - YAML frontmatter patterns
42
+ * - mcp://context7/github/issues-api - GitHub Issues API v3 best practices
43
+ * - mcp://context7/conflict-resolution/sync - Conflict resolution strategies
34
44
  */
35
45
 
36
46
  const PRDService = require('./PRDService');
@@ -878,6 +888,1085 @@ ${prdContent}`;
878
888
  const path = require('path');
879
889
  return path.join(this.getEpicPath(epicName), 'epic.md');
880
890
  }
891
+
892
+ // ==========================================
893
+ // 7. GITHUB EPIC SYNC METHODS (6 METHODS)
894
+ // ==========================================
895
+
896
+ /**
897
+ * Sync local epic to GitHub (enhanced push with conflict detection)
898
+ *
899
+ * @param {string} epicName - Local epic name
900
+ * @param {Object} [options={}] - Sync options
901
+ * @param {boolean} [options.detectConflicts=false] - Enable conflict detection
902
+ * @returns {Promise<Object>} Result: { success, epicName, githubNumber, action, conflict? }
903
+ * @throws {Error} If no provider configured or epic not found
904
+ */
905
+ async syncEpicToGitHub(epicName, options = {}) {
906
+ if (!this.provider) {
907
+ throw new Error('No provider configured for GitHub sync');
908
+ }
909
+
910
+ const fs = require('fs-extra');
911
+ const epicFilePath = this.getEpicFilePath(epicName);
912
+
913
+ // Check if epic exists
914
+ const exists = await fs.pathExists(epicFilePath);
915
+ if (!exists) {
916
+ throw new Error(`Epic not found: ${epicName}`);
917
+ }
918
+
919
+ // Read epic content
920
+ const content = await fs.readFile(epicFilePath, 'utf8');
921
+ const metadata = this.parseFrontmatter(content);
922
+
923
+ // Extract overview and tasks from content
924
+ const overviewMatch = content.match(/## Overview\s+([\s\S]*?)(?=\n## |$)/);
925
+ const overview = overviewMatch ? overviewMatch[1].trim() : '';
926
+
927
+ const tasksMatch = content.match(/## Tasks?\s+([\s\S]*?)(?=\n## |$)/);
928
+ const tasksContent = tasksMatch ? tasksMatch[1].trim() : '';
929
+
930
+ // Parse tasks from markdown checkboxes
931
+ const tasks = this._parseTasksFromContent(tasksContent);
932
+
933
+ const epicData = {
934
+ name: epicName,
935
+ title: metadata?.name || epicName,
936
+ overview,
937
+ tasks,
938
+ priority: metadata?.priority || 'P2',
939
+ status: metadata?.status || 'planning',
940
+ updated: metadata?.updated
941
+ };
942
+
943
+ const syncMap = await this._loadEpicSyncMap();
944
+ const githubNumber = syncMap['epic-to-github'][epicName];
945
+
946
+ let result;
947
+ let action;
948
+
949
+ if (githubNumber) {
950
+ // Check for conflicts if enabled
951
+ if (options.detectConflicts) {
952
+ const githubIssue = await this.provider.getIssue(githubNumber);
953
+ const conflict = this._detectEpicConflict(epicData, githubIssue);
954
+
955
+ if (conflict.hasConflict && conflict.remoteNewer) {
956
+ return {
957
+ success: false,
958
+ epicName,
959
+ githubNumber: String(githubNumber),
960
+ conflict
961
+ };
962
+ }
963
+ }
964
+
965
+ // Update existing GitHub epic
966
+ result = await this.updateGitHubEpic(githubNumber, epicData);
967
+ action = 'updated';
968
+ } else {
969
+ // Create new GitHub epic
970
+ result = await this.createGitHubEpic(epicData);
971
+ action = 'created';
972
+ }
973
+
974
+ // Update sync-map
975
+ await this._updateEpicSyncMap(epicName, String(result.number));
976
+
977
+ return {
978
+ success: true,
979
+ epicName,
980
+ githubNumber: String(result.number),
981
+ action
982
+ };
983
+ }
984
+
985
+ /**
986
+ * Sync GitHub epic to local (enhanced pull with merge)
987
+ *
988
+ * @param {number|string} githubNumber - GitHub issue number
989
+ * @param {Object} [options={}] - Sync options
990
+ * @param {boolean} [options.detectConflicts=false] - Enable conflict detection
991
+ * @returns {Promise<Object>} Result: { success, epicName, githubNumber, action, conflict? }
992
+ * @throws {Error} If issue is not an epic
993
+ */
994
+ async syncEpicFromGitHub(githubNumber, options = {}) {
995
+ const fs = require('fs-extra');
996
+ const path = require('path');
997
+
998
+ const githubIssue = await this.provider.getIssue(githubNumber);
999
+
1000
+ // Verify it's an epic
1001
+ const labels = githubIssue.labels || [];
1002
+ const isEpic = labels.some(label => {
1003
+ const name = typeof label === 'string' ? label : label.name;
1004
+ return name === 'epic';
1005
+ });
1006
+
1007
+ if (!isEpic) {
1008
+ throw new Error(`GitHub issue #${githubNumber} is not an epic`);
1009
+ }
1010
+
1011
+ const syncMap = await this._loadEpicSyncMap();
1012
+ let epicName = syncMap['github-to-epic'][String(githubNumber)];
1013
+
1014
+ let action;
1015
+
1016
+ if (epicName) {
1017
+ // Check for conflicts if enabled
1018
+ if (options.detectConflicts) {
1019
+ const epicFilePath = this.getEpicFilePath(epicName);
1020
+ const localContent = await fs.readFile(epicFilePath, 'utf8');
1021
+ const localMetadata = this.parseFrontmatter(localContent);
1022
+
1023
+ const conflict = this._detectEpicConflict(
1024
+ { updated: localMetadata?.updated },
1025
+ githubIssue
1026
+ );
1027
+
1028
+ if (conflict.hasConflict && conflict.localNewer) {
1029
+ return {
1030
+ success: false,
1031
+ epicName,
1032
+ githubNumber: String(githubNumber),
1033
+ conflict
1034
+ };
1035
+ }
1036
+ }
1037
+
1038
+ action = 'updated';
1039
+ } else {
1040
+ // Generate epic name from title
1041
+ epicName = this._generateEpicNameFromTitle(githubIssue.title);
1042
+ action = 'created';
1043
+ }
1044
+
1045
+ // Parse GitHub epic
1046
+ const epicData = this._parseGitHubEpic(githubIssue);
1047
+
1048
+ // Build epic content
1049
+ const epicContent = this._buildEpicContent(epicData, githubNumber);
1050
+
1051
+ // Ensure epic directory exists
1052
+ const epicPath = this.getEpicPath(epicName);
1053
+ await fs.ensureDir(epicPath);
1054
+
1055
+ // Write epic.md
1056
+ const epicFilePath = this.getEpicFilePath(epicName);
1057
+ await fs.writeFile(epicFilePath, epicContent);
1058
+
1059
+ // Update sync-map
1060
+ await this._updateEpicSyncMap(epicName, String(githubNumber));
1061
+
1062
+ return {
1063
+ success: true,
1064
+ epicName,
1065
+ githubNumber: String(githubNumber),
1066
+ action
1067
+ };
1068
+ }
1069
+
1070
+ /**
1071
+ * Bidirectional sync - sync in the direction of newer changes
1072
+ *
1073
+ * @param {string} epicName - Local epic name
1074
+ * @param {Object} [options={}] - Sync options
1075
+ * @param {string} [options.conflictStrategy='detect'] - How to handle conflicts
1076
+ * @returns {Promise<Object>} Result: { success, direction, conflict? }
1077
+ */
1078
+ async syncEpicBidirectional(epicName, options = {}) {
1079
+ const syncMap = await this._loadEpicSyncMap();
1080
+ const githubNumber = syncMap['epic-to-github'][epicName];
1081
+
1082
+ if (!githubNumber) {
1083
+ // No GitHub mapping, push to GitHub
1084
+ const result = await this.syncEpicToGitHub(epicName);
1085
+ return { ...result, direction: 'to-github' };
1086
+ }
1087
+
1088
+ // Get both versions
1089
+ const fs = require('fs-extra');
1090
+ const epicFilePath = this.getEpicFilePath(epicName);
1091
+ const localContent = await fs.readFile(epicFilePath, 'utf8');
1092
+ const localMetadata = this.parseFrontmatter(localContent);
1093
+
1094
+ const githubIssue = await this.provider.getIssue(githubNumber);
1095
+
1096
+ const conflict = this._detectEpicConflict(
1097
+ { updated: localMetadata?.updated },
1098
+ githubIssue
1099
+ );
1100
+
1101
+ if (conflict.hasConflict) {
1102
+ if (options.conflictStrategy === 'detect') {
1103
+ return {
1104
+ success: false,
1105
+ direction: 'conflict',
1106
+ conflict
1107
+ };
1108
+ }
1109
+
1110
+ // Auto-resolve based on timestamps
1111
+ if (conflict.localNewer) {
1112
+ const result = await this.syncEpicToGitHub(epicName);
1113
+ return { ...result, direction: 'to-github' };
1114
+ } else if (conflict.remoteNewer) {
1115
+ const result = await this.syncEpicFromGitHub(githubNumber);
1116
+ return { ...result, direction: 'from-github' };
1117
+ }
1118
+ }
1119
+
1120
+ // No conflict - sync local to GitHub
1121
+ const result = await this.syncEpicToGitHub(epicName);
1122
+ return { ...result, direction: 'to-github' };
1123
+ }
1124
+
1125
+ /**
1126
+ * Create new GitHub epic from local data
1127
+ *
1128
+ * @param {Object} epicData - Epic data
1129
+ * @returns {Promise<Object>} Created GitHub issue
1130
+ */
1131
+ async createGitHubEpic(epicData) {
1132
+ const labels = ['epic'];
1133
+
1134
+ if (epicData.priority) {
1135
+ labels.push(`priority:${epicData.priority}`);
1136
+ }
1137
+
1138
+ const body = this._formatEpicForGitHub(epicData);
1139
+
1140
+ const githubData = {
1141
+ title: `Epic: ${epicData.title}`,
1142
+ body,
1143
+ labels,
1144
+ state: 'open'
1145
+ };
1146
+
1147
+ const result = await this.provider.createIssue(githubData);
1148
+
1149
+ // Update sync-map
1150
+ if (epicData.name) {
1151
+ await this._updateEpicSyncMap(epicData.name, String(result.number));
1152
+ }
1153
+
1154
+ return result;
1155
+ }
1156
+
1157
+ /**
1158
+ * Update existing GitHub epic with local data
1159
+ *
1160
+ * @param {number|string} githubNumber - GitHub issue number
1161
+ * @param {Object} epicData - Epic data
1162
+ * @returns {Promise<Object>} Updated GitHub issue
1163
+ */
1164
+ async updateGitHubEpic(githubNumber, epicData) {
1165
+ const updateData = {};
1166
+
1167
+ if (epicData.title) {
1168
+ updateData.title = `Epic: ${epicData.title}`;
1169
+ }
1170
+
1171
+ if (epicData.overview || epicData.tasks) {
1172
+ updateData.body = this._formatEpicForGitHub(epicData);
1173
+ }
1174
+
1175
+ if (epicData.priority) {
1176
+ updateData.labels = ['epic', `priority:${epicData.priority}`];
1177
+ }
1178
+
1179
+ return await this.provider.updateIssue(githubNumber, updateData);
1180
+ }
1181
+
1182
+ /**
1183
+ * Get sync status for an epic
1184
+ *
1185
+ * @param {string} epicName - Local epic name
1186
+ * @returns {Promise<Object>} Status: { synced, epicName, githubNumber, lastSync, status }
1187
+ */
1188
+ async getEpicSyncStatus(epicName) {
1189
+ const syncMap = await this._loadEpicSyncMap();
1190
+ const githubNumber = syncMap['epic-to-github'][epicName];
1191
+
1192
+ if (!githubNumber) {
1193
+ return {
1194
+ synced: false,
1195
+ epicName,
1196
+ githubNumber: null,
1197
+ status: 'not-synced'
1198
+ };
1199
+ }
1200
+
1201
+ const metadata = syncMap.metadata[epicName] || {};
1202
+
1203
+ // Check if out of sync
1204
+ try {
1205
+ const fs = require('fs-extra');
1206
+ const epicFilePath = this.getEpicFilePath(epicName);
1207
+ const localContent = await fs.readFile(epicFilePath, 'utf8');
1208
+ const localMetadata = this.parseFrontmatter(localContent);
1209
+
1210
+ const githubIssue = await this.provider.getIssue(githubNumber);
1211
+
1212
+ const localTime = new Date(localMetadata?.updated || localMetadata?.created || 0);
1213
+ const githubTime = new Date(githubIssue.updated_at || githubIssue.created_at || 0);
1214
+ const lastSyncTime = new Date(metadata.lastSync || 0);
1215
+
1216
+ const isOutOfSync = localTime > lastSyncTime || githubTime > lastSyncTime;
1217
+
1218
+ return {
1219
+ synced: !isOutOfSync,
1220
+ epicName,
1221
+ githubNumber: String(githubNumber),
1222
+ lastSync: metadata.lastSync,
1223
+ status: isOutOfSync ? 'out-of-sync' : 'synced'
1224
+ };
1225
+ } catch (error) {
1226
+ // If error checking, assume synced
1227
+ return {
1228
+ synced: true,
1229
+ epicName,
1230
+ githubNumber: String(githubNumber),
1231
+ lastSync: metadata.lastSync,
1232
+ status: 'synced'
1233
+ };
1234
+ }
1235
+ }
1236
+
1237
+ // ==========================================
1238
+ // PRIVATE HELPER METHODS FOR EPIC SYNC
1239
+ // ==========================================
1240
+
1241
+ /**
1242
+ * Load epic-sync-map from file
1243
+ * @private
1244
+ */
1245
+ async _loadEpicSyncMap() {
1246
+ const fs = require('fs-extra');
1247
+ const path = require('path');
1248
+ const syncMapPath = path.join(process.cwd(), '.claude/epic-sync-map.json');
1249
+
1250
+ if (await fs.pathExists(syncMapPath)) {
1251
+ return await fs.readJSON(syncMapPath);
1252
+ }
1253
+
1254
+ return {
1255
+ 'epic-to-github': {},
1256
+ 'github-to-epic': {},
1257
+ 'metadata': {}
1258
+ };
1259
+ }
1260
+
1261
+ /**
1262
+ * Save epic-sync-map to file
1263
+ * @private
1264
+ */
1265
+ async _saveEpicSyncMap(syncMap) {
1266
+ const fs = require('fs-extra');
1267
+ const path = require('path');
1268
+ const syncMapPath = path.join(process.cwd(), '.claude/epic-sync-map.json');
1269
+ await fs.writeJSON(syncMapPath, syncMap, { spaces: 2 });
1270
+ }
1271
+
1272
+ /**
1273
+ * Update epic-sync-map with new mapping
1274
+ * @private
1275
+ */
1276
+ async _updateEpicSyncMap(epicName, githubNumber) {
1277
+ const syncMap = await this._loadEpicSyncMap();
1278
+
1279
+ syncMap['epic-to-github'][epicName] = String(githubNumber);
1280
+ syncMap['github-to-epic'][String(githubNumber)] = epicName;
1281
+ syncMap['metadata'][epicName] = {
1282
+ lastSync: new Date().toISOString(),
1283
+ githubNumber: String(githubNumber)
1284
+ };
1285
+
1286
+ await this._saveEpicSyncMap(syncMap);
1287
+ }
1288
+
1289
+ /**
1290
+ * Format epic data as GitHub issue body
1291
+ * @private
1292
+ */
1293
+ _formatEpicForGitHub(epicData) {
1294
+ let body = '';
1295
+
1296
+ if (epicData.overview) {
1297
+ body += `## Overview\n${epicData.overview}\n\n`;
1298
+ }
1299
+
1300
+ body += '## Task Breakdown\n';
1301
+
1302
+ if (epicData.tasks && epicData.tasks.length > 0) {
1303
+ epicData.tasks.forEach(task => {
1304
+ const checkbox = task.status === 'closed' ? '[x]' : '[ ]';
1305
+ body += `- ${checkbox} ${task.title}\n`;
1306
+ });
1307
+ } else {
1308
+ body += 'No tasks defined yet.\n';
1309
+ }
1310
+
1311
+ return body;
1312
+ }
1313
+
1314
+ /**
1315
+ * Parse GitHub issue to epic format
1316
+ * @private
1317
+ */
1318
+ _parseGitHubEpic(githubIssue) {
1319
+ // Extract epic name from title
1320
+ const titleMatch = githubIssue.title.match(/Epic:\s*(.+)/i);
1321
+ const title = titleMatch ? titleMatch[1].trim() : githubIssue.title;
1322
+ const name = this._generateEpicNameFromTitle(title);
1323
+
1324
+ // Extract overview
1325
+ const overviewMatch = githubIssue.body?.match(/## Overview\s+([\s\S]*?)(?=\n## |$)/);
1326
+ const overview = overviewMatch ? overviewMatch[1].trim() : '';
1327
+
1328
+ // Extract priority from labels
1329
+ const labels = githubIssue.labels || [];
1330
+ let priority = 'P2';
1331
+ labels.forEach(label => {
1332
+ const labelName = typeof label === 'string' ? label : label.name;
1333
+ const priorityMatch = labelName.match(/priority:(P\d)/i);
1334
+ if (priorityMatch) {
1335
+ priority = priorityMatch[1];
1336
+ }
1337
+ });
1338
+
1339
+ // Extract tasks from checkboxes
1340
+ const tasksMatch = githubIssue.body?.match(/## Task Breakdown\s+([\s\S]*?)(?=\n## |$)/);
1341
+ const tasksContent = tasksMatch ? tasksMatch[1].trim() : '';
1342
+ const tasks = this._parseTasksFromContent(tasksContent);
1343
+
1344
+ return {
1345
+ name,
1346
+ title,
1347
+ overview,
1348
+ priority,
1349
+ tasks,
1350
+ created: githubIssue.created_at,
1351
+ updated: githubIssue.updated_at
1352
+ };
1353
+ }
1354
+
1355
+ /**
1356
+ * Parse tasks from markdown checkbox content
1357
+ * @private
1358
+ */
1359
+ _parseTasksFromContent(content) {
1360
+ const tasks = [];
1361
+ const lines = content.split('\n');
1362
+
1363
+ for (const line of lines) {
1364
+ const checkboxMatch = line.match(/^-\s+\[([ x])\]\s+(.+)$/i);
1365
+ if (checkboxMatch) {
1366
+ const status = checkboxMatch[1].toLowerCase() === 'x' ? 'closed' : 'open';
1367
+ const title = checkboxMatch[2].trim();
1368
+ tasks.push({ title, status });
1369
+ }
1370
+ }
1371
+
1372
+ return tasks;
1373
+ }
1374
+
1375
+ /**
1376
+ * Generate epic name from title
1377
+ * @private
1378
+ */
1379
+ _generateEpicNameFromTitle(title) {
1380
+ return title
1381
+ .toLowerCase()
1382
+ .replace(/[^a-z0-9\s-]/g, '')
1383
+ .replace(/\s+/g, '-')
1384
+ .replace(/-+/g, '-')
1385
+ .replace(/^-|-$/g, '');
1386
+ }
1387
+
1388
+ /**
1389
+ * Build epic content from parsed data
1390
+ * @private
1391
+ */
1392
+ _buildEpicContent(epicData, githubNumber) {
1393
+ const frontmatter = `---
1394
+ name: ${epicData.name}
1395
+ status: planning
1396
+ priority: ${epicData.priority}
1397
+ created: ${epicData.created}
1398
+ updated: ${epicData.updated}
1399
+ progress: 0%
1400
+ github: https://github.com/owner/repo/issues/${githubNumber}
1401
+ ---
1402
+
1403
+ # Epic: ${epicData.title}
1404
+
1405
+ ## Overview
1406
+ ${epicData.overview || 'No overview provided.'}
1407
+
1408
+ ## Tasks
1409
+ ${epicData.tasks.map(task => {
1410
+ const checkbox = task.status === 'closed' ? '[x]' : '[ ]';
1411
+ return `- ${checkbox} ${task.title}`;
1412
+ }).join('\n')}
1413
+ `;
1414
+
1415
+ return frontmatter;
1416
+ }
1417
+
1418
+ /**
1419
+ * Detect conflict between local epic and GitHub issue
1420
+ * @private
1421
+ */
1422
+ _detectEpicConflict(localEpic, githubIssue) {
1423
+ const localTime = new Date(localEpic.updated || localEpic.created || 0);
1424
+ const githubTime = new Date(githubIssue.updated_at || githubIssue.created_at || 0);
1425
+
1426
+ const hasConflict = localTime.getTime() !== githubTime.getTime();
1427
+ const localNewer = localTime > githubTime;
1428
+ const remoteNewer = githubTime > localTime;
1429
+
1430
+ return {
1431
+ hasConflict,
1432
+ localNewer,
1433
+ remoteNewer,
1434
+ conflictFields: []
1435
+ };
1436
+ }
1437
+
1438
+ // ==========================================
1439
+ // 8. AZURE DEVOPS EPIC SYNC METHODS (6 METHODS)
1440
+ // ==========================================
1441
+
1442
+ /**
1443
+ * Sync local epic to Azure DevOps (enhanced push with conflict detection)
1444
+ *
1445
+ * @param {string} epicName - Local epic name
1446
+ * @param {Object} [options={}] - Sync options
1447
+ * @param {boolean} [options.detectConflicts=false] - Enable conflict detection
1448
+ * @returns {Promise<Object>} Result: { success, epicName, workItemId, action, conflict? }
1449
+ * @throws {Error} If no provider configured or epic not found
1450
+ */
1451
+ async syncEpicToAzure(epicName, options = {}) {
1452
+ if (!this.provider) {
1453
+ throw new Error('No provider configured for Azure sync');
1454
+ }
1455
+
1456
+ const fs = require('fs-extra');
1457
+ const epicFilePath = this.getEpicFilePath(epicName);
1458
+
1459
+ // Check if epic exists
1460
+ const exists = await fs.pathExists(epicFilePath);
1461
+ if (!exists) {
1462
+ throw new Error(`Epic not found: ${epicName}`);
1463
+ }
1464
+
1465
+ // Read epic content
1466
+ const content = await fs.readFile(epicFilePath, 'utf8');
1467
+ const metadata = this.parseFrontmatter(content);
1468
+
1469
+ // Extract overview and tasks from content
1470
+ const overviewMatch = content.match(/## Overview\s+([\s\S]*?)(?=\n## |$)/);
1471
+ const overview = overviewMatch ? overviewMatch[1].trim() : '';
1472
+
1473
+ const tasksMatch = content.match(/## Tasks?\s+([\s\S]*?)(?=\n## |$)/);
1474
+ const tasksContent = tasksMatch ? tasksMatch[1].trim() : '';
1475
+
1476
+ // Parse tasks from markdown checkboxes
1477
+ const tasks = this._parseTasksFromContent(tasksContent);
1478
+
1479
+ const epicData = {
1480
+ name: epicName,
1481
+ title: metadata?.name || epicName,
1482
+ overview,
1483
+ tasks,
1484
+ priority: metadata?.priority || 'P2',
1485
+ status: metadata?.status || 'planning',
1486
+ updated: metadata?.updated
1487
+ };
1488
+
1489
+ const syncMap = await this._loadAzureEpicSyncMap();
1490
+ const workItemId = syncMap['epic-to-azure'][epicName];
1491
+
1492
+ let result;
1493
+ let action;
1494
+
1495
+ if (workItemId) {
1496
+ // Check for conflicts if enabled
1497
+ if (options.detectConflicts) {
1498
+ const azureWorkItem = await this.provider.getWorkItem(workItemId);
1499
+ const conflict = this._detectAzureEpicConflict(epicData, azureWorkItem);
1500
+
1501
+ if (conflict.hasConflict && conflict.remoteNewer) {
1502
+ return {
1503
+ success: false,
1504
+ epicName,
1505
+ workItemId: String(workItemId),
1506
+ conflict
1507
+ };
1508
+ }
1509
+ }
1510
+
1511
+ // Update existing Azure epic
1512
+ result = await this.updateAzureEpic(workItemId, epicData);
1513
+ action = 'updated';
1514
+ } else {
1515
+ // Create new Azure epic
1516
+ result = await this.createAzureEpic(epicData);
1517
+ action = 'created';
1518
+ }
1519
+
1520
+ // Update sync-map
1521
+ await this._updateAzureEpicSyncMap(epicName, String(result.id));
1522
+
1523
+ return {
1524
+ success: true,
1525
+ epicName,
1526
+ workItemId: String(result.id),
1527
+ action
1528
+ };
1529
+ }
1530
+
1531
+ /**
1532
+ * Sync Azure DevOps epic to local (enhanced pull with merge)
1533
+ *
1534
+ * @param {number|string} workItemId - Azure work item ID
1535
+ * @param {Object} [options={}] - Sync options
1536
+ * @param {boolean} [options.detectConflicts=false] - Enable conflict detection
1537
+ * @returns {Promise<Object>} Result: { success, epicName, workItemId, action, conflict? }
1538
+ * @throws {Error} If work item is not an Epic
1539
+ */
1540
+ async syncEpicFromAzure(workItemId, options = {}) {
1541
+ const fs = require('fs-extra');
1542
+ const path = require('path');
1543
+
1544
+ const azureWorkItem = await this.provider.getWorkItem(workItemId);
1545
+
1546
+ // Verify it's an Epic
1547
+ const workItemType = azureWorkItem.fields['System.WorkItemType'];
1548
+ if (workItemType !== 'Epic') {
1549
+ throw new Error(`Azure work item #${workItemId} is not an Epic (type: ${workItemType})`);
1550
+ }
1551
+
1552
+ const syncMap = await this._loadAzureEpicSyncMap();
1553
+ let epicName = syncMap['azure-to-epic'][String(workItemId)];
1554
+
1555
+ let action;
1556
+
1557
+ if (epicName) {
1558
+ // Check for conflicts if enabled
1559
+ if (options.detectConflicts) {
1560
+ const epicFilePath = this.getEpicFilePath(epicName);
1561
+ const localContent = await fs.readFile(epicFilePath, 'utf8');
1562
+ const localMetadata = this.parseFrontmatter(localContent);
1563
+
1564
+ const conflict = this._detectAzureEpicConflict(
1565
+ { updated: localMetadata?.updated },
1566
+ azureWorkItem
1567
+ );
1568
+
1569
+ if (conflict.hasConflict && conflict.localNewer) {
1570
+ return {
1571
+ success: false,
1572
+ epicName,
1573
+ workItemId: String(workItemId),
1574
+ conflict
1575
+ };
1576
+ }
1577
+ }
1578
+
1579
+ action = 'updated';
1580
+ } else {
1581
+ // Generate epic name from title
1582
+ const title = azureWorkItem.fields['System.Title'] || 'Untitled';
1583
+ epicName = this._generateEpicNameFromTitle(title);
1584
+ action = 'created';
1585
+ }
1586
+
1587
+ // Parse Azure epic
1588
+ const epicData = this._parseAzureEpic(azureWorkItem);
1589
+
1590
+ // Build epic content
1591
+ const epicContent = this._buildAzureEpicContent(epicData, workItemId);
1592
+
1593
+ // Ensure epic directory exists
1594
+ const epicPath = this.getEpicPath(epicName);
1595
+ await fs.ensureDir(epicPath);
1596
+
1597
+ // Write epic.md
1598
+ const epicFilePath = this.getEpicFilePath(epicName);
1599
+ await fs.writeFile(epicFilePath, epicContent);
1600
+
1601
+ // Update sync-map
1602
+ await this._updateAzureEpicSyncMap(epicName, String(workItemId));
1603
+
1604
+ return {
1605
+ success: true,
1606
+ epicName,
1607
+ workItemId: String(workItemId),
1608
+ action
1609
+ };
1610
+ }
1611
+
1612
+ /**
1613
+ * Bidirectional sync - sync in the direction of newer changes (Azure)
1614
+ *
1615
+ * @param {string} epicName - Local epic name
1616
+ * @param {Object} [options={}] - Sync options
1617
+ * @param {string} [options.conflictStrategy='detect'] - How to handle conflicts
1618
+ * @returns {Promise<Object>} Result: { success, direction, conflict? }
1619
+ */
1620
+ async syncEpicBidirectionalAzure(epicName, options = {}) {
1621
+ const syncMap = await this._loadAzureEpicSyncMap();
1622
+ const workItemId = syncMap['epic-to-azure'][epicName];
1623
+
1624
+ if (!workItemId) {
1625
+ // No Azure mapping, push to Azure
1626
+ const result = await this.syncEpicToAzure(epicName);
1627
+ return { ...result, direction: 'to-azure' };
1628
+ }
1629
+
1630
+ // Get both versions
1631
+ const fs = require('fs-extra');
1632
+ const epicFilePath = this.getEpicFilePath(epicName);
1633
+ const localContent = await fs.readFile(epicFilePath, 'utf8');
1634
+ const localMetadata = this.parseFrontmatter(localContent);
1635
+
1636
+ const azureWorkItem = await this.provider.getWorkItem(workItemId);
1637
+
1638
+ const conflict = this._detectAzureEpicConflict(
1639
+ { updated: localMetadata?.updated },
1640
+ azureWorkItem
1641
+ );
1642
+
1643
+ if (conflict.hasConflict) {
1644
+ if (options.conflictStrategy === 'detect') {
1645
+ return {
1646
+ success: false,
1647
+ direction: 'conflict',
1648
+ conflict
1649
+ };
1650
+ }
1651
+
1652
+ // Auto-resolve based on timestamps
1653
+ if (conflict.localNewer) {
1654
+ const result = await this.syncEpicToAzure(epicName);
1655
+ return { ...result, direction: 'to-azure' };
1656
+ } else if (conflict.remoteNewer) {
1657
+ const result = await this.syncEpicFromAzure(workItemId);
1658
+ return { ...result, direction: 'from-azure' };
1659
+ }
1660
+ }
1661
+
1662
+ // No conflict - sync local to Azure
1663
+ const result = await this.syncEpicToAzure(epicName);
1664
+ return { ...result, direction: 'to-azure' };
1665
+ }
1666
+
1667
+ /**
1668
+ * Create new Azure epic work item from local data
1669
+ *
1670
+ * @param {Object} epicData - Epic data
1671
+ * @returns {Promise<Object>} Created Azure work item
1672
+ */
1673
+ async createAzureEpic(epicData) {
1674
+ const tags = epicData.priority ? `priority:${epicData.priority}` : '';
1675
+ const description = this._formatEpicForAzure(epicData);
1676
+
1677
+ const azureData = {
1678
+ title: epicData.title,
1679
+ description,
1680
+ tags,
1681
+ state: 'New'
1682
+ };
1683
+
1684
+ const result = await this.provider.createWorkItem('Epic', azureData);
1685
+
1686
+ // Update sync-map if epic name exists
1687
+ if (epicData.name) {
1688
+ await this._updateAzureEpicSyncMap(epicData.name, String(result.id));
1689
+ }
1690
+
1691
+ return result;
1692
+ }
1693
+
1694
+ /**
1695
+ * Update existing Azure epic with local data
1696
+ *
1697
+ * @param {number|string} workItemId - Azure work item ID
1698
+ * @param {Object} epicData - Epic data
1699
+ * @returns {Promise<Object>} Updated Azure work item
1700
+ */
1701
+ async updateAzureEpic(workItemId, epicData) {
1702
+ const updateData = {};
1703
+
1704
+ if (epicData.title) {
1705
+ updateData.title = epicData.title;
1706
+ }
1707
+
1708
+ if (epicData.status) {
1709
+ updateData.state = this._mapEpicStatusToAzure(epicData.status);
1710
+ }
1711
+
1712
+ if (epicData.overview || epicData.tasks) {
1713
+ updateData.description = this._formatEpicForAzure(epicData);
1714
+ }
1715
+
1716
+ if (epicData.priority) {
1717
+ updateData.tags = `priority:${epicData.priority}`;
1718
+ }
1719
+
1720
+ return await this.provider.updateWorkItem(workItemId, updateData);
1721
+ }
1722
+
1723
+ /**
1724
+ * Get sync status for an epic (Azure)
1725
+ *
1726
+ * @param {string} epicName - Local epic name
1727
+ * @returns {Promise<Object>} Status: { synced, epicName, workItemId, lastSync, status }
1728
+ */
1729
+ async getEpicAzureSyncStatus(epicName) {
1730
+ const syncMap = await this._loadAzureEpicSyncMap();
1731
+ const workItemId = syncMap['epic-to-azure'][epicName];
1732
+
1733
+ if (!workItemId) {
1734
+ return {
1735
+ synced: false,
1736
+ epicName,
1737
+ workItemId: null,
1738
+ status: 'not-synced'
1739
+ };
1740
+ }
1741
+
1742
+ const metadata = syncMap.metadata[epicName] || {};
1743
+
1744
+ // Check if out of sync
1745
+ try {
1746
+ const fs = require('fs-extra');
1747
+ const epicFilePath = this.getEpicFilePath(epicName);
1748
+ const localContent = await fs.readFile(epicFilePath, 'utf8');
1749
+ const localMetadata = this.parseFrontmatter(localContent);
1750
+
1751
+ const azureWorkItem = await this.provider.getWorkItem(workItemId);
1752
+
1753
+ const localTime = new Date(localMetadata?.updated || localMetadata?.created || 0);
1754
+ const azureTime = new Date(
1755
+ azureWorkItem.fields['System.ChangedDate'] ||
1756
+ azureWorkItem.fields['System.CreatedDate'] ||
1757
+ 0
1758
+ );
1759
+ const lastSyncTime = new Date(metadata.lastSync || 0);
1760
+
1761
+ const isOutOfSync = localTime > lastSyncTime || azureTime > lastSyncTime;
1762
+
1763
+ return {
1764
+ synced: !isOutOfSync,
1765
+ epicName,
1766
+ workItemId: String(workItemId),
1767
+ lastSync: metadata.lastSync,
1768
+ status: isOutOfSync ? 'out-of-sync' : 'synced'
1769
+ };
1770
+ } catch (error) {
1771
+ // If error checking, assume synced
1772
+ return {
1773
+ synced: true,
1774
+ epicName,
1775
+ workItemId: String(workItemId),
1776
+ lastSync: metadata.lastSync,
1777
+ status: 'synced'
1778
+ };
1779
+ }
1780
+ }
1781
+
1782
+ // ==========================================
1783
+ // PRIVATE HELPER METHODS FOR AZURE EPIC SYNC
1784
+ // ==========================================
1785
+
1786
+ /**
1787
+ * Load epic-azure-sync-map from file
1788
+ * @private
1789
+ */
1790
+ async _loadAzureEpicSyncMap() {
1791
+ const fs = require('fs-extra');
1792
+ const path = require('path');
1793
+ const syncMapPath = path.join(process.cwd(), '.claude/epic-azure-sync-map.json');
1794
+
1795
+ if (await fs.pathExists(syncMapPath)) {
1796
+ return await fs.readJSON(syncMapPath);
1797
+ }
1798
+
1799
+ return {
1800
+ 'epic-to-azure': {},
1801
+ 'azure-to-epic': {},
1802
+ 'metadata': {}
1803
+ };
1804
+ }
1805
+
1806
+ /**
1807
+ * Save epic-azure-sync-map to file
1808
+ * @private
1809
+ */
1810
+ async _saveAzureEpicSyncMap(syncMap) {
1811
+ const fs = require('fs-extra');
1812
+ const path = require('path');
1813
+ const syncMapPath = path.join(process.cwd(), '.claude/epic-azure-sync-map.json');
1814
+ await fs.writeJSON(syncMapPath, syncMap, { spaces: 2 });
1815
+ }
1816
+
1817
+ /**
1818
+ * Update epic-azure-sync-map with new mapping
1819
+ * @private
1820
+ */
1821
+ async _updateAzureEpicSyncMap(epicName, workItemId) {
1822
+ const syncMap = await this._loadAzureEpicSyncMap();
1823
+
1824
+ syncMap['epic-to-azure'][epicName] = String(workItemId);
1825
+ syncMap['azure-to-epic'][String(workItemId)] = epicName;
1826
+ syncMap['metadata'][epicName] = {
1827
+ lastSync: new Date().toISOString(),
1828
+ workItemId: String(workItemId),
1829
+ workItemType: 'Epic'
1830
+ };
1831
+
1832
+ await this._saveAzureEpicSyncMap(syncMap);
1833
+ }
1834
+
1835
+ /**
1836
+ * Format epic data as Azure work item description
1837
+ * @private
1838
+ */
1839
+ _formatEpicForAzure(epicData) {
1840
+ let description = '';
1841
+
1842
+ if (epicData.overview) {
1843
+ description += `${epicData.overview}\n\n`;
1844
+ }
1845
+
1846
+ description += '## Tasks\n';
1847
+
1848
+ if (epicData.tasks && epicData.tasks.length > 0) {
1849
+ epicData.tasks.forEach(task => {
1850
+ const checkbox = task.status === 'closed' ? '[x]' : '[ ]';
1851
+ description += `- ${checkbox} ${task.title}\n`;
1852
+ });
1853
+ } else {
1854
+ description += 'No tasks defined yet.\n';
1855
+ }
1856
+
1857
+ return description.trim();
1858
+ }
1859
+
1860
+ /**
1861
+ * Parse Azure work item to epic format
1862
+ * @private
1863
+ */
1864
+ _parseAzureEpic(azureWorkItem) {
1865
+ const title = azureWorkItem.fields['System.Title'] || '';
1866
+ const name = this._generateEpicNameFromTitle(title);
1867
+ const description = azureWorkItem.fields['System.Description'] || '';
1868
+
1869
+ // Extract overview (text before ## Tasks)
1870
+ const overviewMatch = description.match(/^([\s\S]*?)(?=## Tasks|$)/);
1871
+ const overview = overviewMatch ? overviewMatch[1].trim() : '';
1872
+
1873
+ // Extract priority from tags
1874
+ const tags = azureWorkItem.fields['System.Tags'] || '';
1875
+ const priorityMatch = tags.match(/priority:(P\d)/i);
1876
+ const priority = priorityMatch ? priorityMatch[1] : 'P2';
1877
+
1878
+ // Extract tasks from checkboxes
1879
+ const tasksMatch = description.match(/## Tasks\s+([\s\S]*?)$/);
1880
+ const tasksContent = tasksMatch ? tasksMatch[1].trim() : '';
1881
+ const tasks = this._parseTasksFromContent(tasksContent);
1882
+
1883
+ return {
1884
+ name,
1885
+ title,
1886
+ overview,
1887
+ priority,
1888
+ tasks,
1889
+ created: azureWorkItem.fields['System.CreatedDate'],
1890
+ updated: azureWorkItem.fields['System.ChangedDate']
1891
+ };
1892
+ }
1893
+
1894
+ /**
1895
+ * Detect conflict between local epic and Azure work item
1896
+ * @private
1897
+ */
1898
+ _detectAzureEpicConflict(localEpic, azureWorkItem) {
1899
+ const localTime = new Date(localEpic.updated || localEpic.created || 0);
1900
+ const azureTime = new Date(
1901
+ azureWorkItem.fields['System.ChangedDate'] ||
1902
+ azureWorkItem.fields['System.CreatedDate'] ||
1903
+ 0
1904
+ );
1905
+
1906
+ const hasConflict = localTime.getTime() !== azureTime.getTime();
1907
+ const localNewer = localTime > azureTime;
1908
+ const remoteNewer = azureTime > localTime;
1909
+
1910
+ return {
1911
+ hasConflict,
1912
+ localNewer,
1913
+ remoteNewer,
1914
+ conflictFields: []
1915
+ };
1916
+ }
1917
+
1918
+ /**
1919
+ * Map epic status to Azure DevOps state
1920
+ * @private
1921
+ */
1922
+ _mapEpicStatusToAzure(status) {
1923
+ if (!status) return 'New';
1924
+
1925
+ const lowerStatus = status.toLowerCase();
1926
+ const statusMap = {
1927
+ 'backlog': 'New',
1928
+ 'planning': 'New',
1929
+ 'in-progress': 'Active',
1930
+ 'in_progress': 'Active',
1931
+ 'active': 'Active',
1932
+ 'done': 'Resolved',
1933
+ 'completed': 'Resolved',
1934
+ 'closed': 'Closed'
1935
+ };
1936
+
1937
+ return statusMap[lowerStatus] || 'New';
1938
+ }
1939
+
1940
+ /**
1941
+ * Build Azure epic content from parsed data
1942
+ * @private
1943
+ */
1944
+ _buildAzureEpicContent(epicData, workItemId) {
1945
+ const frontmatter = `---
1946
+ name: ${epicData.name}
1947
+ status: planning
1948
+ priority: ${epicData.priority}
1949
+ created: ${epicData.created}
1950
+ updated: ${epicData.updated}
1951
+ progress: 0%
1952
+ azure_work_item_id: ${workItemId}
1953
+ work_item_type: Epic
1954
+ ---
1955
+
1956
+ # Epic: ${epicData.title}
1957
+
1958
+ ## Overview
1959
+ ${epicData.overview || 'No overview provided.'}
1960
+
1961
+ ## Tasks
1962
+ ${epicData.tasks.map(task => {
1963
+ const checkbox = task.status === 'closed' ? '[x]' : '[ ]';
1964
+ return `- ${checkbox} ${task.title}`;
1965
+ }).join('\n')}
1966
+ `;
1967
+
1968
+ return frontmatter;
1969
+ }
881
1970
  }
882
1971
 
883
1972
  module.exports = EpicService;