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,245 @@
1
+ /**
2
+ * GitHub AC Comment Poster — Posts progress comments to GitHub issues
3
+ * when acceptance criteria are completed in spec.md.
4
+ *
5
+ * Triggered by github-ac-sync-handler.sh after task-ac-sync-guard
6
+ * updates spec.md ACs.
7
+ *
8
+ * @module github-ac-comment-poster
9
+ */
10
+
11
+ import { readFile } from 'fs/promises';
12
+ import { execFileNoThrow } from '../../../src/utils/execFileNoThrow.js';
13
+ import { pushSyncUserStories } from './github-push-sync.js';
14
+ import type { UserStoryForSync } from './github-push-sync.js';
15
+
16
+ export interface CommentPostOptions {
17
+ owner: string;
18
+ repo: string;
19
+ token?: string;
20
+ }
21
+
22
+ export interface CommentPostResult {
23
+ posted: Array<{ usId: string; issueNumber: number }>;
24
+ errors: Array<{ usId: string; error: string }>;
25
+ }
26
+
27
+ interface ParsedACState {
28
+ id: string;
29
+ description: string;
30
+ completed: boolean;
31
+ }
32
+
33
+ interface ParsedUSIssueLink {
34
+ issueNumber: number;
35
+ issueUrl: string;
36
+ }
37
+
38
+ /**
39
+ * Post AC progress comments to GitHub issues for affected user stories.
40
+ *
41
+ * For each affected US:
42
+ * 1. Look up GitHub issue link from spec.md frontmatter
43
+ * 2. Extract AC states for that US
44
+ * 3. Build progress comment
45
+ * 4. Post via `gh issue comment`
46
+ *
47
+ * Never throws — all errors are captured in result.errors.
48
+ */
49
+ export async function postACProgressComments(
50
+ incrementId: string,
51
+ affectedUSIds: string[],
52
+ specPath: string,
53
+ options: CommentPostOptions,
54
+ ): Promise<CommentPostResult> {
55
+ const result: CommentPostResult = { posted: [], errors: [] };
56
+
57
+ if (affectedUSIds.length === 0) {
58
+ return result;
59
+ }
60
+
61
+ let content: string;
62
+ try {
63
+ content = await readFile(specPath, 'utf-8');
64
+ } catch (err) {
65
+ result.errors.push({
66
+ usId: affectedUSIds[0],
67
+ error: err instanceof Error ? err.message : String(err),
68
+ });
69
+ return result;
70
+ }
71
+
72
+ const issueLinks = parseIssueLinks(content);
73
+ const repoSlug = `${options.owner}/${options.repo}`;
74
+ const env = options.token ? { GH_TOKEN: options.token } : undefined;
75
+
76
+ for (const usId of affectedUSIds) {
77
+ const link = issueLinks[usId];
78
+ if (!link) {
79
+ continue;
80
+ }
81
+
82
+ const acStates = parseACStatesForUS(content, usId);
83
+ const commentBody = buildProgressCommentForUS(incrementId, usId, acStates);
84
+
85
+ const execResult = await execFileNoThrow(
86
+ 'gh',
87
+ ['issue', 'comment', String(link.issueNumber), '--body', commentBody, '-R', repoSlug],
88
+ env ? { env } : {},
89
+ );
90
+
91
+ if (execResult.success) {
92
+ result.posted.push({ usId, issueNumber: link.issueNumber });
93
+ } else {
94
+ result.errors.push({
95
+ usId,
96
+ error: execResult.stderr || 'Unknown error posting comment',
97
+ });
98
+ }
99
+
100
+ // Targeted push-sync: update issue body with current AC states
101
+ const usForSync = buildUserStoryForSync(content, usId, acStates, incrementId);
102
+ if (usForSync) {
103
+ try {
104
+ await pushSyncUserStories([usForSync], {
105
+ owner: options.owner,
106
+ repo: options.repo,
107
+ token: options.token,
108
+ });
109
+ } catch {
110
+ // Push-sync failure is non-blocking
111
+ }
112
+ }
113
+ }
114
+
115
+ return result;
116
+ }
117
+
118
+ /**
119
+ * Parse issue links from spec.md YAML frontmatter.
120
+ * Extracts externalLinks.github.userStories entries.
121
+ */
122
+ function parseIssueLinks(content: string): Record<string, ParsedUSIssueLink> {
123
+ const links: Record<string, ParsedUSIssueLink> = {};
124
+
125
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
126
+ if (!fmMatch) return links;
127
+
128
+ const frontmatter = fmMatch[1];
129
+
130
+ // Parse userStories section from YAML frontmatter
131
+ const usBlockMatch = frontmatter.match(/userStories:\s*\n((?:\s{6,}[\s\S]*?)(?=\n\s{0,3}\S|$))/);
132
+ if (!usBlockMatch) return links;
133
+
134
+ const usBlock = usBlockMatch[1];
135
+ const usEntries = usBlock.match(/^\s+(US-\d+):\s*\n((?:\s+\w[\s\S]*?)(?=\n\s+US-|\s*$))/gm);
136
+
137
+ if (!usEntries) return links;
138
+
139
+ for (const entry of usEntries) {
140
+ const idMatch = entry.match(/(US-\d+):/);
141
+ const numMatch = entry.match(/issueNumber:\s*(\d+)/);
142
+ const urlMatch = entry.match(/issueUrl:\s*"([^"]+)"/);
143
+
144
+ if (idMatch && numMatch) {
145
+ links[idMatch[1]] = {
146
+ issueNumber: parseInt(numMatch[1], 10),
147
+ issueUrl: urlMatch ? urlMatch[1] : '',
148
+ };
149
+ }
150
+ }
151
+
152
+ return links;
153
+ }
154
+
155
+ /**
156
+ * Extract AC states for a specific user story from spec.md content.
157
+ */
158
+ function parseACStatesForUS(content: string, usId: string): ParsedACState[] {
159
+ const states: ParsedACState[] = [];
160
+ // AC IDs use unpadded US number: US-001 → AC-US1-XX
161
+ const usNum = String(parseInt(usId.replace('US-', ''), 10));
162
+
163
+ const acPattern = new RegExp(
164
+ `- \\[([ x])\\] \\*\\*AC-US${usNum}-(\\d+)\\*\\*:\\s*(.+)`,
165
+ 'g',
166
+ );
167
+
168
+ let match;
169
+ while ((match = acPattern.exec(content)) !== null) {
170
+ states.push({
171
+ id: `AC-US${usNum}-${match[2]}`,
172
+ description: match[3].trim(),
173
+ completed: match[1] === 'x',
174
+ });
175
+ }
176
+
177
+ return states;
178
+ }
179
+
180
+ /**
181
+ * Build a UserStoryForSync object for targeted push-sync.
182
+ */
183
+ function buildUserStoryForSync(
184
+ content: string,
185
+ usId: string,
186
+ acStates: ParsedACState[],
187
+ incrementId: string,
188
+ ): UserStoryForSync | null {
189
+ // Extract US title from content
190
+ const usNum = String(parseInt(usId.replace('US-', ''), 10)).padStart(3, '0');
191
+ const titleMatch = content.match(new RegExp(`### US-${usNum}:\\s*(.+)`));
192
+ const title = titleMatch ? titleMatch[1].trim() : usId;
193
+
194
+ return {
195
+ id: usId,
196
+ title,
197
+ description: '',
198
+ priority: 'P1',
199
+ status: 'in-progress',
200
+ acceptanceCriteria: acStates.map(ac => ({
201
+ id: ac.id,
202
+ description: ac.description,
203
+ completed: ac.completed,
204
+ })),
205
+ specId: incrementId,
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Build a progress comment for a specific user story.
211
+ * Follows ProgressCommentBuilder format: percentage, AC checkboxes, timestamp.
212
+ */
213
+ function buildProgressCommentForUS(
214
+ incrementId: string,
215
+ usId: string,
216
+ acStates: ParsedACState[],
217
+ ): string {
218
+ const total = acStates.length;
219
+ const completed = acStates.filter(ac => ac.completed).length;
220
+ const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
221
+
222
+ let comment = `**Progress Update** — ${usId} (Increment ${incrementId})\n\n`;
223
+ comment += `**Status**: ${completed}/${total} ACs complete (${percentage}%)\n\n`;
224
+
225
+ if (completed > 0) {
226
+ comment += `**Completed**:\n`;
227
+ for (const ac of acStates.filter(a => a.completed)) {
228
+ comment += `- [x] **${ac.id}**: ${ac.description}\n`;
229
+ }
230
+ comment += '\n';
231
+ }
232
+
233
+ if (completed < total) {
234
+ comment += `**Remaining**:\n`;
235
+ for (const ac of acStates.filter(a => !a.completed)) {
236
+ comment += `- [ ] **${ac.id}**: ${ac.description}\n`;
237
+ }
238
+ comment += '\n';
239
+ }
240
+
241
+ comment += `---\n`;
242
+ comment += `Auto-synced by SpecWeave | ${new Date().toISOString().split('T')[0]}\n`;
243
+
244
+ return comment;
245
+ }
@@ -0,0 +1,93 @@
1
+ import { glob } from "glob";
2
+ import { readFile } from "fs/promises";
3
+ import { join } from "path";
4
+ import { GitHubSyncOrchestrator } from "./github-sync-orchestrator.js";
5
+ async function batchSyncAllSpecs(config) {
6
+ const specsDir = join(config.workspaceRoot, ".specweave/docs/internal/specs");
7
+ const specFiles = await glob("**/spec-*.md", { cwd: specsDir });
8
+ const result = {
9
+ specsProcessed: 0,
10
+ specsFailed: 0,
11
+ totalIssuesCreated: 0,
12
+ totalIssuesUpdated: 0,
13
+ errors: [],
14
+ summary: ""
15
+ };
16
+ if (specFiles.length === 0) {
17
+ result.summary = "No specs found";
18
+ return result;
19
+ }
20
+ const orchestrator = new GitHubSyncOrchestrator({
21
+ owner: config.owner,
22
+ repo: config.repo,
23
+ token: config.token,
24
+ dryRun: config.dryRun,
25
+ projectV2Enabled: config.projectV2Enabled,
26
+ projectV2Number: config.projectV2Number,
27
+ projectV2Id: config.projectV2Id,
28
+ statusFieldMapping: config.statusFieldMapping,
29
+ priorityFieldMapping: config.priorityFieldMapping
30
+ });
31
+ for (const specFile of specFiles) {
32
+ const specPath = join(specsDir, specFile);
33
+ result.specsProcessed++;
34
+ try {
35
+ const content = await readFile(specPath, "utf-8");
36
+ const userStories = parseUserStoriesFromSpec(content);
37
+ const syncResult = await orchestrator.syncSpec(specPath, userStories);
38
+ result.totalIssuesCreated += syncResult.pushResult.created.length;
39
+ result.totalIssuesUpdated += syncResult.pushResult.updated.length;
40
+ } catch (err) {
41
+ result.specsFailed++;
42
+ result.errors.push({
43
+ specPath,
44
+ error: err instanceof Error ? err.message : String(err)
45
+ });
46
+ }
47
+ }
48
+ const synced = result.specsProcessed - result.specsFailed;
49
+ result.summary = `Synced ${synced}/${result.specsProcessed} specs, ${result.totalIssuesCreated} issues created, ${result.totalIssuesUpdated} updated`;
50
+ return result;
51
+ }
52
+ const US_HEADER_RE = /^###?\s+(US-\d{3,}E?):\s*(.+)$/;
53
+ const AC_CHECKBOX_RE = /^-\s+\[([ xX])\]\s+\*\*(?<acId>AC-US\d+E?-\d{2})\*\*:\s*(?<desc>.+)$/;
54
+ const PRIORITY_RE = /\*\*Priority\*\*:\s*(P[0-3])/;
55
+ function parseUserStoriesFromSpec(content) {
56
+ const lines = content.split("\n");
57
+ const userStories = [];
58
+ let current = null;
59
+ for (const line of lines) {
60
+ const usMatch = line.match(US_HEADER_RE);
61
+ if (usMatch) {
62
+ if (current) userStories.push(current);
63
+ current = {
64
+ id: usMatch[1],
65
+ title: usMatch[2],
66
+ description: "",
67
+ priority: "P3",
68
+ status: "planned",
69
+ acceptanceCriteria: []
70
+ };
71
+ continue;
72
+ }
73
+ if (!current) continue;
74
+ const acMatch = line.match(AC_CHECKBOX_RE);
75
+ if (acMatch?.groups) {
76
+ current.acceptanceCriteria.push({
77
+ id: acMatch.groups.acId,
78
+ description: acMatch.groups.desc.trim(),
79
+ completed: acMatch[1] !== " "
80
+ });
81
+ continue;
82
+ }
83
+ const prioMatch = line.match(PRIORITY_RE);
84
+ if (prioMatch) {
85
+ current.priority = prioMatch[1];
86
+ }
87
+ }
88
+ if (current) userStories.push(current);
89
+ return userStories;
90
+ }
91
+ export {
92
+ batchSyncAllSpecs
93
+ };
@@ -0,0 +1,152 @@
1
+ /**
2
+ * GitHub Batch Sync — Sync all specs in workspace
3
+ *
4
+ * Discovers all spec files and syncs them sequentially
5
+ * using the GitHubSyncOrchestrator.
6
+ *
7
+ * @module github-batch-sync
8
+ */
9
+
10
+ import { glob } from 'glob';
11
+ import { readFile } from 'fs/promises';
12
+ import { join } from 'path';
13
+ import { GitHubSyncOrchestrator } from './github-sync-orchestrator.js';
14
+ import type { UserStoryForSync } from './github-push-sync.js';
15
+
16
+ export interface BatchSyncConfig {
17
+ owner: string;
18
+ repo: string;
19
+ workspaceRoot: string;
20
+ token?: string;
21
+ dryRun?: boolean;
22
+ projectV2Enabled?: boolean;
23
+ projectV2Number?: number;
24
+ projectV2Id?: string;
25
+ statusFieldMapping?: Record<string, string>;
26
+ priorityFieldMapping?: Record<string, string>;
27
+ }
28
+
29
+ export interface BatchSyncResult {
30
+ specsProcessed: number;
31
+ specsFailed: number;
32
+ totalIssuesCreated: number;
33
+ totalIssuesUpdated: number;
34
+ errors: Array<{ specPath: string; error: string }>;
35
+ summary: string;
36
+ }
37
+
38
+ /**
39
+ * Discover all specs and sync them sequentially.
40
+ */
41
+ export async function batchSyncAllSpecs(config: BatchSyncConfig): Promise<BatchSyncResult> {
42
+ const specsDir = join(config.workspaceRoot, '.specweave/docs/internal/specs');
43
+
44
+ // Discover specs
45
+ const specFiles = await glob('**/spec-*.md', { cwd: specsDir });
46
+
47
+ const result: BatchSyncResult = {
48
+ specsProcessed: 0,
49
+ specsFailed: 0,
50
+ totalIssuesCreated: 0,
51
+ totalIssuesUpdated: 0,
52
+ errors: [],
53
+ summary: '',
54
+ };
55
+
56
+ if (specFiles.length === 0) {
57
+ result.summary = 'No specs found';
58
+ return result;
59
+ }
60
+
61
+ // Create orchestrator
62
+ const orchestrator = new GitHubSyncOrchestrator({
63
+ owner: config.owner,
64
+ repo: config.repo,
65
+ token: config.token,
66
+ dryRun: config.dryRun,
67
+ projectV2Enabled: config.projectV2Enabled,
68
+ projectV2Number: config.projectV2Number,
69
+ projectV2Id: config.projectV2Id,
70
+ statusFieldMapping: config.statusFieldMapping,
71
+ priorityFieldMapping: config.priorityFieldMapping,
72
+ });
73
+
74
+ // Sync each spec sequentially
75
+ for (const specFile of specFiles) {
76
+ const specPath = join(specsDir, specFile);
77
+ result.specsProcessed++;
78
+
79
+ try {
80
+ const content = await readFile(specPath, 'utf-8');
81
+ const userStories = parseUserStoriesFromSpec(content);
82
+ const syncResult = await orchestrator.syncSpec(specPath, userStories);
83
+
84
+ result.totalIssuesCreated += syncResult.pushResult.created.length;
85
+ result.totalIssuesUpdated += syncResult.pushResult.updated.length;
86
+ } catch (err) {
87
+ result.specsFailed++;
88
+ result.errors.push({
89
+ specPath,
90
+ error: err instanceof Error ? err.message : String(err),
91
+ });
92
+ }
93
+ }
94
+
95
+ const synced = result.specsProcessed - result.specsFailed;
96
+ result.summary = `Synced ${synced}/${result.specsProcessed} specs, ${result.totalIssuesCreated} issues created, ${result.totalIssuesUpdated} updated`;
97
+
98
+ return result;
99
+ }
100
+
101
+ // US header: ### US-001: Title
102
+ const US_HEADER_RE = /^###?\s+(US-\d{3,}E?):\s*(.+)$/;
103
+ // AC checkbox: - [x] **AC-US1-01**: Description
104
+ const AC_CHECKBOX_RE = /^-\s+\[([ xX])\]\s+\*\*(?<acId>AC-US\d+E?-\d{2})\*\*:\s*(?<desc>.+)$/;
105
+ // Priority: **Priority**: P1
106
+ const PRIORITY_RE = /\*\*Priority\*\*:\s*(P[0-3])/;
107
+
108
+ /**
109
+ * Parse user stories from spec.md content.
110
+ */
111
+ function parseUserStoriesFromSpec(content: string): UserStoryForSync[] {
112
+ const lines = content.split('\n');
113
+ const userStories: UserStoryForSync[] = [];
114
+ let current: UserStoryForSync | null = null;
115
+
116
+ for (const line of lines) {
117
+ const usMatch = line.match(US_HEADER_RE);
118
+ if (usMatch) {
119
+ if (current) userStories.push(current);
120
+ current = {
121
+ id: usMatch[1],
122
+ title: usMatch[2],
123
+ description: '',
124
+ priority: 'P3',
125
+ status: 'planned',
126
+ acceptanceCriteria: [],
127
+ };
128
+ continue;
129
+ }
130
+
131
+ if (!current) continue;
132
+
133
+ const acMatch = line.match(AC_CHECKBOX_RE);
134
+ if (acMatch?.groups) {
135
+ current.acceptanceCriteria.push({
136
+ id: acMatch.groups.acId,
137
+ description: acMatch.groups.desc.trim(),
138
+ completed: acMatch[1] !== ' ',
139
+ });
140
+ continue;
141
+ }
142
+
143
+ const prioMatch = line.match(PRIORITY_RE);
144
+ if (prioMatch) {
145
+ current.priority = prioMatch[1];
146
+ }
147
+ }
148
+
149
+ if (current) userStories.push(current);
150
+
151
+ return userStories;
152
+ }
@@ -0,0 +1,47 @@
1
+ class GitHubBoardResolverV2 {
2
+ constructor(client, config) {
3
+ this.client = client;
4
+ this.config = config;
5
+ }
6
+ /**
7
+ * Find an existing project or create a new one.
8
+ *
9
+ * Priority:
10
+ * 1. If projectV2Id is configured, use it directly (skip lookup)
11
+ * 2. If projectV2Number is configured, return with that number
12
+ * 3. Otherwise, create a new project
13
+ */
14
+ async findOrCreateProject(title) {
15
+ if (this.config.projectV2Id) {
16
+ return {
17
+ id: this.config.projectV2Id,
18
+ number: this.config.projectV2Number || 0
19
+ };
20
+ }
21
+ if (this.config.projectV2Number) {
22
+ const ownerId2 = await this.client.getOwnerNodeId(this.config.owner);
23
+ return {
24
+ id: ownerId2,
25
+ // Will be resolved to actual project ID during operations
26
+ number: this.config.projectV2Number
27
+ };
28
+ }
29
+ const ownerId = await this.client.getOwnerNodeId(this.config.owner);
30
+ return this.client.createProjectV2(ownerId, title);
31
+ }
32
+ /**
33
+ * Add issues (by node ID) to a Projects V2 board.
34
+ * Returns the project item IDs for each added issue.
35
+ */
36
+ async addIssuesToProject(projectId, issueNodeIds) {
37
+ const itemIds = [];
38
+ for (const nodeId of issueNodeIds) {
39
+ const itemId = await this.client.addProjectV2Item(projectId, nodeId);
40
+ itemIds.push(itemId);
41
+ }
42
+ return itemIds;
43
+ }
44
+ }
45
+ export {
46
+ GitHubBoardResolverV2
47
+ };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * GitHub Board Resolver V2 — Projects V2 Integration
3
+ *
4
+ * Finds or creates GitHub Projects V2 boards and adds issues to them.
5
+ * Replaces the deprecated V1 Classic Projects board resolver.
6
+ *
7
+ * @module github-board-resolver-v2
8
+ */
9
+
10
+ import { GitHubGraphQLClient } from './github-graphql-client.js';
11
+
12
+ export interface BoardResolverConfig {
13
+ owner: string;
14
+ projectV2Number?: number;
15
+ projectV2Id?: string;
16
+ }
17
+
18
+ export class GitHubBoardResolverV2 {
19
+ private client: GitHubGraphQLClient;
20
+ private config: BoardResolverConfig;
21
+
22
+ constructor(client: GitHubGraphQLClient, config: BoardResolverConfig) {
23
+ this.client = client;
24
+ this.config = config;
25
+ }
26
+
27
+ /**
28
+ * Find an existing project or create a new one.
29
+ *
30
+ * Priority:
31
+ * 1. If projectV2Id is configured, use it directly (skip lookup)
32
+ * 2. If projectV2Number is configured, return with that number
33
+ * 3. Otherwise, create a new project
34
+ */
35
+ async findOrCreateProject(title: string): Promise<{ id: string; number: number }> {
36
+ // If we have a direct project ID, use it
37
+ if (this.config.projectV2Id) {
38
+ return {
39
+ id: this.config.projectV2Id,
40
+ number: this.config.projectV2Number || 0,
41
+ };
42
+ }
43
+
44
+ // If we have a project number, look it up by querying the owner
45
+ if (this.config.projectV2Number) {
46
+ const ownerId = await this.client.getOwnerNodeId(this.config.owner);
47
+ // Return the project reference — the number is known, ID will be resolved on first use
48
+ return {
49
+ id: ownerId, // Will be resolved to actual project ID during operations
50
+ number: this.config.projectV2Number,
51
+ };
52
+ }
53
+
54
+ // No project configured — create a new one
55
+ const ownerId = await this.client.getOwnerNodeId(this.config.owner);
56
+ return this.client.createProjectV2(ownerId, title);
57
+ }
58
+
59
+ /**
60
+ * Add issues (by node ID) to a Projects V2 board.
61
+ * Returns the project item IDs for each added issue.
62
+ */
63
+ async addIssuesToProject(projectId: string, issueNodeIds: string[]): Promise<string[]> {
64
+ const itemIds: string[] = [];
65
+
66
+ for (const nodeId of issueNodeIds) {
67
+ const itemId = await this.client.addProjectV2Item(projectId, nodeId);
68
+ itemIds.push(itemId);
69
+ }
70
+
71
+ return itemIds;
72
+ }
73
+ }
@@ -0,0 +1,90 @@
1
+ class GitHubConflictResolver {
2
+ constructor(config) {
3
+ this.statusResolution = config?.defaultStatusResolution ?? "github-wins";
4
+ this.contentResolution = config?.defaultContentResolution ?? "prompt";
5
+ this.acResolution = config?.defaultACResolution ?? "github-wins";
6
+ }
7
+ /**
8
+ * Detect conflicts between spec and GitHub states.
9
+ */
10
+ detectConflicts(specState, githubState) {
11
+ const conflicts = [];
12
+ if (specState.title !== githubState.title) {
13
+ conflicts.push({
14
+ field: "title",
15
+ specValue: specState.title,
16
+ githubValue: githubState.title,
17
+ defaultResolution: this.contentResolution
18
+ });
19
+ }
20
+ const githubStatus = githubState.state;
21
+ const specStatus = specState.status;
22
+ const statusMismatch = this.isStatusConflict(specStatus, githubStatus);
23
+ if (statusMismatch) {
24
+ conflicts.push({
25
+ field: "status",
26
+ specValue: specStatus,
27
+ githubValue: githubStatus,
28
+ defaultResolution: this.statusResolution
29
+ });
30
+ }
31
+ const ghAcMap = new Map(
32
+ githubState.acceptanceCriteria.map((ac) => [ac.id, ac.completed])
33
+ );
34
+ for (const specAc of specState.acceptanceCriteria) {
35
+ const ghCompleted = ghAcMap.get(specAc.id);
36
+ if (ghCompleted !== void 0 && specAc.completed !== ghCompleted) {
37
+ conflicts.push({
38
+ field: `ac:${specAc.id}`,
39
+ specValue: String(specAc.completed),
40
+ githubValue: String(ghCompleted),
41
+ defaultResolution: this.acResolution
42
+ });
43
+ }
44
+ }
45
+ return conflicts;
46
+ }
47
+ /**
48
+ * Resolve all conflicts using their default resolution strategy.
49
+ */
50
+ resolveConflicts(conflicts) {
51
+ return conflicts.map((c) => this.resolveConflict(c));
52
+ }
53
+ /**
54
+ * Resolve a single conflict with optional override.
55
+ */
56
+ resolveConflict(conflict, resolution) {
57
+ const effectiveResolution = resolution ?? conflict.defaultResolution;
58
+ let resolvedValue;
59
+ switch (effectiveResolution) {
60
+ case "github-wins":
61
+ resolvedValue = conflict.githubValue;
62
+ break;
63
+ case "spec-wins":
64
+ resolvedValue = conflict.specValue;
65
+ break;
66
+ case "prompt":
67
+ resolvedValue = conflict.specValue;
68
+ break;
69
+ }
70
+ return {
71
+ field: conflict.field,
72
+ specValue: conflict.specValue,
73
+ githubValue: conflict.githubValue,
74
+ resolution: effectiveResolution,
75
+ resolvedValue,
76
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
77
+ };
78
+ }
79
+ /**
80
+ * Check if spec status and GitHub state represent a conflict.
81
+ */
82
+ isStatusConflict(specStatus, githubState) {
83
+ if (specStatus === "completed" && githubState === "open") return true;
84
+ if (specStatus !== "completed" && githubState === "closed") return true;
85
+ return false;
86
+ }
87
+ }
88
+ export {
89
+ GitHubConflictResolver
90
+ };