specweave 0.28.17 → 0.28.20

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 (204) hide show
  1. package/dist/plugins/specweave-ado/lib/ado-board-resolver.d.ts +94 -0
  2. package/dist/plugins/specweave-ado/lib/ado-board-resolver.d.ts.map +1 -0
  3. package/dist/plugins/specweave-ado/lib/ado-board-resolver.js +219 -0
  4. package/dist/plugins/specweave-ado/lib/ado-board-resolver.js.map +1 -0
  5. package/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts +16 -0
  6. package/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts.map +1 -1
  7. package/dist/plugins/specweave-ado/lib/ado-spec-sync.js +63 -3
  8. package/dist/plugins/specweave-ado/lib/ado-spec-sync.js.map +1 -1
  9. package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts +12 -3
  10. package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts.map +1 -1
  11. package/dist/plugins/specweave-ado/lib/ado-status-sync.js +37 -3
  12. package/dist/plugins/specweave-ado/lib/ado-status-sync.js.map +1 -1
  13. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +6 -11
  14. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
  15. package/dist/plugins/specweave-github/lib/github-feature-sync.js +6 -11
  16. package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
  17. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts +21 -0
  18. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts.map +1 -0
  19. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js +445 -0
  20. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js.map +1 -0
  21. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts +10 -0
  22. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts.map +1 -1
  23. package/dist/plugins/specweave-github/lib/github-status-sync.js +40 -2
  24. package/dist/plugins/specweave-github/lib/github-status-sync.js.map +1 -1
  25. package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts +94 -0
  26. package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts.map +1 -0
  27. package/dist/plugins/specweave-github/lib/increment-issue-builder.js +369 -0
  28. package/dist/plugins/specweave-github/lib/increment-issue-builder.js.map +1 -0
  29. package/dist/plugins/specweave-jira/lib/jira-board-resolver.d.ts +50 -0
  30. package/dist/plugins/specweave-jira/lib/jira-board-resolver.d.ts.map +1 -0
  31. package/dist/plugins/specweave-jira/lib/jira-board-resolver.js +84 -0
  32. package/dist/plugins/specweave-jira/lib/jira-board-resolver.js.map +1 -0
  33. package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts +12 -0
  34. package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts.map +1 -1
  35. package/dist/plugins/specweave-jira/lib/jira-spec-sync.js +57 -5
  36. package/dist/plugins/specweave-jira/lib/jira-spec-sync.js.map +1 -1
  37. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +5 -1
  38. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -1
  39. package/dist/plugins/specweave-jira/lib/jira-status-sync.js +12 -4
  40. package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -1
  41. package/dist/src/cli/commands/import-external.d.ts.map +1 -1
  42. package/dist/src/cli/commands/import-external.js +12 -7
  43. package/dist/src/cli/commands/import-external.js.map +1 -1
  44. package/dist/src/cli/helpers/init/external-import.d.ts.map +1 -1
  45. package/dist/src/cli/helpers/init/external-import.js +308 -36
  46. package/dist/src/cli/helpers/init/external-import.js.map +1 -1
  47. package/dist/src/cli/helpers/init/jira-ado-auto-detect.d.ts +115 -0
  48. package/dist/src/cli/helpers/init/jira-ado-auto-detect.d.ts.map +1 -0
  49. package/dist/src/cli/helpers/init/jira-ado-auto-detect.js +590 -0
  50. package/dist/src/cli/helpers/init/jira-ado-auto-detect.js.map +1 -0
  51. package/dist/src/cli/helpers/issue-tracker/ado-area-selection.d.ts +65 -0
  52. package/dist/src/cli/helpers/issue-tracker/ado-area-selection.d.ts.map +1 -0
  53. package/dist/src/cli/helpers/issue-tracker/ado-area-selection.js +278 -0
  54. package/dist/src/cli/helpers/issue-tracker/ado-area-selection.js.map +1 -0
  55. package/dist/src/cli/helpers/issue-tracker/jira-board-selection.d.ts +64 -0
  56. package/dist/src/cli/helpers/issue-tracker/jira-board-selection.d.ts.map +1 -0
  57. package/dist/src/cli/helpers/issue-tracker/jira-board-selection.js +251 -0
  58. package/dist/src/cli/helpers/issue-tracker/jira-board-selection.js.map +1 -0
  59. package/dist/src/config/types.d.ts +6 -6
  60. package/dist/src/core/ac-test-validator-cli.js +4 -1
  61. package/dist/src/core/ac-test-validator-cli.js.map +1 -1
  62. package/dist/src/core/ac-test-validator.d.ts.map +1 -1
  63. package/dist/src/core/ac-test-validator.js +4 -1
  64. package/dist/src/core/ac-test-validator.js.map +1 -1
  65. package/dist/src/core/background/index.d.ts +11 -0
  66. package/dist/src/core/background/index.d.ts.map +1 -0
  67. package/dist/src/core/background/index.js +11 -0
  68. package/dist/src/core/background/index.js.map +1 -0
  69. package/dist/src/core/background/job-manager.d.ts +65 -0
  70. package/dist/src/core/background/job-manager.d.ts.map +1 -0
  71. package/dist/src/core/background/job-manager.js +192 -0
  72. package/dist/src/core/background/job-manager.js.map +1 -0
  73. package/dist/src/core/background/types.d.ts +59 -0
  74. package/dist/src/core/background/types.d.ts.map +1 -0
  75. package/dist/src/core/background/types.js +8 -0
  76. package/dist/src/core/background/types.js.map +1 -0
  77. package/dist/src/core/repo-structure/multi-repo-configurator.d.ts +25 -0
  78. package/dist/src/core/repo-structure/multi-repo-configurator.d.ts.map +1 -0
  79. package/dist/src/core/repo-structure/multi-repo-configurator.js +614 -0
  80. package/dist/src/core/repo-structure/multi-repo-configurator.js.map +1 -0
  81. package/dist/src/core/repo-structure/repo-initializer.d.ts +40 -0
  82. package/dist/src/core/repo-structure/repo-initializer.d.ts.map +1 -0
  83. package/dist/src/core/repo-structure/repo-initializer.js +252 -0
  84. package/dist/src/core/repo-structure/repo-initializer.js.map +1 -0
  85. package/dist/src/core/repo-structure/repo-structure-manager.d.ts +3 -37
  86. package/dist/src/core/repo-structure/repo-structure-manager.d.ts.map +1 -1
  87. package/dist/src/core/repo-structure/repo-structure-manager.js +23 -803
  88. package/dist/src/core/repo-structure/repo-structure-manager.js.map +1 -1
  89. package/dist/src/core/types/increment-metadata.d.ts +75 -0
  90. package/dist/src/core/types/increment-metadata.d.ts.map +1 -1
  91. package/dist/src/core/types/spec-metadata.d.ts +2 -0
  92. package/dist/src/core/types/spec-metadata.d.ts.map +1 -1
  93. package/dist/src/core/types/sync-profile.d.ts +137 -5
  94. package/dist/src/core/types/sync-profile.d.ts.map +1 -1
  95. package/dist/src/core/types/sync-profile.js +63 -0
  96. package/dist/src/core/types/sync-profile.js.map +1 -1
  97. package/dist/src/importers/external-importer.d.ts +25 -0
  98. package/dist/src/importers/external-importer.d.ts.map +1 -1
  99. package/dist/src/importers/github-importer.d.ts.map +1 -1
  100. package/dist/src/importers/github-importer.js +5 -3
  101. package/dist/src/importers/github-importer.js.map +1 -1
  102. package/dist/src/importers/import-coordinator.d.ts +20 -0
  103. package/dist/src/importers/import-coordinator.d.ts.map +1 -1
  104. package/dist/src/importers/import-coordinator.js.map +1 -1
  105. package/dist/src/importers/item-converter.d.ts +51 -0
  106. package/dist/src/importers/item-converter.d.ts.map +1 -1
  107. package/dist/src/importers/item-converter.js +39 -12
  108. package/dist/src/importers/item-converter.js.map +1 -1
  109. package/dist/src/init/architecture/types.d.ts +2 -2
  110. package/dist/src/init/compliance/types.d.ts +1 -1
  111. package/dist/src/init/repo/types.d.ts +1 -1
  112. package/dist/src/living-docs/fs-id-allocator.d.ts +72 -3
  113. package/dist/src/living-docs/fs-id-allocator.d.ts.map +1 -1
  114. package/dist/src/living-docs/fs-id-allocator.js +142 -16
  115. package/dist/src/living-docs/fs-id-allocator.js.map +1 -1
  116. package/dist/src/locales/de/cli.json +14 -0
  117. package/dist/src/locales/es/cli.json +14 -0
  118. package/dist/src/locales/fr/cli.json +14 -0
  119. package/dist/src/locales/ja/cli.json +14 -0
  120. package/dist/src/locales/ko/cli.json +14 -0
  121. package/dist/src/locales/pt/cli.json +14 -0
  122. package/dist/src/locales/ru/cli.json +14 -0
  123. package/dist/src/locales/zh/cli.json +14 -0
  124. package/dist/src/utils/chalk-fallback.d.ts +38 -0
  125. package/dist/src/utils/chalk-fallback.d.ts.map +1 -0
  126. package/dist/src/utils/chalk-fallback.js +118 -0
  127. package/dist/src/utils/chalk-fallback.js.map +1 -0
  128. package/dist/src/utils/project-id-generator.d.ts +127 -0
  129. package/dist/src/utils/project-id-generator.d.ts.map +1 -0
  130. package/dist/src/utils/project-id-generator.js +228 -0
  131. package/dist/src/utils/project-id-generator.js.map +1 -0
  132. package/package.json +1 -1
  133. package/plugins/specweave/agents/pm/AGENT.md +202 -0
  134. package/plugins/specweave/commands/specweave-import-external.md +5 -3
  135. package/plugins/specweave/commands/specweave-jobs.md +160 -0
  136. package/plugins/specweave/commands/specweave-sync-docs.md +6 -2
  137. package/plugins/specweave/hooks/pre-task-completion.sh +35 -17
  138. package/plugins/specweave/lib/vendor/core/ac-test-validator-cli.d.ts +16 -0
  139. package/plugins/specweave/lib/vendor/core/ac-test-validator-cli.js +121 -0
  140. package/plugins/specweave/lib/vendor/core/ac-test-validator-cli.js.map +1 -0
  141. package/plugins/specweave/lib/vendor/core/ac-test-validator.d.ts +111 -0
  142. package/plugins/specweave/lib/vendor/core/ac-test-validator.js +295 -0
  143. package/plugins/specweave/lib/vendor/core/ac-test-validator.js.map +1 -0
  144. package/plugins/specweave/lib/vendor/core/types/increment-metadata.d.ts +75 -0
  145. package/plugins/specweave/lib/vendor/utils/chalk-fallback.d.ts +38 -0
  146. package/plugins/specweave/lib/vendor/utils/chalk-fallback.js +118 -0
  147. package/plugins/specweave/lib/vendor/utils/chalk-fallback.js.map +1 -0
  148. package/plugins/specweave/lib/vendor/utils/fs-native.d.ts +179 -0
  149. package/plugins/specweave/lib/vendor/utils/fs-native.js +319 -0
  150. package/plugins/specweave/lib/vendor/utils/fs-native.js.map +1 -0
  151. package/plugins/specweave/skills/code-reviewer/SKILL.md +1 -1
  152. package/plugins/specweave/skills/docs-updater/SKILL.md +61 -0
  153. package/plugins/specweave/skills/increment-planner/SKILL.md +10 -335
  154. package/plugins/specweave/skills/increment-planner/templates/metadata.json +13 -0
  155. package/plugins/specweave/skills/increment-planner/templates/plan.md +50 -0
  156. package/plugins/specweave/skills/increment-planner/templates/spec-multi-project.md +86 -0
  157. package/plugins/specweave/skills/increment-planner/templates/spec-single-project.md +50 -0
  158. package/plugins/specweave/skills/increment-planner/templates/tasks-multi-project.md +86 -0
  159. package/plugins/specweave/skills/increment-planner/templates/tasks-single-project.md +48 -0
  160. package/plugins/specweave-ado/commands/specweave-ado-import-areas.md +358 -0
  161. package/plugins/specweave-ado/lib/ado-spec-sync.js +59 -3
  162. package/plugins/specweave-ado/lib/ado-spec-sync.ts +72 -3
  163. package/plugins/specweave-ado/lib/ado-status-sync.js +35 -3
  164. package/plugins/specweave-ado/lib/ado-status-sync.ts +48 -4
  165. package/plugins/specweave-alternatives/skills/architecture-alternatives/SKILL.md +1 -0
  166. package/plugins/specweave-alternatives/skills/bmad-method/SKILL.md +1 -0
  167. package/plugins/specweave-core/skills/code-quality/SKILL.md +1 -0
  168. package/plugins/specweave-core/skills/design-patterns/SKILL.md +1 -0
  169. package/plugins/specweave-core/skills/software-architecture/SKILL.md +1 -0
  170. package/plugins/specweave-github/commands/specweave-github-cleanup-duplicates.md +14 -10
  171. package/plugins/specweave-github/commands/specweave-github-sync.md +57 -0
  172. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +74 -0
  173. package/plugins/specweave-github/lib/github-feature-sync.ts +6 -11
  174. package/plugins/specweave-github/lib/github-increment-sync-cli.js +456 -0
  175. package/plugins/specweave-github/lib/github-increment-sync-cli.ts +588 -0
  176. package/plugins/specweave-github/lib/github-status-sync.js +37 -1
  177. package/plugins/specweave-github/lib/github-status-sync.ts +60 -4
  178. package/plugins/specweave-github/lib/increment-issue-builder.js +389 -0
  179. package/plugins/specweave-github/lib/increment-issue-builder.ts +502 -0
  180. package/plugins/specweave-github/skills/github-issue-standard/SKILL.md +19 -24
  181. package/plugins/specweave-infrastructure/agents/observability-engineer/AGENT.md +15 -23
  182. package/plugins/specweave-jira/commands/specweave-jira-import-boards.md +331 -0
  183. package/plugins/specweave-jira/lib/jira-spec-sync.js +53 -5
  184. package/plugins/specweave-jira/lib/jira-spec-sync.ts +87 -7
  185. package/plugins/specweave-jira/lib/jira-status-sync.js +9 -3
  186. package/plugins/specweave-jira/lib/jira-status-sync.ts +15 -6
  187. package/plugins/specweave-ml/agents/data-scientist/AGENT.md +16 -20
  188. package/plugins/specweave-ml/agents/ml-engineer/AGENT.md +18 -19
  189. package/plugins/specweave-ml/skills/{ml-pipeline-workflow → mlops-dag-builder}/SKILL.md +18 -14
  190. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +111 -0
  191. package/plugins/specweave-ui/skills/browser-automation/SKILL.md +1 -1
  192. package/plugins/specweave-ui/skills/ui-testing/SKILL.md +10 -122
  193. package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts +0 -70
  194. package/dist/plugins/specweave-github/lib/epic-content-builder.d.ts.map +0 -1
  195. package/dist/plugins/specweave-github/lib/epic-content-builder.js +0 -258
  196. package/dist/plugins/specweave-github/lib/epic-content-builder.js.map +0 -1
  197. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts +0 -83
  198. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts.map +0 -1
  199. package/dist/plugins/specweave-github/lib/github-epic-sync.js +0 -466
  200. package/dist/plugins/specweave-github/lib/github-epic-sync.js.map +0 -1
  201. package/plugins/specweave-github/lib/epic-content-builder.js +0 -265
  202. package/plugins/specweave-github/lib/epic-content-builder.ts +0 -376
  203. package/plugins/specweave-github/lib/github-epic-sync.js +0 -488
  204. package/plugins/specweave-github/lib/github-epic-sync.ts +0 -715
@@ -0,0 +1,588 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * GitHub Increment Sync CLI
4
+ *
5
+ * For brownfield projects without living docs structure.
6
+ * Creates GitHub issues directly from increment spec.md with CORRECT format.
7
+ *
8
+ * CORRECT FORMAT (SpecWeave Universal Hierarchy):
9
+ * - Feature (FS-XXX) → GitHub Milestone
10
+ * - User Story (US-XXX) → GitHub Issue with [FS-XXX][US-YYY] Title
11
+ * - Tasks (T-XXX) → Checkboxes in User Story issue
12
+ * - ACs → Checkboxes in User Story issue
13
+ *
14
+ * Usage:
15
+ * node github-increment-sync-cli.js <increment-id>
16
+ * node github-increment-sync-cli.js 0002-thumbnail-optimizer-mvp
17
+ *
18
+ * @see CLAUDE.md (GitHub Issue Format rules)
19
+ */
20
+
21
+ import { existsSync, readFileSync } from 'fs';
22
+ import * as fs from 'fs/promises';
23
+ import * as path from 'path';
24
+ import { IncrementIssueBuilder, UserStory, Task, IncrementData } from './increment-issue-builder.js';
25
+ import { execFileNoThrow } from '../../../src/utils/execFileNoThrow.js';
26
+
27
+ interface GitHubConfig {
28
+ owner: string;
29
+ repo: string;
30
+ token: string;
31
+ }
32
+
33
+ interface SyncResult {
34
+ milestoneNumber: number;
35
+ milestoneUrl: string;
36
+ issues: Array<{
37
+ userStoryId: string;
38
+ issueNumber: number;
39
+ issueUrl: string;
40
+ title: string;
41
+ }>;
42
+ }
43
+
44
+ async function loadGitHubConfig(): Promise<GitHubConfig | null> {
45
+ const projectRoot = process.cwd();
46
+ const configPath = path.join(projectRoot, '.specweave/config.json');
47
+
48
+ let owner = process.env.GITHUB_OWNER || '';
49
+ let repo = process.env.GITHUB_REPO || '';
50
+ const token = process.env.GITHUB_TOKEN || '';
51
+
52
+ // Try to load from config.json
53
+ if (existsSync(configPath)) {
54
+ try {
55
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
56
+
57
+ // Method 1: sync.github
58
+ if (config.sync?.github?.owner && config.sync?.github?.repo) {
59
+ owner = config.sync.github.owner;
60
+ repo = config.sync.github.repo;
61
+ }
62
+ // Method 2: multiProject.projects[activeProject].externalTools.github
63
+ else if (config.multiProject?.enabled && config.multiProject?.activeProject) {
64
+ const activeProject = config.multiProject.activeProject;
65
+ const projectConfig = config.multiProject.projects?.[activeProject];
66
+ if (projectConfig?.externalTools?.github?.repository) {
67
+ const parts = projectConfig.externalTools.github.repository.split('/');
68
+ if (parts.length === 2) {
69
+ owner = parts[0];
70
+ repo = parts[1];
71
+ }
72
+ }
73
+ }
74
+ // Method 3: sync.profiles[activeProfile]
75
+ else if (config.sync?.activeProfile && config.sync?.profiles) {
76
+ const profile = config.sync.profiles[config.sync.activeProfile];
77
+ if (profile?.config?.owner && profile?.config?.repo) {
78
+ owner = profile.config.owner;
79
+ repo = profile.config.repo;
80
+ }
81
+ }
82
+ } catch (error) {
83
+ console.error('āš ļø Failed to parse config.json:', error);
84
+ }
85
+ }
86
+
87
+ // Fallback: detect from git remote
88
+ if (!owner || !repo) {
89
+ const result = await execFileNoThrow('git', ['remote', 'get-url', 'origin']);
90
+ if (result.exitCode === 0 && result.stdout) {
91
+ const remoteUrl = result.stdout.trim();
92
+ const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
93
+ if (match) {
94
+ owner = owner || match[1];
95
+ repo = repo || match[2];
96
+ }
97
+ }
98
+ }
99
+
100
+ if (!token) {
101
+ console.error('āŒ GITHUB_TOKEN not set');
102
+ console.error(' Set it in .env file or export GITHUB_TOKEN=ghp_xxx');
103
+ return null;
104
+ }
105
+
106
+ if (!owner || !repo) {
107
+ console.error('āŒ Could not detect GitHub owner/repo');
108
+ console.error(' Set sync.github.owner and sync.github.repo in .specweave/config.json');
109
+ return null;
110
+ }
111
+
112
+ return { owner, repo, token };
113
+ }
114
+
115
+ /**
116
+ * Find increment folder by ID (supports partial matching)
117
+ */
118
+ async function findIncrementFolder(incrementId: string): Promise<string | null> {
119
+ const projectRoot = process.cwd();
120
+ const incrementsDir = path.join(projectRoot, '.specweave/increments');
121
+
122
+ if (!existsSync(incrementsDir)) {
123
+ return null;
124
+ }
125
+
126
+ const entries = await fs.readdir(incrementsDir, { withFileTypes: true });
127
+
128
+ for (const entry of entries) {
129
+ if (!entry.isDirectory()) continue;
130
+ if (entry.name.startsWith('_')) continue; // Skip _archive, _backlog
131
+
132
+ // Exact match
133
+ if (entry.name === incrementId) {
134
+ return path.join(incrementsDir, entry.name);
135
+ }
136
+
137
+ // Partial match (e.g., "0063" matches "0063-fix-external-import")
138
+ if (entry.name.startsWith(incrementId + '-')) {
139
+ return path.join(incrementsDir, entry.name);
140
+ }
141
+ }
142
+
143
+ return null;
144
+ }
145
+
146
+ /**
147
+ * Load existing GitHub links from metadata.json
148
+ */
149
+ function loadExistingGitHubLinks(incrementPath: string): {
150
+ milestone?: number;
151
+ userStoryIssues: Record<string, number>;
152
+ } {
153
+ const metadataPath = path.join(incrementPath, 'metadata.json');
154
+
155
+ if (!existsSync(metadataPath)) {
156
+ return { userStoryIssues: {} };
157
+ }
158
+
159
+ try {
160
+ const metadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
161
+
162
+ return {
163
+ milestone: metadata.github?.milestone,
164
+ userStoryIssues: metadata.github?.userStoryIssues || {}
165
+ };
166
+ } catch {
167
+ return { userStoryIssues: {} };
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Update increment metadata with GitHub links
173
+ */
174
+ async function updateIncrementMetadata(
175
+ incrementPath: string,
176
+ milestoneNumber: number,
177
+ userStoryIssues: Record<string, number>
178
+ ): Promise<void> {
179
+ const metadataPath = path.join(incrementPath, 'metadata.json');
180
+
181
+ let metadata: Record<string, unknown> = {};
182
+
183
+ if (existsSync(metadataPath)) {
184
+ try {
185
+ metadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
186
+ } catch {
187
+ // Start fresh
188
+ }
189
+ }
190
+
191
+ // Update github section
192
+ metadata.github = {
193
+ milestone: milestoneNumber,
194
+ userStoryIssues,
195
+ lastSync: new Date().toISOString()
196
+ };
197
+
198
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2) + '\n');
199
+ }
200
+
201
+ /**
202
+ * Create or get GitHub milestone for the feature
203
+ */
204
+ async function createOrGetMilestone(
205
+ owner: string,
206
+ repo: string,
207
+ featureId: string,
208
+ title: string,
209
+ existingMilestone?: number
210
+ ): Promise<{ number: number; url: string }> {
211
+ // If we have an existing milestone, verify it exists
212
+ if (existingMilestone) {
213
+ const verifyResult = await execFileNoThrow('gh', [
214
+ 'api', `repos/${owner}/${repo}/milestones/${existingMilestone}`,
215
+ '--jq', '.number'
216
+ ]);
217
+ if (verifyResult.exitCode === 0) {
218
+ return {
219
+ number: existingMilestone,
220
+ url: `https://github.com/${owner}/${repo}/milestone/${existingMilestone}`
221
+ };
222
+ }
223
+ }
224
+
225
+ // Search for existing milestone by title
226
+ const searchResult = await execFileNoThrow('gh', [
227
+ 'api', `repos/${owner}/${repo}/milestones`,
228
+ '--jq', `.[] | select(.title | contains("${featureId}")) | .number`
229
+ ]);
230
+
231
+ if (searchResult.exitCode === 0 && searchResult.stdout.trim()) {
232
+ const milestoneNumber = parseInt(searchResult.stdout.trim().split('\n')[0], 10);
233
+ return {
234
+ number: milestoneNumber,
235
+ url: `https://github.com/${owner}/${repo}/milestone/${milestoneNumber}`
236
+ };
237
+ }
238
+
239
+ // Create new milestone
240
+ const milestoneTitle = `[${featureId}] ${title}`;
241
+ const createResult = await execFileNoThrow('gh', [
242
+ 'api', `repos/${owner}/${repo}/milestones`,
243
+ '-X', 'POST',
244
+ '-f', `title=${milestoneTitle}`,
245
+ '-f', `description=Feature milestone for ${featureId}`,
246
+ '--jq', '.number'
247
+ ]);
248
+
249
+ if (createResult.exitCode !== 0) {
250
+ throw new Error(`Failed to create milestone: ${createResult.stderr || createResult.stdout}`);
251
+ }
252
+
253
+ const milestoneNumber = parseInt(createResult.stdout.trim(), 10);
254
+ return {
255
+ number: milestoneNumber,
256
+ url: `https://github.com/${owner}/${repo}/milestone/${milestoneNumber}`
257
+ };
258
+ }
259
+
260
+ /**
261
+ * Build issue body for a single user story
262
+ */
263
+ function buildUserStoryIssueBody(
264
+ story: UserStory,
265
+ tasks: Task[],
266
+ incrementData: IncrementData,
267
+ githubRepo: string
268
+ ): string {
269
+ const incrementId = incrementData.frontmatter.increment;
270
+ let body = '';
271
+
272
+ // Header with metadata
273
+ body += `**Feature**: ${incrementData.frontmatter.feature_id || 'N/A'}\n`;
274
+ body += `**Status**: ${story.acceptanceCriteria.every(ac => ac.completed) ? 'complete' : 'in-progress'}\n`;
275
+ body += `**Priority**: ${story.priority || incrementData.frontmatter.priority || 'P2'}\n`;
276
+
277
+ body += `\n---\n\n`;
278
+
279
+ // User Story description
280
+ body += `## User Story\n\n`;
281
+ if (story.asA && story.iWant && story.soThat) {
282
+ body += `**As a** ${story.asA}\n`;
283
+ body += `**I want** ${story.iWant}\n`;
284
+ body += `**So that** ${story.soThat}\n\n`;
285
+ } else {
286
+ body += `${story.title}\n\n`;
287
+ }
288
+
289
+ body += `---\n\n`;
290
+
291
+ // Acceptance Criteria
292
+ body += `## Acceptance Criteria\n\n`;
293
+ if (story.acceptanceCriteria.length > 0) {
294
+ const completed = story.acceptanceCriteria.filter(ac => ac.completed).length;
295
+ const total = story.acceptanceCriteria.length;
296
+ const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
297
+ body += `Progress: ${completed}/${total} criteria met (${percentage}%)\n\n`;
298
+
299
+ for (const ac of story.acceptanceCriteria) {
300
+ const checkbox = ac.completed ? '[x]' : '[ ]';
301
+ body += `- ${checkbox} **${ac.id}**: ${ac.description}\n`;
302
+ }
303
+ body += '\n';
304
+ } else {
305
+ body += `*No acceptance criteria defined*\n\n`;
306
+ }
307
+
308
+ body += `---\n\n`;
309
+
310
+ // Tasks for this user story
311
+ const storyTasks = tasks.filter(t =>
312
+ t.userStories.includes(story.id) ||
313
+ t.userStories.some(us => us.includes(story.id.replace('US-', '')))
314
+ );
315
+
316
+ if (storyTasks.length > 0) {
317
+ body += `## Implementation Tasks\n\n`;
318
+ const completedTasks = storyTasks.filter(t => t.completed).length;
319
+ const totalTasks = storyTasks.length;
320
+ const taskPercentage = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
321
+ body += `Progress: ${completedTasks}/${totalTasks} tasks (${taskPercentage}%)\n\n`;
322
+
323
+ for (const task of storyTasks) {
324
+ const checkbox = task.completed ? '[x]' : '[ ]';
325
+ body += `- ${checkbox} **${task.id}**: ${task.title}\n`;
326
+ }
327
+ body += '\n';
328
+
329
+ body += `---\n\n`;
330
+ }
331
+
332
+ // Link to increment
333
+ body += `## SpecWeave Increment\n\n`;
334
+ body += `**Increment**: [${incrementId}](https://github.com/${githubRepo}/tree/develop/.specweave/increments/${incrementId})\n\n`;
335
+
336
+ body += `---\n\n`;
337
+ body += `šŸ¤– Auto-synced by SpecWeave`;
338
+
339
+ return body;
340
+ }
341
+
342
+ /**
343
+ * Create or update GitHub issue for a user story
344
+ */
345
+ async function syncUserStoryIssue(
346
+ owner: string,
347
+ repo: string,
348
+ featureId: string,
349
+ story: UserStory,
350
+ tasks: Task[],
351
+ incrementData: IncrementData,
352
+ milestoneNumber: number,
353
+ existingIssueNumber?: number
354
+ ): Promise<{ number: number; url: string }> {
355
+ // CORRECT FORMAT: [FS-XXX][US-YYY] User Story Title
356
+ const title = `[${featureId}][${story.id}] ${story.title}`;
357
+ const body = buildUserStoryIssueBody(story, tasks, incrementData, `${owner}/${repo}`);
358
+
359
+ // Labels
360
+ const labels = ['specweave', 'user-story'];
361
+ const priority = story.priority?.toLowerCase() || incrementData.frontmatter.priority?.toLowerCase() || 'p2';
362
+ labels.push(priority);
363
+
364
+ if (existingIssueNumber) {
365
+ // Update existing issue
366
+ const updateResult = await execFileNoThrow('gh', [
367
+ 'issue', 'edit',
368
+ String(existingIssueNumber),
369
+ '--repo', `${owner}/${repo}`,
370
+ '--title', title,
371
+ '--body', body
372
+ ]);
373
+
374
+ if (updateResult.exitCode !== 0) {
375
+ throw new Error(`Failed to update issue #${existingIssueNumber}: ${updateResult.stderr}`);
376
+ }
377
+
378
+ return {
379
+ number: existingIssueNumber,
380
+ url: `https://github.com/${owner}/${repo}/issues/${existingIssueNumber}`
381
+ };
382
+ }
383
+
384
+ // Create new issue
385
+ const createArgs = [
386
+ 'issue', 'create',
387
+ '--repo', `${owner}/${repo}`,
388
+ '--title', title,
389
+ '--body', body,
390
+ '--milestone', String(milestoneNumber)
391
+ ];
392
+
393
+ if (labels.length > 0) {
394
+ createArgs.push('--label', labels.join(','));
395
+ }
396
+
397
+ const createResult = await execFileNoThrow('gh', createArgs);
398
+
399
+ if (createResult.exitCode !== 0) {
400
+ throw new Error(`Failed to create issue: ${createResult.stderr || createResult.stdout}`);
401
+ }
402
+
403
+ // Parse issue URL from output
404
+ const urlMatch = createResult.stdout.match(/https:\/\/github\.com\/[^\s]+\/issues\/(\d+)/);
405
+ if (!urlMatch) {
406
+ throw new Error('Could not parse issue URL from gh output');
407
+ }
408
+
409
+ return {
410
+ number: parseInt(urlMatch[1], 10),
411
+ url: urlMatch[0]
412
+ };
413
+ }
414
+
415
+ async function main() {
416
+ const args = process.argv.slice(2);
417
+
418
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
419
+ console.log('Usage: node github-increment-sync-cli.js <increment-id> [options]');
420
+ console.log('');
421
+ console.log('Creates GitHub issues with CORRECT format:');
422
+ console.log(' - Milestone: [FS-XXX] Feature Title');
423
+ console.log(' - Issues: [FS-XXX][US-YYY] User Story Title (one per US)');
424
+ console.log('');
425
+ console.log('Arguments:');
426
+ console.log(' increment-id Increment ID (e.g., 0002 or 0002-thumbnail-mvp)');
427
+ console.log('');
428
+ console.log('Options:');
429
+ console.log(' --dry-run Preview issues without creating');
430
+ console.log('');
431
+ console.log('Environment:');
432
+ console.log(' GITHUB_TOKEN Required - GitHub personal access token');
433
+ console.log('');
434
+ console.log('Example:');
435
+ console.log(' GITHUB_TOKEN=ghp_xxx node github-increment-sync-cli.js 0002');
436
+ process.exit(args.length === 0 ? 1 : 0);
437
+ }
438
+
439
+ const incrementId = args[0];
440
+ const dryRun = args.includes('--dry-run');
441
+
442
+ console.log(`\nšŸ™ GitHub Increment Sync CLI (Per-User-Story Mode)`);
443
+ console.log(` Increment: ${incrementId}`);
444
+
445
+ // Find increment folder
446
+ const incrementPath = await findIncrementFolder(incrementId);
447
+ if (!incrementPath) {
448
+ console.error(`āŒ Increment not found: ${incrementId}`);
449
+ console.error(' Check: ls .specweave/increments/');
450
+ process.exit(1);
451
+ }
452
+
453
+ const fullIncrementId = path.basename(incrementPath);
454
+ console.log(` Found: ${fullIncrementId}`);
455
+
456
+ // For dry-run, we can skip config validation
457
+ let config: GitHubConfig | null = null;
458
+ if (!dryRun) {
459
+ config = await loadGitHubConfig();
460
+ if (!config) {
461
+ process.exit(1);
462
+ }
463
+ console.log(` Repository: ${config.owner}/${config.repo}`);
464
+ } else {
465
+ config = await loadGitHubConfig().catch((): null => null);
466
+ if (config) {
467
+ console.log(` Repository: ${config.owner}/${config.repo}`);
468
+ } else {
469
+ console.log(` Repository: (not detected - dry run mode)`);
470
+ }
471
+ }
472
+
473
+ // Parse increment
474
+ const projectRoot = process.cwd();
475
+ const builder = new IncrementIssueBuilder(incrementPath, projectRoot);
476
+
477
+ try {
478
+ console.log(`\nšŸ”„ Parsing increment spec.md...`);
479
+ const incrementData = await builder.parse();
480
+
481
+ const featureId = incrementData.frontmatter.feature_id ||
482
+ `FS-${fullIncrementId.match(/^(\d+)/)?.[1]?.padStart(3, '0') || 'UNKNOWN'}`;
483
+
484
+ console.log(` šŸ“¦ Feature: ${featureId}`);
485
+ console.log(` šŸ“¦ Title: ${incrementData.title}`);
486
+ console.log(` šŸ“ User Stories: ${incrementData.userStories.length}`);
487
+
488
+ const totalACs = incrementData.userStories.reduce(
489
+ (sum, us) => sum + us.acceptanceCriteria.length, 0
490
+ );
491
+ console.log(` āœ“ Acceptance Criteria: ${totalACs}`);
492
+ console.log(` šŸ”§ Tasks: ${incrementData.tasks.length}`);
493
+
494
+ if (incrementData.userStories.length === 0) {
495
+ console.error(`\nāŒ No user stories found in spec.md`);
496
+ console.error(' Ensure spec.md has ### US-XXX: Title sections');
497
+ process.exit(1);
498
+ }
499
+
500
+ // Preview issues
501
+ console.log(`\nšŸ“‹ Issues to Create/Update:`);
502
+ console.log(` šŸŽÆ Milestone: [${featureId}] ${incrementData.title}`);
503
+ for (const story of incrementData.userStories) {
504
+ const storyTasks = incrementData.tasks.filter(t =>
505
+ t.userStories.includes(story.id) ||
506
+ t.userStories.some(us => us.includes(story.id.replace('US-', '')))
507
+ );
508
+ console.log(` šŸ“ [${featureId}][${story.id}] ${story.title}`);
509
+ console.log(` └─ ${story.acceptanceCriteria.length} ACs, ${storyTasks.length} tasks`);
510
+ }
511
+
512
+ if (dryRun) {
513
+ console.log(`\nšŸ“„ Sample Issue Body (${incrementData.userStories[0].id}):\n`);
514
+ const sampleBody = buildUserStoryIssueBody(
515
+ incrementData.userStories[0],
516
+ incrementData.tasks,
517
+ incrementData,
518
+ config ? `${config.owner}/${config.repo}` : 'owner/repo'
519
+ );
520
+ console.log(sampleBody);
521
+ console.log(`\nāœ… Dry run complete (no issues created)`);
522
+ process.exit(0);
523
+ }
524
+
525
+ // After dry-run check, config is guaranteed to be non-null
526
+ if (!config) {
527
+ console.error('āŒ GitHub config not available');
528
+ process.exit(1);
529
+ }
530
+
531
+ // Load existing links
532
+ const existingLinks = loadExistingGitHubLinks(incrementPath);
533
+
534
+ // Create or get milestone
535
+ console.log(`\nšŸŽÆ Creating/updating milestone...`);
536
+ const milestone = await createOrGetMilestone(
537
+ config.owner,
538
+ config.repo,
539
+ featureId,
540
+ incrementData.title,
541
+ existingLinks.milestone
542
+ );
543
+ console.log(` āœ… Milestone #${milestone.number}: [${featureId}] ${incrementData.title}`);
544
+
545
+ // Create/update issues for each user story
546
+ console.log(`\nšŸ“ Creating/updating user story issues...`);
547
+ const userStoryIssues: Record<string, number> = {};
548
+
549
+ for (const story of incrementData.userStories) {
550
+ const existingIssue = existingLinks.userStoryIssues[story.id];
551
+
552
+ const issue = await syncUserStoryIssue(
553
+ config.owner,
554
+ config.repo,
555
+ featureId,
556
+ story,
557
+ incrementData.tasks,
558
+ incrementData,
559
+ milestone.number,
560
+ existingIssue
561
+ );
562
+
563
+ userStoryIssues[story.id] = issue.number;
564
+
565
+ const action = existingIssue ? 'ā™»ļø Updated' : 'āœ… Created';
566
+ console.log(` ${action} #${issue.number}: [${featureId}][${story.id}] ${story.title}`);
567
+ }
568
+
569
+ // Update metadata
570
+ await updateIncrementMetadata(incrementPath, milestone.number, userStoryIssues);
571
+ console.log(`\nšŸ“ Metadata updated`);
572
+
573
+ console.log(`\nāœ… Sync complete!`);
574
+ console.log(` šŸŽÆ Milestone: ${milestone.url}`);
575
+ console.log(` šŸ“ Issues: ${Object.keys(userStoryIssues).length} user stories synced`);
576
+
577
+ process.exit(0);
578
+ } catch (error) {
579
+ console.error(`\nāŒ Sync failed:`, error);
580
+ process.exit(1);
581
+ }
582
+ }
583
+
584
+ // Run CLI
585
+ main().catch(error => {
586
+ console.error('Fatal error:', error);
587
+ process.exit(1);
588
+ });
@@ -26,6 +26,9 @@ class GitHubStatusSync {
26
26
  /**
27
27
  * Update GitHub issue status
28
28
  *
29
+ * Preserves existing labels that are not status-related.
30
+ * Only replaces labels that start with "status:" prefix.
31
+ *
29
32
  * @param issueNumber - GitHub issue number
30
33
  * @param status - New status (state and labels)
31
34
  */
@@ -37,10 +40,43 @@ class GitHubStatusSync {
37
40
  state: status.state
38
41
  };
39
42
  if (status.labels && status.labels.length > 0) {
40
- updateData.labels = status.labels;
43
+ const currentLabels = await this.getCurrentLabels(issueNumber);
44
+ const preservedLabels = currentLabels.filter(
45
+ (label) => !label.startsWith("status:")
46
+ );
47
+ const newStatusLabels = status.labels.filter(
48
+ (label) => label.startsWith("status:")
49
+ );
50
+ const newOtherLabels = status.labels.filter(
51
+ (label) => !label.startsWith("status:")
52
+ );
53
+ const mergedLabels = [.../* @__PURE__ */ new Set([
54
+ ...preservedLabels,
55
+ ...newStatusLabels,
56
+ ...newOtherLabels
57
+ ])];
58
+ updateData.labels = mergedLabels;
41
59
  }
42
60
  await this.octokit.rest.issues.update(updateData);
43
61
  }
62
+ /**
63
+ * Get current labels from GitHub issue
64
+ *
65
+ * @param issueNumber - GitHub issue number
66
+ * @returns Array of current label names
67
+ */
68
+ async getCurrentLabels(issueNumber) {
69
+ try {
70
+ const response = await this.octokit.rest.issues.get({
71
+ owner: this.owner,
72
+ repo: this.repo,
73
+ issue_number: issueNumber
74
+ });
75
+ return response.data.labels.map((label) => typeof label === "string" ? label : label.name).filter(Boolean);
76
+ } catch {
77
+ return [];
78
+ }
79
+ }
44
80
  /**
45
81
  * Post status change comment to GitHub issue
46
82
  *