specweave 1.0.255 → 1.0.256

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 (92) hide show
  1. package/CLAUDE.md +24 -24
  2. package/README.md +138 -203
  3. package/dist/src/core/ac-checkbox-formatter.d.ts +24 -0
  4. package/dist/src/core/ac-checkbox-formatter.d.ts.map +1 -0
  5. package/dist/src/core/ac-checkbox-formatter.js +35 -0
  6. package/dist/src/core/ac-checkbox-formatter.js.map +1 -0
  7. package/dist/src/core/ac-progress-sync.d.ts +116 -0
  8. package/dist/src/core/ac-progress-sync.d.ts.map +1 -0
  9. package/dist/src/core/ac-progress-sync.js +272 -0
  10. package/dist/src/core/ac-progress-sync.js.map +1 -0
  11. package/dist/src/core/fabric/registry-schema.d.ts +79 -0
  12. package/dist/src/core/fabric/registry-schema.d.ts.map +1 -0
  13. package/dist/src/core/fabric/registry-schema.js +6 -0
  14. package/dist/src/core/fabric/registry-schema.js.map +1 -0
  15. package/dist/src/core/fabric/security-scanner.d.ts +12 -0
  16. package/dist/src/core/fabric/security-scanner.d.ts.map +1 -0
  17. package/dist/src/core/fabric/security-scanner.js +219 -0
  18. package/dist/src/core/fabric/security-scanner.js.map +1 -0
  19. package/dist/src/core/types/sync-profile.d.ts +44 -0
  20. package/dist/src/core/types/sync-profile.d.ts.map +1 -1
  21. package/dist/src/core/types/sync-profile.js.map +1 -1
  22. package/package.json +1 -1
  23. package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +4 -4
  24. package/plugins/{specweave-github/hooks/github-ac-sync-handler.sh → specweave/hooks/v2/handlers/ac-sync-dispatcher.sh} +96 -92
  25. package/plugins/specweave/skills/architect/SKILL.md +1 -1
  26. package/plugins/specweave/skills/auto/SKILL.md +1 -1
  27. package/plugins/specweave/skills/cancel-auto/SKILL.md +1 -1
  28. package/plugins/specweave/skills/code-simplifier/SKILL.md +1 -1
  29. package/plugins/specweave/skills/do/SKILL.md +1 -1
  30. package/plugins/specweave/skills/docs/SKILL.md +1 -1
  31. package/plugins/specweave/skills/docs-updater/SKILL.md +1 -1
  32. package/plugins/specweave/skills/done/SKILL.md +13 -70
  33. package/plugins/specweave/skills/framework/SKILL.md +1 -1
  34. package/plugins/specweave/skills/grill/SKILL.md +1 -1
  35. package/plugins/specweave/skills/increment/SKILL.md +1 -1
  36. package/plugins/specweave/skills/increment-planner/SKILL.md +1 -1
  37. package/plugins/specweave/skills/lsp/SKILL.md +1 -1
  38. package/plugins/specweave/skills/pm/SKILL.md +1 -1
  39. package/plugins/specweave/skills/progress/SKILL.md +1 -1
  40. package/plugins/specweave/skills/save/SKILL.md +1 -1
  41. package/plugins/specweave/skills/security/SKILL.md +1 -1
  42. package/plugins/specweave/skills/security-patterns/SKILL.md +1 -1
  43. package/plugins/specweave/skills/tdd-cycle/SKILL.md +1 -1
  44. package/plugins/specweave/skills/tdd-green/SKILL.md +1 -1
  45. package/plugins/specweave/skills/tdd-orchestrator/SKILL.md +1 -1
  46. package/plugins/specweave/skills/tdd-red/SKILL.md +1 -1
  47. package/plugins/specweave/skills/validate/SKILL.md +1 -1
  48. package/plugins/specweave-github/commands/sync.md +1 -22
  49. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.d.ts +0 -205
  50. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.d.ts.map +0 -1
  51. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.js +0 -685
  52. package/dist/plugins/specweave-github/lib/ThreeLayerSyncManager.js.map +0 -1
  53. package/dist/plugins/specweave-github/lib/cli-sync-increment-changes.d.ts +0 -12
  54. package/dist/plugins/specweave-github/lib/cli-sync-increment-changes.d.ts.map +0 -1
  55. package/dist/plugins/specweave-github/lib/cli-sync-increment-changes.js +0 -28
  56. package/dist/plugins/specweave-github/lib/cli-sync-increment-changes.js.map +0 -1
  57. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts +0 -21
  58. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts.map +0 -1
  59. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js +0 -471
  60. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js.map +0 -1
  61. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts +0 -53
  62. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts.map +0 -1
  63. package/dist/plugins/specweave-github/lib/github-status-sync.js +0 -120
  64. package/dist/plugins/specweave-github/lib/github-status-sync.js.map +0 -1
  65. package/dist/plugins/specweave-github/lib/github-sync-increment-changes.d.ts +0 -18
  66. package/dist/plugins/specweave-github/lib/github-sync-increment-changes.d.ts.map +0 -1
  67. package/dist/plugins/specweave-github/lib/github-sync-increment-changes.js +0 -297
  68. package/dist/plugins/specweave-github/lib/github-sync-increment-changes.js.map +0 -1
  69. package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts +0 -94
  70. package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts.map +0 -1
  71. package/dist/plugins/specweave-github/lib/increment-issue-builder.js +0 -385
  72. package/dist/plugins/specweave-github/lib/increment-issue-builder.js.map +0 -1
  73. package/plugins/specweave-github/lib/ThreeLayerSyncManager.js +0 -611
  74. package/plugins/specweave-github/lib/ThreeLayerSyncManager.ts +0 -909
  75. package/plugins/specweave-github/lib/cli-sync-increment-changes.d.js +0 -1
  76. package/plugins/specweave-github/lib/cli-sync-increment-changes.d.ts +0 -12
  77. package/plugins/specweave-github/lib/cli-sync-increment-changes.d.ts.map +0 -1
  78. package/plugins/specweave-github/lib/cli-sync-increment-changes.js +0 -17
  79. package/plugins/specweave-github/lib/cli-sync-increment-changes.js.map +0 -1
  80. package/plugins/specweave-github/lib/cli-sync-increment-changes.ts +0 -33
  81. package/plugins/specweave-github/lib/github-increment-sync-cli.js +0 -474
  82. package/plugins/specweave-github/lib/github-increment-sync-cli.ts +0 -616
  83. package/plugins/specweave-github/lib/github-status-sync.js +0 -107
  84. package/plugins/specweave-github/lib/github-status-sync.ts +0 -163
  85. package/plugins/specweave-github/lib/github-sync-increment-changes.d.js +0 -0
  86. package/plugins/specweave-github/lib/github-sync-increment-changes.d.ts +0 -18
  87. package/plugins/specweave-github/lib/github-sync-increment-changes.d.ts.map +0 -1
  88. package/plugins/specweave-github/lib/github-sync-increment-changes.js +0 -253
  89. package/plugins/specweave-github/lib/github-sync-increment-changes.js.map +0 -1
  90. package/plugins/specweave-github/lib/github-sync-increment-changes.ts +0 -391
  91. package/plugins/specweave-github/lib/increment-issue-builder.js +0 -402
  92. package/plugins/specweave-github/lib/increment-issue-builder.ts +0 -520
@@ -1 +0,0 @@
1
- #!/usr/bin/env node
@@ -1,12 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * CLI wrapper for syncing increment changes to GitHub
4
- *
5
- * Usage:
6
- * node dist/plugins/specweave-github/lib/cli-sync-increment-changes.js <incrementId> <changedFile>
7
- *
8
- * Example:
9
- * node dist/plugins/specweave-github/lib/cli-sync-increment-changes.js 0015-hierarchical-sync spec.md
10
- */
11
- export {};
12
- //# sourceMappingURL=cli-sync-increment-changes.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli-sync-increment-changes.d.ts","sourceRoot":"","sources":["cli-sync-increment-changes.ts"],"names":[],"mappings":";AAEA;;;;;;;;GAQG"}
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env node
2
- import { syncIncrementChanges } from "./github-sync-increment-changes.js";
3
- const incrementId = process.argv[2];
4
- const changedFile = process.argv[3];
5
- if (!incrementId || !changedFile) {
6
- console.error("\u274C Usage: cli-sync-increment-changes <incrementId> <changedFile>");
7
- console.error(" Example: cli-sync-increment-changes 0015-hierarchical-sync spec.md");
8
- process.exit(1);
9
- }
10
- if (!["spec.md", "plan.md", "tasks.md"].includes(changedFile)) {
11
- console.error(`\u274C Invalid file: ${changedFile}`);
12
- console.error(" Must be one of: spec.md, plan.md, tasks.md");
13
- process.exit(1);
14
- }
15
- syncIncrementChanges(incrementId, changedFile).catch((error) => {
16
- console.error("\u274C Fatal error:", error);
17
- });
@@ -1 +0,0 @@
1
- {"version":3,"file":"cli-sync-increment-changes.js","sourceRoot":"","sources":["cli-sync-increment-changes.ts"],"names":[],"mappings":";AAEA;;;;;;;;GAQG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAE1E,MAAM,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACpC,MAAM,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAuC,CAAC;AAE1E,IAAI,CAAC,WAAW,IAAI,CAAC,WAAW,EAAE,CAAC;IACjC,OAAO,CAAC,KAAK,CAAC,iEAAiE,CAAC,CAAC;IACjF,OAAO,CAAC,KAAK,CAAC,uEAAuE,CAAC,CAAC;IACvF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,IAAI,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;IAC9D,OAAO,CAAC,KAAK,CAAC,mBAAmB,WAAW,EAAE,CAAC,CAAC;IAChD,OAAO,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC;IAC/D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,oBAAoB,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IAC7D,OAAO,CAAC,KAAK,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC;IACvC,gDAAgD;AAClD,CAAC,CAAC,CAAC"}
@@ -1,33 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * CLI wrapper for syncing increment changes to GitHub
5
- *
6
- * Usage:
7
- * node dist/plugins/specweave-github/lib/cli-sync-increment-changes.js <incrementId> <changedFile>
8
- *
9
- * Example:
10
- * node dist/plugins/specweave-github/lib/cli-sync-increment-changes.js 0015-hierarchical-sync spec.md
11
- */
12
-
13
- import { syncIncrementChanges } from './github-sync-increment-changes.js';
14
-
15
- const incrementId = process.argv[2];
16
- const changedFile = process.argv[3] as 'spec.md' | 'plan.md' | 'tasks.md';
17
-
18
- if (!incrementId || !changedFile) {
19
- console.error('❌ Usage: cli-sync-increment-changes <incrementId> <changedFile>');
20
- console.error(' Example: cli-sync-increment-changes 0015-hierarchical-sync spec.md');
21
- process.exit(1);
22
- }
23
-
24
- if (!['spec.md', 'plan.md', 'tasks.md'].includes(changedFile)) {
25
- console.error(`❌ Invalid file: ${changedFile}`);
26
- console.error(' Must be one of: spec.md, plan.md, tasks.md');
27
- process.exit(1);
28
- }
29
-
30
- syncIncrementChanges(incrementId, changedFile).catch((error) => {
31
- console.error('❌ Fatal error:', error);
32
- // Don't exit with error code - best-effort sync
33
- });
@@ -1,474 +0,0 @@
1
- #!/usr/bin/env node
2
- import { existsSync, readFileSync } from "fs";
3
- import * as fs from "fs/promises";
4
- import * as path from "path";
5
- import { IncrementIssueBuilder } from "./increment-issue-builder.js";
6
- import { execFileNoThrow } from "../../../src/utils/execFileNoThrow.js";
7
- import { getGitHubAuthFromProject } from "../../../src/utils/auth-helpers.js";
8
- function getGhEnv() {
9
- const { token } = getGitHubAuthFromProject(process.cwd());
10
- return token ? { ...process.env, GH_TOKEN: token } : process.env;
11
- }
12
- async function loadGitHubConfig() {
13
- const projectRoot = process.cwd();
14
- const configPath = path.join(projectRoot, ".specweave/config.json");
15
- let owner = process.env.GITHUB_OWNER || "";
16
- let repo = process.env.GITHUB_REPO || "";
17
- const token = process.env.GITHUB_TOKEN || "";
18
- if (existsSync(configPath)) {
19
- try {
20
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
21
- if (config.sync?.github?.owner && config.sync?.github?.repo) {
22
- owner = config.sync.github.owner;
23
- repo = config.sync.github.repo;
24
- } else if (config.multiProject?.enabled && config.multiProject?.activeProject) {
25
- const activeProject = config.multiProject.activeProject;
26
- const projectConfig = config.multiProject.projects?.[activeProject];
27
- if (projectConfig?.externalTools?.github?.repository) {
28
- const parts = projectConfig.externalTools.github.repository.split("/");
29
- if (parts.length === 2) {
30
- owner = parts[0];
31
- repo = parts[1];
32
- }
33
- }
34
- } else if (config.sync?.defaultProfile && config.sync?.profiles) {
35
- const profile = config.sync.profiles[config.sync.defaultProfile];
36
- if (profile?.config?.owner && profile?.config?.repo) {
37
- owner = profile.config.owner;
38
- repo = profile.config.repo;
39
- }
40
- }
41
- } catch (error) {
42
- console.error("\u26A0\uFE0F Failed to parse config.json:", error);
43
- }
44
- }
45
- if (!owner || !repo) {
46
- const result = await execFileNoThrow("git", ["remote", "get-url", "origin"]);
47
- if (result.exitCode === 0 && result.stdout) {
48
- const remoteUrl = result.stdout.trim();
49
- const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
50
- if (match) {
51
- owner = owner || match[1];
52
- repo = repo || match[2];
53
- }
54
- }
55
- }
56
- if (!token) {
57
- console.error("\u274C GITHUB_TOKEN not set");
58
- console.error(" Set it in .env file or export GITHUB_TOKEN=ghp_xxx");
59
- return null;
60
- }
61
- if (!owner || !repo) {
62
- console.error("\u274C Could not detect GitHub owner/repo");
63
- console.error(" Set sync.github.owner and sync.github.repo in .specweave/config.json");
64
- return null;
65
- }
66
- return { owner, repo, token };
67
- }
68
- async function findIncrementFolder(incrementId) {
69
- const projectRoot = process.cwd();
70
- const incrementsDir = path.join(projectRoot, ".specweave/increments");
71
- if (!existsSync(incrementsDir)) {
72
- return null;
73
- }
74
- const entries = await fs.readdir(incrementsDir, { withFileTypes: true });
75
- for (const entry of entries) {
76
- if (!entry.isDirectory()) continue;
77
- if (entry.name.startsWith("_")) continue;
78
- if (entry.name === incrementId) {
79
- return path.join(incrementsDir, entry.name);
80
- }
81
- if (entry.name.startsWith(incrementId + "-")) {
82
- return path.join(incrementsDir, entry.name);
83
- }
84
- }
85
- return null;
86
- }
87
- function loadExistingGitHubLinks(incrementPath) {
88
- const metadataPath = path.join(incrementPath, "metadata.json");
89
- if (!existsSync(metadataPath)) {
90
- return { userStoryIssues: {} };
91
- }
92
- try {
93
- const metadata = JSON.parse(readFileSync(metadataPath, "utf-8"));
94
- return {
95
- milestone: metadata.github?.milestone,
96
- userStoryIssues: metadata.github?.userStoryIssues || {}
97
- };
98
- } catch {
99
- return { userStoryIssues: {} };
100
- }
101
- }
102
- async function updateIncrementMetadata(incrementPath, milestoneNumber, userStoryIssues) {
103
- const metadataPath = path.join(incrementPath, "metadata.json");
104
- let metadata = {};
105
- if (existsSync(metadataPath)) {
106
- try {
107
- metadata = JSON.parse(readFileSync(metadataPath, "utf-8"));
108
- } catch {
109
- }
110
- }
111
- metadata.github = {
112
- milestone: milestoneNumber,
113
- userStoryIssues,
114
- lastSync: (/* @__PURE__ */ new Date()).toISOString()
115
- };
116
- await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2) + "\n");
117
- }
118
- async function createOrGetMilestone(owner, repo, featureId, title, existingMilestone) {
119
- if (existingMilestone) {
120
- const verifyResult = await execFileNoThrow("gh", [
121
- "api",
122
- `repos/${owner}/${repo}/milestones/${existingMilestone}`,
123
- "--jq",
124
- ".number"
125
- ], { env: getGhEnv() });
126
- if (verifyResult.exitCode === 0) {
127
- return {
128
- number: existingMilestone,
129
- url: `https://github.com/${owner}/${repo}/milestone/${existingMilestone}`
130
- };
131
- }
132
- }
133
- const searchResult = await execFileNoThrow("gh", [
134
- "api",
135
- `repos/${owner}/${repo}/milestones`,
136
- "--jq",
137
- `.[] | select(.title | contains("${featureId}")) | .number`
138
- ], { env: getGhEnv() });
139
- if (searchResult.exitCode === 0 && searchResult.stdout.trim()) {
140
- const milestoneNumber2 = parseInt(searchResult.stdout.trim().split("\n")[0], 10);
141
- return {
142
- number: milestoneNumber2,
143
- url: `https://github.com/${owner}/${repo}/milestone/${milestoneNumber2}`
144
- };
145
- }
146
- const milestoneTitle = `[${featureId}] ${title}`;
147
- const createResult = await execFileNoThrow("gh", [
148
- "api",
149
- `repos/${owner}/${repo}/milestones`,
150
- "-X",
151
- "POST",
152
- "-f",
153
- `title=${milestoneTitle}`,
154
- "-f",
155
- `description=Feature milestone for ${featureId}`,
156
- "--jq",
157
- ".number"
158
- ], { env: getGhEnv() });
159
- if (createResult.exitCode !== 0) {
160
- throw new Error(`Failed to create milestone: ${createResult.stderr || createResult.stdout}`);
161
- }
162
- const milestoneNumber = parseInt(createResult.stdout.trim(), 10);
163
- return {
164
- number: milestoneNumber,
165
- url: `https://github.com/${owner}/${repo}/milestone/${milestoneNumber}`
166
- };
167
- }
168
- function buildUserStoryIssueBody(story, tasks, incrementData, githubRepo) {
169
- const incrementId = incrementData.frontmatter.increment;
170
- let body = "";
171
- const completedACs = story.acceptanceCriteria.filter((ac) => ac.completed).length;
172
- const totalACs = story.acceptanceCriteria.length;
173
- const storyTasks = tasks.filter(
174
- (t) => t.userStories.includes(story.id) || t.userStories.some((us) => us.includes(story.id.replace("US-", "")))
175
- );
176
- const completedTasks = storyTasks.filter((t) => t.completed).length;
177
- const totalTasks = storyTasks.length;
178
- const overallPercentage = totalACs + totalTasks > 0 ? Math.round((completedACs + completedTasks) / (totalACs + totalTasks) * 100) : 0;
179
- body += `## Progress
180
-
181
- `;
182
- body += `**Acceptance Criteria**: ${completedACs}/${totalACs} (${totalACs > 0 ? Math.round(completedACs / totalACs * 100) : 0}%)
183
- `;
184
- body += `**Tasks**: ${completedTasks}/${totalTasks} (${totalTasks > 0 ? Math.round(completedTasks / totalTasks * 100) : 0}%)
185
- `;
186
- body += `**Overall**: ${overallPercentage}%
187
-
188
- `;
189
- const filledBlocks = Math.floor(overallPercentage / 5);
190
- const emptyBlocks = 20 - filledBlocks;
191
- body += `${"\u2588".repeat(filledBlocks)}${"\u2591".repeat(emptyBlocks)} ${overallPercentage}%
192
-
193
- `;
194
- body += `---
195
-
196
- `;
197
- body += `## User Story
198
-
199
- `;
200
- if (story.asA && story.iWant && story.soThat) {
201
- body += `**As a** ${story.asA}
202
- `;
203
- body += `**I want** ${story.iWant}
204
- `;
205
- body += `**So that** ${story.soThat}
206
-
207
- `;
208
- } else {
209
- body += `${story.title}
210
-
211
- `;
212
- }
213
- body += `---
214
-
215
- `;
216
- body += `## Acceptance Criteria
217
-
218
- `;
219
- if (story.acceptanceCriteria.length > 0) {
220
- const completed = story.acceptanceCriteria.filter((ac) => ac.completed).length;
221
- const total = story.acceptanceCriteria.length;
222
- const percentage = total > 0 ? Math.round(completed / total * 100) : 0;
223
- body += `Progress: ${completed}/${total} criteria met (${percentage}%)
224
-
225
- `;
226
- for (const ac of story.acceptanceCriteria) {
227
- const checkbox = ac.completed ? "[x]" : "[ ]";
228
- body += `- ${checkbox} **${ac.id}**: ${ac.description}
229
- `;
230
- }
231
- body += "\n";
232
- } else {
233
- body += `*No acceptance criteria defined*
234
-
235
- `;
236
- }
237
- body += `---
238
-
239
- `;
240
- if (storyTasks.length > 0) {
241
- body += `## Implementation Tasks
242
-
243
- `;
244
- const completedTasks2 = storyTasks.filter((t) => t.completed).length;
245
- const totalTasks2 = storyTasks.length;
246
- const taskPercentage = totalTasks2 > 0 ? Math.round(completedTasks2 / totalTasks2 * 100) : 0;
247
- body += `Progress: ${completedTasks2}/${totalTasks2} tasks (${taskPercentage}%)
248
-
249
- `;
250
- for (const task of storyTasks) {
251
- const checkbox = task.completed ? "[x]" : "[ ]";
252
- body += `- ${checkbox} **${task.id}**: ${task.title}
253
- `;
254
- }
255
- body += "\n";
256
- body += `---
257
-
258
- `;
259
- }
260
- body += `## SpecWeave Increment
261
-
262
- `;
263
- body += `**Increment**: [${incrementId}](https://github.com/${githubRepo}/tree/develop/.specweave/increments/${incrementId})
264
-
265
- `;
266
- body += `---
267
-
268
- `;
269
- body += `\u{1F916} Auto-synced by SpecWeave`;
270
- return body;
271
- }
272
- async function syncUserStoryIssue(owner, repo, featureId, story, tasks, incrementData, milestoneNumber, existingIssueNumber) {
273
- const title = `[${featureId}][${story.id}] ${story.title}`;
274
- const body = buildUserStoryIssueBody(story, tasks, incrementData, `${owner}/${repo}`);
275
- const labels = ["specweave", "user-story"];
276
- const priority = story.priority?.toLowerCase() || incrementData.frontmatter.priority?.toLowerCase() || "p2";
277
- labels.push(priority);
278
- if (existingIssueNumber) {
279
- const updateResult = await execFileNoThrow("gh", [
280
- "issue",
281
- "edit",
282
- String(existingIssueNumber),
283
- "--repo",
284
- `${owner}/${repo}`,
285
- "--title",
286
- title,
287
- "--body",
288
- body
289
- ], { env: getGhEnv() });
290
- if (updateResult.exitCode !== 0) {
291
- throw new Error(`Failed to update issue #${existingIssueNumber}: ${updateResult.stderr}`);
292
- }
293
- return {
294
- number: existingIssueNumber,
295
- url: `https://github.com/${owner}/${repo}/issues/${existingIssueNumber}`
296
- };
297
- }
298
- const createArgs = [
299
- "issue",
300
- "create",
301
- "--repo",
302
- `${owner}/${repo}`,
303
- "--title",
304
- title,
305
- "--body",
306
- body,
307
- "--milestone",
308
- String(milestoneNumber)
309
- ];
310
- if (labels.length > 0) {
311
- createArgs.push("--label", labels.join(","));
312
- }
313
- const createResult = await execFileNoThrow("gh", createArgs, { env: getGhEnv() });
314
- if (createResult.exitCode !== 0) {
315
- throw new Error(`Failed to create issue: ${createResult.stderr || createResult.stdout}`);
316
- }
317
- const urlMatch = createResult.stdout.match(/https:\/\/github\.com\/[^\s]+\/issues\/(\d+)/);
318
- if (!urlMatch) {
319
- throw new Error("Could not parse issue URL from gh output");
320
- }
321
- return {
322
- number: parseInt(urlMatch[1], 10),
323
- url: urlMatch[0]
324
- };
325
- }
326
- async function main() {
327
- const args = process.argv.slice(2);
328
- if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
329
- console.log("Usage: node github-increment-sync-cli.js <increment-id> [options]");
330
- console.log("");
331
- console.log("Creates GitHub issues with CORRECT format:");
332
- console.log(" - Milestone: [FS-XXX] Feature Title");
333
- console.log(" - Issues: [FS-XXX][US-YYY] User Story Title (one per US)");
334
- console.log("");
335
- console.log("Arguments:");
336
- console.log(" increment-id Increment ID (e.g., 0002 or 0002-thumbnail-mvp)");
337
- console.log("");
338
- console.log("Options:");
339
- console.log(" --dry-run Preview issues without creating");
340
- console.log("");
341
- console.log("Environment:");
342
- console.log(" GITHUB_TOKEN Required - GitHub personal access token");
343
- console.log("");
344
- console.log("Example:");
345
- console.log(" GITHUB_TOKEN=ghp_xxx node github-increment-sync-cli.js 0002");
346
- process.exit(args.length === 0 ? 1 : 0);
347
- }
348
- const incrementId = args[0];
349
- const dryRun = args.includes("--dry-run");
350
- console.log(`
351
- \u{1F419} GitHub Increment Sync CLI (Per-User-Story Mode)`);
352
- console.log(` Increment: ${incrementId}`);
353
- const incrementPath = await findIncrementFolder(incrementId);
354
- if (!incrementPath) {
355
- console.error(`\u274C Increment not found: ${incrementId}`);
356
- console.error(" Check: ls .specweave/increments/");
357
- process.exit(1);
358
- }
359
- const fullIncrementId = path.basename(incrementPath);
360
- console.log(` Found: ${fullIncrementId}`);
361
- let config = null;
362
- if (!dryRun) {
363
- config = await loadGitHubConfig();
364
- if (!config) {
365
- process.exit(1);
366
- }
367
- console.log(` Repository: ${config.owner}/${config.repo}`);
368
- } else {
369
- config = await loadGitHubConfig().catch(() => null);
370
- if (config) {
371
- console.log(` Repository: ${config.owner}/${config.repo}`);
372
- } else {
373
- console.log(` Repository: (not detected - dry run mode)`);
374
- }
375
- }
376
- const projectRoot = process.cwd();
377
- const builder = new IncrementIssueBuilder(incrementPath, projectRoot);
378
- try {
379
- console.log(`
380
- \u{1F504} Parsing increment spec.md...`);
381
- const incrementData = await builder.parse();
382
- const featureId = incrementData.frontmatter.feature_id || `FS-${fullIncrementId.match(/^(\d+)/)?.[1]?.padStart(3, "0") || "UNKNOWN"}`;
383
- console.log(` \u{1F4E6} Feature: ${featureId}`);
384
- console.log(` \u{1F4E6} Title: ${incrementData.title}`);
385
- console.log(` \u{1F4DD} User Stories: ${incrementData.userStories.length}`);
386
- const totalACs = incrementData.userStories.reduce(
387
- (sum, us) => sum + us.acceptanceCriteria.length,
388
- 0
389
- );
390
- console.log(` \u2713 Acceptance Criteria: ${totalACs}`);
391
- console.log(` \u{1F527} Tasks: ${incrementData.tasks.length}`);
392
- if (incrementData.userStories.length === 0) {
393
- console.error(`
394
- \u274C No user stories found in spec.md`);
395
- console.error(" Ensure spec.md has ### US-XXX: Title sections");
396
- process.exit(1);
397
- }
398
- console.log(`
399
- \u{1F4CB} Issues to Create/Update:`);
400
- console.log(` \u{1F3AF} Milestone: [${featureId}] ${incrementData.title}`);
401
- for (const story of incrementData.userStories) {
402
- const storyTasks = incrementData.tasks.filter(
403
- (t) => t.userStories.includes(story.id) || t.userStories.some((us) => us.includes(story.id.replace("US-", "")))
404
- );
405
- console.log(` \u{1F4DD} [${featureId}][${story.id}] ${story.title}`);
406
- console.log(` \u2514\u2500 ${story.acceptanceCriteria.length} ACs, ${storyTasks.length} tasks`);
407
- }
408
- if (dryRun) {
409
- console.log(`
410
- \u{1F4C4} Sample Issue Body (${incrementData.userStories[0].id}):
411
- `);
412
- const sampleBody = buildUserStoryIssueBody(
413
- incrementData.userStories[0],
414
- incrementData.tasks,
415
- incrementData,
416
- config ? `${config.owner}/${config.repo}` : "owner/repo"
417
- );
418
- console.log(sampleBody);
419
- console.log(`
420
- \u2705 Dry run complete (no issues created)`);
421
- process.exit(0);
422
- }
423
- if (!config) {
424
- console.error("\u274C GitHub config not available");
425
- process.exit(1);
426
- }
427
- const existingLinks = loadExistingGitHubLinks(incrementPath);
428
- console.log(`
429
- \u{1F3AF} Creating/updating milestone...`);
430
- const milestone = await createOrGetMilestone(
431
- config.owner,
432
- config.repo,
433
- featureId,
434
- incrementData.title,
435
- existingLinks.milestone
436
- );
437
- console.log(` \u2705 Milestone #${milestone.number}: [${featureId}] ${incrementData.title}`);
438
- console.log(`
439
- \u{1F4DD} Creating/updating user story issues...`);
440
- const userStoryIssues = {};
441
- for (const story of incrementData.userStories) {
442
- const existingIssue = existingLinks.userStoryIssues[story.id];
443
- const issue = await syncUserStoryIssue(
444
- config.owner,
445
- config.repo,
446
- featureId,
447
- story,
448
- incrementData.tasks,
449
- incrementData,
450
- milestone.number,
451
- existingIssue
452
- );
453
- userStoryIssues[story.id] = issue.number;
454
- const action = existingIssue ? "\u267B\uFE0F Updated" : "\u2705 Created";
455
- console.log(` ${action} #${issue.number}: [${featureId}][${story.id}] ${story.title}`);
456
- }
457
- await updateIncrementMetadata(incrementPath, milestone.number, userStoryIssues);
458
- console.log(`
459
- \u{1F4DD} Metadata updated`);
460
- console.log(`
461
- \u2705 Sync complete!`);
462
- console.log(` \u{1F3AF} Milestone: ${milestone.url}`);
463
- console.log(` \u{1F4DD} Issues: ${Object.keys(userStoryIssues).length} user stories synced`);
464
- process.exit(0);
465
- } catch (error) {
466
- console.error(`
467
- \u274C Sync failed:`, error);
468
- process.exit(1);
469
- }
470
- }
471
- main().catch((error) => {
472
- console.error("Fatal error:", error);
473
- process.exit(1);
474
- });