specweave 1.0.235 → 1.0.239

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 (196) hide show
  1. package/README.md +89 -193
  2. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.d.ts +37 -0
  3. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.d.ts.map +1 -0
  4. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js +176 -0
  5. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js.map +1 -0
  6. package/dist/plugins/specweave-github/lib/github-batch-sync.d.ts +36 -0
  7. package/dist/plugins/specweave-github/lib/github-batch-sync.d.ts.map +1 -0
  8. package/dist/plugins/specweave-github/lib/github-batch-sync.js +115 -0
  9. package/dist/plugins/specweave-github/lib/github-batch-sync.js.map +1 -0
  10. package/dist/plugins/specweave-github/lib/github-board-resolver-v2.d.ts +37 -0
  11. package/dist/plugins/specweave-github/lib/github-board-resolver-v2.d.ts.map +1 -0
  12. package/dist/plugins/specweave-github/lib/github-board-resolver-v2.js +56 -0
  13. package/dist/plugins/specweave-github/lib/github-board-resolver-v2.js.map +1 -0
  14. package/dist/plugins/specweave-github/lib/github-conflict-resolver.d.ts +68 -0
  15. package/dist/plugins/specweave-github/lib/github-conflict-resolver.d.ts.map +1 -0
  16. package/dist/plugins/specweave-github/lib/github-conflict-resolver.js +102 -0
  17. package/dist/plugins/specweave-github/lib/github-conflict-resolver.js.map +1 -0
  18. package/dist/plugins/specweave-github/lib/github-cross-repo-sync.d.ts +64 -0
  19. package/dist/plugins/specweave-github/lib/github-cross-repo-sync.d.ts.map +1 -0
  20. package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js +162 -0
  21. package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js.map +1 -0
  22. package/dist/plugins/specweave-github/lib/github-field-sync.d.ts +50 -0
  23. package/dist/plugins/specweave-github/lib/github-field-sync.d.ts.map +1 -0
  24. package/dist/plugins/specweave-github/lib/github-field-sync.js +107 -0
  25. package/dist/plugins/specweave-github/lib/github-field-sync.js.map +1 -0
  26. package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts +53 -0
  27. package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts.map +1 -0
  28. package/dist/plugins/specweave-github/lib/github-graphql-client.js +138 -0
  29. package/dist/plugins/specweave-github/lib/github-graphql-client.js.map +1 -0
  30. package/dist/plugins/specweave-github/lib/github-issue-body-generator.d.ts +40 -0
  31. package/dist/plugins/specweave-github/lib/github-issue-body-generator.d.ts.map +1 -0
  32. package/dist/plugins/specweave-github/lib/github-issue-body-generator.js +50 -0
  33. package/dist/plugins/specweave-github/lib/github-issue-body-generator.js.map +1 -0
  34. package/dist/plugins/specweave-github/lib/github-issue-body-parser.d.ts +30 -0
  35. package/dist/plugins/specweave-github/lib/github-issue-body-parser.d.ts.map +1 -0
  36. package/dist/plugins/specweave-github/lib/github-issue-body-parser.js +75 -0
  37. package/dist/plugins/specweave-github/lib/github-issue-body-parser.js.map +1 -0
  38. package/dist/plugins/specweave-github/lib/github-pull-sync.d.ts +94 -0
  39. package/dist/plugins/specweave-github/lib/github-pull-sync.d.ts.map +1 -0
  40. package/dist/plugins/specweave-github/lib/github-pull-sync.js +232 -0
  41. package/dist/plugins/specweave-github/lib/github-pull-sync.js.map +1 -0
  42. package/dist/plugins/specweave-github/lib/github-push-sync.d.ts +50 -0
  43. package/dist/plugins/specweave-github/lib/github-push-sync.d.ts.map +1 -0
  44. package/dist/plugins/specweave-github/lib/github-push-sync.js +114 -0
  45. package/dist/plugins/specweave-github/lib/github-push-sync.js.map +1 -0
  46. package/dist/plugins/specweave-github/lib/github-rate-limiter.d.ts +53 -0
  47. package/dist/plugins/specweave-github/lib/github-rate-limiter.d.ts.map +1 -0
  48. package/dist/plugins/specweave-github/lib/github-rate-limiter.js +109 -0
  49. package/dist/plugins/specweave-github/lib/github-rate-limiter.js.map +1 -0
  50. package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.d.ts +21 -0
  51. package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.d.ts.map +1 -0
  52. package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +161 -0
  53. package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js.map +1 -0
  54. package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts +46 -0
  55. package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts.map +1 -0
  56. package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js +99 -0
  57. package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js.map +1 -0
  58. package/dist/plugins/specweave-github/lib/github-us-auto-closer.d.ts +43 -0
  59. package/dist/plugins/specweave-github/lib/github-us-auto-closer.d.ts.map +1 -0
  60. package/dist/plugins/specweave-github/lib/github-us-auto-closer.js +153 -0
  61. package/dist/plugins/specweave-github/lib/github-us-auto-closer.js.map +1 -0
  62. package/dist/plugins/specweave-github/lib/index.d.ts +1 -4
  63. package/dist/plugins/specweave-github/lib/index.d.ts.map +1 -1
  64. package/dist/plugins/specweave-github/lib/index.js +1 -4
  65. package/dist/plugins/specweave-github/lib/index.js.map +1 -1
  66. package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.d.ts +7 -0
  67. package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.d.ts.map +1 -0
  68. package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.js +15 -0
  69. package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.js.map +1 -0
  70. package/dist/plugins/specweave-testing/lib/playwright-cli-detector.d.ts +10 -0
  71. package/dist/plugins/specweave-testing/lib/playwright-cli-detector.d.ts.map +1 -0
  72. package/dist/plugins/specweave-testing/lib/playwright-cli-detector.js +36 -0
  73. package/dist/plugins/specweave-testing/lib/playwright-cli-detector.js.map +1 -0
  74. package/dist/plugins/specweave-testing/lib/playwright-cli-runner.d.ts +25 -0
  75. package/dist/plugins/specweave-testing/lib/playwright-cli-runner.d.ts.map +1 -0
  76. package/dist/plugins/specweave-testing/lib/playwright-cli-runner.js +57 -0
  77. package/dist/plugins/specweave-testing/lib/playwright-cli-runner.js.map +1 -0
  78. package/dist/plugins/specweave-testing/lib/playwright-routing.d.ts +7 -0
  79. package/dist/plugins/specweave-testing/lib/playwright-routing.d.ts.map +1 -0
  80. package/dist/plugins/specweave-testing/lib/playwright-routing.js +17 -0
  81. package/dist/plugins/specweave-testing/lib/playwright-routing.js.map +1 -0
  82. package/dist/src/cli/commands/auto.d.ts.map +1 -1
  83. package/dist/src/cli/commands/auto.js +1 -2
  84. package/dist/src/cli/commands/auto.js.map +1 -1
  85. package/dist/src/cli/commands/cancel-auto.js +1 -2
  86. package/dist/src/cli/commands/cancel-auto.js.map +1 -1
  87. package/dist/src/cli/commands/living-docs.js +2 -2
  88. package/dist/src/cli/commands/living-docs.js.map +1 -1
  89. package/dist/src/cli/commands/update.d.ts.map +1 -1
  90. package/dist/src/cli/commands/update.js +1 -2
  91. package/dist/src/cli/commands/update.js.map +1 -1
  92. package/dist/src/core/config/types.d.ts +8 -0
  93. package/dist/src/core/config/types.d.ts.map +1 -1
  94. package/dist/src/core/config/types.js +3 -0
  95. package/dist/src/core/config/types.js.map +1 -1
  96. package/dist/src/core/types/sync-profile.d.ts +72 -0
  97. package/dist/src/core/types/sync-profile.d.ts.map +1 -1
  98. package/dist/src/core/types/sync-profile.js +6 -0
  99. package/dist/src/core/types/sync-profile.js.map +1 -1
  100. package/package.json +2 -2
  101. package/plugins/specweave/hooks/hooks.json +2 -2
  102. package/plugins/specweave/hooks/startup-health-check.sh +1 -1
  103. package/plugins/specweave/hooks/stop-auto-v5.sh +166 -0
  104. package/plugins/specweave/hooks/user-prompt-submit.sh +10 -0
  105. package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +21 -1
  106. package/plugins/specweave/hooks/v2/dispatchers/session-start.sh +1 -1
  107. package/plugins/specweave/skills/auto/SKILL.md +71 -251
  108. package/plugins/specweave/skills/team-build/SKILL.md +370 -0
  109. package/plugins/specweave/skills/team-merge/SKILL.md +123 -0
  110. package/plugins/specweave/skills/team-orchestrate/SKILL.md +800 -0
  111. package/plugins/specweave/skills/team-status/SKILL.md +89 -0
  112. package/plugins/specweave-github/MULTI-PROJECT-SYNC-ARCHITECTURE.md +94 -8
  113. package/plugins/specweave-github/commands/sync.md +17 -3
  114. package/plugins/specweave-github/hooks/github-ac-sync-handler.sh +255 -0
  115. package/plugins/specweave-github/hooks/github-auto-create-handler.sh +455 -0
  116. package/plugins/specweave-github/lib/github-ac-comment-poster.js +150 -0
  117. package/plugins/specweave-github/lib/github-ac-comment-poster.ts +245 -0
  118. package/plugins/specweave-github/lib/github-batch-sync.js +93 -0
  119. package/plugins/specweave-github/lib/github-batch-sync.ts +152 -0
  120. package/plugins/specweave-github/lib/github-board-resolver-v2.js +47 -0
  121. package/plugins/specweave-github/lib/github-board-resolver-v2.ts +73 -0
  122. package/plugins/specweave-github/lib/github-conflict-resolver.js +90 -0
  123. package/plugins/specweave-github/lib/github-conflict-resolver.ts +154 -0
  124. package/plugins/specweave-github/lib/github-cross-repo-sync.js +168 -0
  125. package/plugins/specweave-github/lib/github-cross-repo-sync.ts +252 -0
  126. package/plugins/specweave-github/lib/github-field-sync.js +116 -0
  127. package/plugins/specweave-github/lib/github-field-sync.ts +165 -0
  128. package/plugins/specweave-github/lib/github-graphql-client.js +129 -0
  129. package/plugins/specweave-github/lib/github-graphql-client.ts +181 -0
  130. package/plugins/specweave-github/lib/github-issue-body-generator.js +30 -0
  131. package/plugins/specweave-github/lib/github-issue-body-generator.ts +76 -0
  132. package/plugins/specweave-github/lib/github-issue-body-parser.js +55 -0
  133. package/plugins/specweave-github/lib/github-issue-body-parser.ts +92 -0
  134. package/plugins/specweave-github/lib/github-pull-sync.js +185 -0
  135. package/plugins/specweave-github/lib/github-pull-sync.ts +343 -0
  136. package/plugins/specweave-github/lib/github-push-sync.js +119 -0
  137. package/plugins/specweave-github/lib/github-push-sync.ts +174 -0
  138. package/plugins/specweave-github/lib/github-rate-limiter.js +96 -0
  139. package/plugins/specweave-github/lib/github-rate-limiter.ts +143 -0
  140. package/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +117 -0
  141. package/plugins/specweave-github/lib/github-spec-frontmatter-updater.ts +180 -0
  142. package/plugins/specweave-github/lib/github-sync-orchestrator.js +84 -0
  143. package/plugins/specweave-github/lib/github-sync-orchestrator.ts +156 -0
  144. package/plugins/specweave-github/lib/github-us-auto-closer.js +134 -0
  145. package/plugins/specweave-github/lib/github-us-auto-closer.ts +226 -0
  146. package/plugins/specweave-github/lib/index.js +1 -7
  147. package/plugins/specweave-github/lib/index.ts +1 -4
  148. package/plugins/specweave-github/skills/github-sync/SKILL.md +76 -4
  149. package/plugins/specweave-testing/commands/e2e-setup.md +18 -0
  150. package/plugins/specweave-testing/commands/ui-automate.md +2 -0
  151. package/plugins/specweave-testing/commands/ui-inspect.md +8 -0
  152. package/plugins/specweave-testing/lib/playwright-ci-defaults.d.ts +6 -0
  153. package/plugins/specweave-testing/lib/playwright-ci-defaults.js +14 -0
  154. package/plugins/specweave-testing/lib/playwright-ci-defaults.ts +24 -0
  155. package/plugins/specweave-testing/lib/playwright-cli-detector.js +33 -0
  156. package/plugins/specweave-testing/lib/playwright-cli-detector.ts +48 -0
  157. package/plugins/specweave-testing/lib/playwright-cli-runner.js +58 -0
  158. package/plugins/specweave-testing/lib/playwright-cli-runner.ts +80 -0
  159. package/plugins/specweave-testing/lib/playwright-routing.js +16 -0
  160. package/plugins/specweave-testing/lib/playwright-routing.ts +38 -0
  161. package/plugins/specweave-testing/skills/e2e-testing/SKILL.md +38 -0
  162. package/src/templates/CLAUDE.md.template +7 -0
  163. package/src/templates/config.json.template +9 -1
  164. package/dist/plugins/specweave-github/lib/subtask-sync.d.ts +0 -51
  165. package/dist/plugins/specweave-github/lib/subtask-sync.d.ts.map +0 -1
  166. package/dist/plugins/specweave-github/lib/subtask-sync.js +0 -147
  167. package/dist/plugins/specweave-github/lib/subtask-sync.js.map +0 -1
  168. package/dist/plugins/specweave-github/lib/task-parser.d.ts +0 -37
  169. package/dist/plugins/specweave-github/lib/task-parser.d.ts.map +0 -1
  170. package/dist/plugins/specweave-github/lib/task-parser.js +0 -211
  171. package/dist/plugins/specweave-github/lib/task-parser.js.map +0 -1
  172. package/dist/plugins/specweave-github/lib/task-sync.d.ts +0 -56
  173. package/dist/plugins/specweave-github/lib/task-sync.d.ts.map +0 -1
  174. package/dist/plugins/specweave-github/lib/task-sync.js +0 -375
  175. package/dist/plugins/specweave-github/lib/task-sync.js.map +0 -1
  176. package/plugins/specweave/hooks/validate-completion-conditions.sh +0 -474
  177. package/plugins/specweave-github/lib/subtask-sync.d.ts +0 -51
  178. package/plugins/specweave-github/lib/subtask-sync.d.ts.map +0 -1
  179. package/plugins/specweave-github/lib/subtask-sync.js +0 -154
  180. package/plugins/specweave-github/lib/subtask-sync.js.map +0 -1
  181. package/plugins/specweave-github/lib/subtask-sync.ts +0 -225
  182. package/plugins/specweave-github/lib/task-parser.d.js +0 -0
  183. package/plugins/specweave-github/lib/task-parser.d.ts +0 -37
  184. package/plugins/specweave-github/lib/task-parser.d.ts.map +0 -1
  185. package/plugins/specweave-github/lib/task-parser.js +0 -195
  186. package/plugins/specweave-github/lib/task-parser.js.map +0 -1
  187. package/plugins/specweave-github/lib/task-parser.ts +0 -246
  188. package/plugins/specweave-github/lib/task-sync.d.js +0 -0
  189. package/plugins/specweave-github/lib/task-sync.d.ts +0 -51
  190. package/plugins/specweave-github/lib/task-sync.d.ts.map +0 -1
  191. package/plugins/specweave-github/lib/task-sync.js +0 -415
  192. package/plugins/specweave-github/lib/task-sync.js.map +0 -1
  193. package/plugins/specweave-github/lib/task-sync.ts +0 -451
  194. package/plugins/specweave-github/skills/github-issue-tracker/SKILL.md +0 -496
  195. /package/plugins/specweave/hooks/{stop-auto.sh → _archive/stop-auto-v4-legacy.sh} +0 -0
  196. /package/plugins/{specweave-github/lib/subtask-sync.d.js → specweave-testing/lib/playwright-ci-defaults.d.js} +0 -0
@@ -0,0 +1,154 @@
1
+ /**
2
+ * GitHub Conflict Resolver
3
+ *
4
+ * Field-level conflict detection and resolution between spec state
5
+ * and GitHub issue state. Supports title, status, and AC fields.
6
+ *
7
+ * @module github-conflict-resolver
8
+ */
9
+
10
+ export type ConflictResolution = 'github-wins' | 'spec-wins' | 'prompt';
11
+
12
+ export interface ConflictField {
13
+ field: string;
14
+ specValue: string;
15
+ githubValue: string;
16
+ defaultResolution: ConflictResolution;
17
+ }
18
+
19
+ export interface ResolvedConflict {
20
+ field: string;
21
+ specValue: string;
22
+ githubValue: string;
23
+ resolution: ConflictResolution;
24
+ resolvedValue: string;
25
+ resolvedAt: string;
26
+ }
27
+
28
+ export interface ConflictResolverConfig {
29
+ defaultStatusResolution?: ConflictResolution;
30
+ defaultContentResolution?: ConflictResolution;
31
+ defaultACResolution?: ConflictResolution;
32
+ }
33
+
34
+ interface SpecState {
35
+ title: string;
36
+ status: string;
37
+ acceptanceCriteria: Array<{ id: string; completed: boolean }>;
38
+ }
39
+
40
+ interface GitHubState {
41
+ title: string;
42
+ state: 'open' | 'closed';
43
+ acceptanceCriteria: Array<{ id: string; completed: boolean }>;
44
+ }
45
+
46
+ export class GitHubConflictResolver {
47
+ private statusResolution: ConflictResolution;
48
+ private contentResolution: ConflictResolution;
49
+ private acResolution: ConflictResolution;
50
+
51
+ constructor(config?: ConflictResolverConfig) {
52
+ this.statusResolution = config?.defaultStatusResolution ?? 'github-wins';
53
+ this.contentResolution = config?.defaultContentResolution ?? 'prompt';
54
+ this.acResolution = config?.defaultACResolution ?? 'github-wins';
55
+ }
56
+
57
+ /**
58
+ * Detect conflicts between spec and GitHub states.
59
+ */
60
+ detectConflicts(specState: SpecState, githubState: GitHubState): ConflictField[] {
61
+ const conflicts: ConflictField[] = [];
62
+
63
+ // Title conflict
64
+ if (specState.title !== githubState.title) {
65
+ conflicts.push({
66
+ field: 'title',
67
+ specValue: specState.title,
68
+ githubValue: githubState.title,
69
+ defaultResolution: this.contentResolution,
70
+ });
71
+ }
72
+
73
+ // Status conflict: map GitHub state to spec status for comparison
74
+ const githubStatus = githubState.state; // 'open' or 'closed'
75
+ const specStatus = specState.status;
76
+ const statusMismatch = this.isStatusConflict(specStatus, githubStatus);
77
+
78
+ if (statusMismatch) {
79
+ conflicts.push({
80
+ field: 'status',
81
+ specValue: specStatus,
82
+ githubValue: githubStatus,
83
+ defaultResolution: this.statusResolution,
84
+ });
85
+ }
86
+
87
+ // AC conflicts
88
+ const ghAcMap = new Map(
89
+ githubState.acceptanceCriteria.map(ac => [ac.id, ac.completed]),
90
+ );
91
+
92
+ for (const specAc of specState.acceptanceCriteria) {
93
+ const ghCompleted = ghAcMap.get(specAc.id);
94
+ if (ghCompleted !== undefined && specAc.completed !== ghCompleted) {
95
+ conflicts.push({
96
+ field: `ac:${specAc.id}`,
97
+ specValue: String(specAc.completed),
98
+ githubValue: String(ghCompleted),
99
+ defaultResolution: this.acResolution,
100
+ });
101
+ }
102
+ }
103
+
104
+ return conflicts;
105
+ }
106
+
107
+ /**
108
+ * Resolve all conflicts using their default resolution strategy.
109
+ */
110
+ resolveConflicts(conflicts: ConflictField[]): ResolvedConflict[] {
111
+ return conflicts.map(c => this.resolveConflict(c));
112
+ }
113
+
114
+ /**
115
+ * Resolve a single conflict with optional override.
116
+ */
117
+ resolveConflict(conflict: ConflictField, resolution?: ConflictResolution): ResolvedConflict {
118
+ const effectiveResolution = resolution ?? conflict.defaultResolution;
119
+ let resolvedValue: string;
120
+
121
+ switch (effectiveResolution) {
122
+ case 'github-wins':
123
+ resolvedValue = conflict.githubValue;
124
+ break;
125
+ case 'spec-wins':
126
+ resolvedValue = conflict.specValue;
127
+ break;
128
+ case 'prompt':
129
+ // Prompt keeps spec value pending user decision
130
+ resolvedValue = conflict.specValue;
131
+ break;
132
+ }
133
+
134
+ return {
135
+ field: conflict.field,
136
+ specValue: conflict.specValue,
137
+ githubValue: conflict.githubValue,
138
+ resolution: effectiveResolution,
139
+ resolvedValue,
140
+ resolvedAt: new Date().toISOString(),
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Check if spec status and GitHub state represent a conflict.
146
+ */
147
+ private isStatusConflict(specStatus: string, githubState: string): boolean {
148
+ // "completed" spec + "open" GitHub = conflict
149
+ if (specStatus === 'completed' && githubState === 'open') return true;
150
+ // "planned" or "in-progress" spec + "closed" GitHub = conflict
151
+ if (specStatus !== 'completed' && githubState === 'closed') return true;
152
+ return false;
153
+ }
154
+ }
@@ -0,0 +1,168 @@
1
+ import { execFileNoThrow } from "../../../src/utils/execFileNoThrow.js";
2
+ import { generateIssueBody } from "./github-issue-body-generator.js";
3
+ async function crossRepoSync(stories, options) {
4
+ const result = {
5
+ created: [],
6
+ updated: [],
7
+ errors: [],
8
+ crossReferences: []
9
+ };
10
+ if (stories.length === 0) return result;
11
+ const env = getEnv(options.token);
12
+ const storyIssueMap = /* @__PURE__ */ new Map();
13
+ for (const story of stories) {
14
+ const repos = story.targetRepos.length > 0 ? story.targetRepos : [`${options.owner}/${options.defaultRepo}`];
15
+ const storyIssues = [];
16
+ for (const repo of repos) {
17
+ try {
18
+ const body = generateIssueBody({
19
+ id: story.id,
20
+ title: story.title,
21
+ description: story.description,
22
+ priority: story.priority,
23
+ acceptanceCriteria: story.acceptanceCriteria,
24
+ specId: story.specId
25
+ });
26
+ const title = `[${story.id}] ${story.title}`;
27
+ const existing = await searchIssue(story.id, repo, env);
28
+ if (existing) {
29
+ await updateIssue(existing.number, title, body, repo, env);
30
+ result.updated.push({
31
+ userStoryId: story.id,
32
+ repo,
33
+ issueNumber: existing.number,
34
+ issueUrl: `https://github.com/${repo}/issues/${existing.number}`
35
+ });
36
+ storyIssues.push({ repo, issueNumber: existing.number });
37
+ } else {
38
+ const created = await createIssue(title, body, story, repo, env);
39
+ result.created.push({
40
+ userStoryId: story.id,
41
+ repo,
42
+ issueNumber: created.number,
43
+ issueUrl: created.url,
44
+ issueNodeId: created.node_id
45
+ });
46
+ storyIssues.push({ repo, issueNumber: created.number });
47
+ }
48
+ } catch (err) {
49
+ result.errors.push({
50
+ userStoryId: story.id,
51
+ repo,
52
+ error: err instanceof Error ? err.message : String(err)
53
+ });
54
+ }
55
+ }
56
+ if (storyIssues.length > 1) {
57
+ storyIssueMap.set(story.id, storyIssues);
58
+ }
59
+ }
60
+ for (const [, issues] of storyIssueMap) {
61
+ for (const issue of issues) {
62
+ const linkedTo = issues.filter((i) => i.repo !== issue.repo);
63
+ result.crossReferences.push({
64
+ repo: issue.repo,
65
+ issueNumber: issue.issueNumber,
66
+ linkedTo
67
+ });
68
+ const crossRefSection = buildCrossRefSection(linkedTo);
69
+ try {
70
+ await appendToIssueBody(issue.issueNumber, issue.repo, crossRefSection, env);
71
+ } catch {
72
+ }
73
+ }
74
+ }
75
+ return result;
76
+ }
77
+ async function searchIssue(usId, repo, env) {
78
+ const res = await execFileNoThrow("gh", [
79
+ "issue",
80
+ "list",
81
+ "--repo",
82
+ repo,
83
+ "--search",
84
+ `[${usId}] in:title`,
85
+ "--json",
86
+ "number,title,node_id",
87
+ "--limit",
88
+ "1"
89
+ ], { env });
90
+ if (!res.success) {
91
+ throw new Error(`Search failed for ${repo}: ${res.stderr}`);
92
+ }
93
+ const issues = JSON.parse(res.stdout);
94
+ return issues.length > 0 ? issues[0] : null;
95
+ }
96
+ async function createIssue(title, body, story, repo, env) {
97
+ const res = await execFileNoThrow("gh", [
98
+ "issue",
99
+ "create",
100
+ "--repo",
101
+ repo,
102
+ "--title",
103
+ title,
104
+ "--body",
105
+ body,
106
+ "--label",
107
+ "user-story",
108
+ "--label",
109
+ `priority:${story.priority}`,
110
+ "--json",
111
+ "number,url,node_id"
112
+ ], { env });
113
+ if (!res.success) {
114
+ throw new Error(`Create failed for ${repo}: ${res.stderr}`);
115
+ }
116
+ return JSON.parse(res.stdout);
117
+ }
118
+ async function updateIssue(issueNumber, title, body, repo, env) {
119
+ const res = await execFileNoThrow("gh", [
120
+ "issue",
121
+ "edit",
122
+ String(issueNumber),
123
+ "--repo",
124
+ repo,
125
+ "--title",
126
+ title,
127
+ "--body",
128
+ body,
129
+ "--json",
130
+ "number,url"
131
+ ], { env });
132
+ if (!res.success) {
133
+ throw new Error(`Update failed for ${repo}: ${res.stderr}`);
134
+ }
135
+ }
136
+ async function appendToIssueBody(issueNumber, repo, section, env) {
137
+ const res = await execFileNoThrow("gh", [
138
+ "issue",
139
+ "edit",
140
+ String(issueNumber),
141
+ "--repo",
142
+ repo,
143
+ "--body",
144
+ section,
145
+ "--json",
146
+ "number,url"
147
+ ], { env });
148
+ if (!res.success) {
149
+ throw new Error(`Cross-ref update failed: ${res.stderr}`);
150
+ }
151
+ }
152
+ function buildCrossRefSection(linkedTo) {
153
+ const refs = linkedTo.map((l) => `- ${l.repo}#${l.issueNumber}`).join("\n");
154
+ return `
155
+
156
+ ---
157
+ **Also tracked in:**
158
+ ${refs}`;
159
+ }
160
+ function getEnv(token) {
161
+ if (token) {
162
+ return { ...process.env, GH_TOKEN: token };
163
+ }
164
+ return process.env;
165
+ }
166
+ export {
167
+ crossRepoSync
168
+ };
@@ -0,0 +1,252 @@
1
+ /**
2
+ * GitHub Cross-Repo Sync — Distributed multi-repo issue creation and linking
3
+ *
4
+ * Creates issues in different repos based on user story target repo tags,
5
+ * then adds cross-reference sections for stories spanning multiple repos.
6
+ *
7
+ * @module github-cross-repo-sync
8
+ */
9
+
10
+ import { execFileNoThrow } from '../../../src/utils/execFileNoThrow.js';
11
+ import { generateIssueBody } from './github-issue-body-generator.js';
12
+
13
+ export interface CrossRepoUserStory {
14
+ id: string;
15
+ title: string;
16
+ description: string;
17
+ priority: string;
18
+ status: string;
19
+ acceptanceCriteria: Array<{ id: string; description: string; completed: boolean }>;
20
+ targetRepos: string[];
21
+ specId?: string;
22
+ }
23
+
24
+ export interface CrossRepoSyncOptions {
25
+ owner: string;
26
+ defaultRepo: string;
27
+ token?: string;
28
+ }
29
+
30
+ interface CreatedIssue {
31
+ userStoryId: string;
32
+ repo: string;
33
+ issueNumber: number;
34
+ issueUrl: string;
35
+ issueNodeId: string;
36
+ }
37
+
38
+ interface UpdatedIssue {
39
+ userStoryId: string;
40
+ repo: string;
41
+ issueNumber: number;
42
+ issueUrl: string;
43
+ }
44
+
45
+ interface CrossReference {
46
+ repo: string;
47
+ issueNumber: number;
48
+ linkedTo: Array<{ repo: string; issueNumber: number }>;
49
+ }
50
+
51
+ export interface CrossRepoSyncResult {
52
+ created: CreatedIssue[];
53
+ updated: UpdatedIssue[];
54
+ errors: Array<{ userStoryId: string; repo: string; error: string }>;
55
+ crossReferences: CrossReference[];
56
+ }
57
+
58
+ /**
59
+ * Create/update issues across multiple repos and add cross-references.
60
+ */
61
+ export async function crossRepoSync(
62
+ stories: CrossRepoUserStory[],
63
+ options: CrossRepoSyncOptions,
64
+ ): Promise<CrossRepoSyncResult> {
65
+ const result: CrossRepoSyncResult = {
66
+ created: [],
67
+ updated: [],
68
+ errors: [],
69
+ crossReferences: [],
70
+ };
71
+
72
+ if (stories.length === 0) return result;
73
+
74
+ const env = getEnv(options.token);
75
+
76
+ // Track per-story issues for cross-referencing
77
+ const storyIssueMap = new Map<string, Array<{ repo: string; issueNumber: number }>>();
78
+
79
+ // Step 1: Create/update issues in target repos
80
+ for (const story of stories) {
81
+ const repos = story.targetRepos.length > 0
82
+ ? story.targetRepos
83
+ : [`${options.owner}/${options.defaultRepo}`];
84
+
85
+ const storyIssues: Array<{ repo: string; issueNumber: number }> = [];
86
+
87
+ for (const repo of repos) {
88
+ try {
89
+ const body = generateIssueBody({
90
+ id: story.id,
91
+ title: story.title,
92
+ description: story.description,
93
+ priority: story.priority,
94
+ acceptanceCriteria: story.acceptanceCriteria,
95
+ specId: story.specId,
96
+ });
97
+
98
+ const title = `[${story.id}] ${story.title}`;
99
+ const existing = await searchIssue(story.id, repo, env);
100
+
101
+ if (existing) {
102
+ await updateIssue(existing.number, title, body, repo, env);
103
+ result.updated.push({
104
+ userStoryId: story.id,
105
+ repo,
106
+ issueNumber: existing.number,
107
+ issueUrl: `https://github.com/${repo}/issues/${existing.number}`,
108
+ });
109
+ storyIssues.push({ repo, issueNumber: existing.number });
110
+ } else {
111
+ const created = await createIssue(title, body, story, repo, env);
112
+ result.created.push({
113
+ userStoryId: story.id,
114
+ repo,
115
+ issueNumber: created.number,
116
+ issueUrl: created.url,
117
+ issueNodeId: created.node_id,
118
+ });
119
+ storyIssues.push({ repo, issueNumber: created.number });
120
+ }
121
+ } catch (err) {
122
+ result.errors.push({
123
+ userStoryId: story.id,
124
+ repo,
125
+ error: err instanceof Error ? err.message : String(err),
126
+ });
127
+ }
128
+ }
129
+
130
+ if (storyIssues.length > 1) {
131
+ storyIssueMap.set(story.id, storyIssues);
132
+ }
133
+ }
134
+
135
+ // Step 2: Add cross-references for multi-repo stories
136
+ for (const [, issues] of storyIssueMap) {
137
+ for (const issue of issues) {
138
+ const linkedTo = issues.filter(i => i.repo !== issue.repo);
139
+ result.crossReferences.push({
140
+ repo: issue.repo,
141
+ issueNumber: issue.issueNumber,
142
+ linkedTo,
143
+ });
144
+
145
+ // Add cross-reference comment via issue edit (append to body)
146
+ const crossRefSection = buildCrossRefSection(linkedTo);
147
+ try {
148
+ await appendToIssueBody(issue.issueNumber, issue.repo, crossRefSection, env);
149
+ } catch {
150
+ // Non-fatal: cross-ref is nice-to-have
151
+ }
152
+ }
153
+ }
154
+
155
+ return result;
156
+ }
157
+
158
+ async function searchIssue(
159
+ usId: string,
160
+ repo: string,
161
+ env: NodeJS.ProcessEnv,
162
+ ): Promise<{ number: number; title: string; node_id: string } | null> {
163
+ const res = await execFileNoThrow('gh', [
164
+ 'issue', 'list',
165
+ '--repo', repo,
166
+ '--search', `[${usId}] in:title`,
167
+ '--json', 'number,title,node_id',
168
+ '--limit', '1',
169
+ ], { env });
170
+
171
+ if (!res.success) {
172
+ throw new Error(`Search failed for ${repo}: ${res.stderr}`);
173
+ }
174
+
175
+ const issues = JSON.parse(res.stdout);
176
+ return issues.length > 0 ? issues[0] : null;
177
+ }
178
+
179
+ async function createIssue(
180
+ title: string,
181
+ body: string,
182
+ story: CrossRepoUserStory,
183
+ repo: string,
184
+ env: NodeJS.ProcessEnv,
185
+ ): Promise<{ number: number; url: string; node_id: string }> {
186
+ const res = await execFileNoThrow('gh', [
187
+ 'issue', 'create',
188
+ '--repo', repo,
189
+ '--title', title,
190
+ '--body', body,
191
+ '--label', 'user-story',
192
+ '--label', `priority:${story.priority}`,
193
+ '--json', 'number,url,node_id',
194
+ ], { env });
195
+
196
+ if (!res.success) {
197
+ throw new Error(`Create failed for ${repo}: ${res.stderr}`);
198
+ }
199
+
200
+ return JSON.parse(res.stdout);
201
+ }
202
+
203
+ async function updateIssue(
204
+ issueNumber: number,
205
+ title: string,
206
+ body: string,
207
+ repo: string,
208
+ env: NodeJS.ProcessEnv,
209
+ ): Promise<void> {
210
+ const res = await execFileNoThrow('gh', [
211
+ 'issue', 'edit', String(issueNumber),
212
+ '--repo', repo,
213
+ '--title', title,
214
+ '--body', body,
215
+ '--json', 'number,url',
216
+ ], { env });
217
+
218
+ if (!res.success) {
219
+ throw new Error(`Update failed for ${repo}: ${res.stderr}`);
220
+ }
221
+ }
222
+
223
+ async function appendToIssueBody(
224
+ issueNumber: number,
225
+ repo: string,
226
+ section: string,
227
+ env: NodeJS.ProcessEnv,
228
+ ): Promise<void> {
229
+ // Use gh issue edit with --add-body
230
+ const res = await execFileNoThrow('gh', [
231
+ 'issue', 'edit', String(issueNumber),
232
+ '--repo', repo,
233
+ '--body', section,
234
+ '--json', 'number,url',
235
+ ], { env });
236
+
237
+ if (!res.success) {
238
+ throw new Error(`Cross-ref update failed: ${res.stderr}`);
239
+ }
240
+ }
241
+
242
+ function buildCrossRefSection(linkedTo: Array<{ repo: string; issueNumber: number }>): string {
243
+ const refs = linkedTo.map(l => `- ${l.repo}#${l.issueNumber}`).join('\n');
244
+ return `\n\n---\n**Also tracked in:**\n${refs}`;
245
+ }
246
+
247
+ function getEnv(token?: string): NodeJS.ProcessEnv {
248
+ if (token) {
249
+ return { ...process.env, GH_TOKEN: token };
250
+ }
251
+ return process.env;
252
+ }
@@ -0,0 +1,116 @@
1
+ const DEFAULT_STATUS_MAPPING = {
2
+ "planned": "Todo",
3
+ "in-progress": "In Progress",
4
+ "completed": "Done"
5
+ };
6
+ const DEFAULT_PRIORITY_MAPPING = {
7
+ "P1": "Urgent",
8
+ "P2": "High",
9
+ "P3": "Medium",
10
+ "P4": "Low"
11
+ };
12
+ class GitHubFieldSync {
13
+ constructor(client, config) {
14
+ this.fieldsLoaded = false;
15
+ this.client = client;
16
+ this.config = config;
17
+ }
18
+ /**
19
+ * Load project fields and cache field IDs and option IDs.
20
+ */
21
+ async loadFields() {
22
+ if (this.fieldsLoaded) return;
23
+ const fields = await this.client.getProjectFields(this.config.projectId);
24
+ for (const field of fields) {
25
+ if (field.name === "Status" && field.options) {
26
+ this.statusField = {
27
+ id: field.id,
28
+ name: field.name,
29
+ options: new Map(field.options.map((o) => [o.name, o.id]))
30
+ };
31
+ } else if (field.name === "Priority" && field.options) {
32
+ this.priorityField = {
33
+ id: field.id,
34
+ name: field.name,
35
+ options: new Map(field.options.map((o) => [o.name, o.id]))
36
+ };
37
+ }
38
+ }
39
+ this.fieldsLoaded = true;
40
+ }
41
+ /**
42
+ * Sync fields for one or more items.
43
+ * Auto-calls loadFields if not called yet.
44
+ */
45
+ async syncItemFields(items) {
46
+ if (!this.fieldsLoaded) {
47
+ await this.loadFields();
48
+ }
49
+ const result = { updated: [], warnings: [] };
50
+ for (const item of items) {
51
+ if (item.status !== void 0) {
52
+ await this.syncField(
53
+ item,
54
+ "Status",
55
+ item.status,
56
+ this.statusField,
57
+ this.config.statusFieldMapping ?? DEFAULT_STATUS_MAPPING,
58
+ result
59
+ );
60
+ }
61
+ if (item.priority !== void 0) {
62
+ await this.syncField(
63
+ item,
64
+ "Priority",
65
+ item.priority,
66
+ this.priorityField,
67
+ this.config.priorityFieldMapping ?? DEFAULT_PRIORITY_MAPPING,
68
+ result
69
+ );
70
+ }
71
+ }
72
+ return result;
73
+ }
74
+ async syncField(item, fieldName, value, cachedField, mapping, result) {
75
+ if (!cachedField) {
76
+ result.warnings.push({
77
+ itemId: item.itemId,
78
+ field: fieldName,
79
+ message: `${fieldName} field not found on project`
80
+ });
81
+ return;
82
+ }
83
+ const mappedValue = mapping[value];
84
+ if (!mappedValue) {
85
+ result.warnings.push({
86
+ itemId: item.itemId,
87
+ field: fieldName,
88
+ message: `No mapping found for ${fieldName} value "${value}"`
89
+ });
90
+ return;
91
+ }
92
+ const optionId = cachedField.options.get(mappedValue);
93
+ if (!optionId) {
94
+ result.warnings.push({
95
+ itemId: item.itemId,
96
+ field: fieldName,
97
+ message: `Option "${mappedValue}" not found on ${fieldName} field`
98
+ });
99
+ return;
100
+ }
101
+ await this.client.updateItemFieldValue(
102
+ this.config.projectId,
103
+ item.itemId,
104
+ cachedField.id,
105
+ { singleSelectOptionId: optionId }
106
+ );
107
+ result.updated.push({
108
+ itemId: item.itemId,
109
+ field: fieldName,
110
+ value: mappedValue
111
+ });
112
+ }
113
+ }
114
+ export {
115
+ GitHubFieldSync
116
+ };