specweave 0.17.15 → 0.17.17

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 (200) hide show
  1. package/CLAUDE.md +405 -2495
  2. package/README.md +92 -2
  3. package/dist/locales/de/.gitkeep +0 -0
  4. package/dist/locales/de/cli.json +108 -0
  5. package/dist/locales/en/cli.json +287 -0
  6. package/dist/locales/en/errors.json +7 -0
  7. package/dist/locales/en/templates.json +6 -0
  8. package/dist/locales/es/.gitkeep +0 -0
  9. package/dist/locales/es/cli.json +41 -0
  10. package/dist/locales/fr/.gitkeep +0 -0
  11. package/dist/locales/fr/cli.json +108 -0
  12. package/dist/locales/ja/.gitkeep +0 -0
  13. package/dist/locales/ja/cli.json +108 -0
  14. package/dist/locales/ko/.gitkeep +0 -0
  15. package/dist/locales/ko/cli.json +108 -0
  16. package/dist/locales/pt/.gitkeep +0 -0
  17. package/dist/locales/pt/cli.json +108 -0
  18. package/dist/locales/ru/.gitkeep +0 -0
  19. package/dist/locales/ru/cli.json +269 -0
  20. package/dist/locales/zh/.gitkeep +0 -0
  21. package/dist/locales/zh/cli.json +108 -0
  22. package/dist/plugins/specweave/lib/hooks/sync-living-docs.d.ts.map +1 -1
  23. package/dist/plugins/specweave/lib/hooks/sync-living-docs.js +188 -36
  24. package/dist/plugins/specweave/lib/hooks/sync-living-docs.js.map +1 -1
  25. package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts +54 -0
  26. package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts.map +1 -0
  27. package/dist/plugins/specweave-ado/lib/ado-status-sync.js +86 -0
  28. package/dist/plugins/specweave-ado/lib/ado-status-sync.js.map +1 -0
  29. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.d.ts +25 -0
  30. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.d.ts.map +1 -0
  31. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.js +191 -0
  32. package/dist/plugins/specweave-ado/lib/enhanced-ado-sync.js.map +1 -0
  33. package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts +139 -0
  34. package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts.map +1 -0
  35. package/dist/plugins/specweave-github/lib/duplicate-detector.js +389 -0
  36. package/dist/plugins/specweave-github/lib/duplicate-detector.js.map +1 -0
  37. package/dist/plugins/specweave-github/lib/enhanced-github-sync.d.ts +26 -0
  38. package/dist/plugins/specweave-github/lib/enhanced-github-sync.d.ts.map +1 -0
  39. package/dist/plugins/specweave-github/lib/enhanced-github-sync.js +249 -0
  40. package/dist/plugins/specweave-github/lib/enhanced-github-sync.js.map +1 -0
  41. package/dist/plugins/specweave-github/lib/github-client.d.ts +1 -1
  42. package/dist/plugins/specweave-github/lib/github-client.d.ts.map +1 -1
  43. package/dist/plugins/specweave-github/lib/github-client.js +25 -13
  44. package/dist/plugins/specweave-github/lib/github-client.js.map +1 -1
  45. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts +83 -0
  46. package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts.map +1 -0
  47. package/dist/plugins/specweave-github/lib/github-epic-sync.js +451 -0
  48. package/dist/plugins/specweave-github/lib/github-epic-sync.js.map +1 -0
  49. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts +43 -0
  50. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts.map +1 -0
  51. package/dist/plugins/specweave-github/lib/github-status-sync.js +82 -0
  52. package/dist/plugins/specweave-github/lib/github-status-sync.js.map +1 -0
  53. package/dist/plugins/specweave-github/lib/task-sync.d.ts +5 -0
  54. package/dist/plugins/specweave-github/lib/task-sync.d.ts.map +1 -1
  55. package/dist/plugins/specweave-github/lib/task-sync.js +38 -2
  56. package/dist/plugins/specweave-github/lib/task-sync.js.map +1 -1
  57. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts +26 -0
  58. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.d.ts.map +1 -0
  59. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js +195 -0
  60. package/dist/plugins/specweave-jira/lib/enhanced-jira-sync.js.map +1 -0
  61. package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts +66 -0
  62. package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts.map +1 -0
  63. package/dist/plugins/specweave-jira/lib/jira-epic-sync.js +274 -0
  64. package/dist/plugins/specweave-jira/lib/jira-epic-sync.js.map +1 -0
  65. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +56 -0
  66. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -0
  67. package/dist/plugins/specweave-jira/lib/jira-status-sync.js +93 -0
  68. package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -0
  69. package/dist/spec-parser.js +629 -0
  70. package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
  71. package/dist/src/cli/helpers/issue-tracker/index.js +48 -3
  72. package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
  73. package/dist/src/core/living-docs/hierarchy-mapper.d.ts +142 -0
  74. package/dist/src/core/living-docs/hierarchy-mapper.d.ts.map +1 -0
  75. package/dist/src/core/living-docs/hierarchy-mapper.js +453 -0
  76. package/dist/src/core/living-docs/hierarchy-mapper.js.map +1 -0
  77. package/dist/src/core/living-docs/index.d.ts +10 -84
  78. package/dist/src/core/living-docs/index.d.ts.map +1 -1
  79. package/dist/src/core/living-docs/index.js +10 -164
  80. package/dist/src/core/living-docs/index.js.map +1 -1
  81. package/dist/src/core/living-docs/spec-distributor.d.ts +106 -0
  82. package/dist/src/core/living-docs/spec-distributor.d.ts.map +1 -0
  83. package/dist/src/core/living-docs/spec-distributor.js +823 -0
  84. package/dist/src/core/living-docs/spec-distributor.js.map +1 -0
  85. package/dist/src/core/living-docs/types.d.ts +201 -0
  86. package/dist/src/core/living-docs/types.d.ts.map +1 -0
  87. package/dist/src/core/living-docs/types.js +15 -0
  88. package/dist/src/core/living-docs/types.js.map +1 -0
  89. package/dist/src/core/logging/prompt-logger.d.ts +70 -0
  90. package/dist/src/core/logging/prompt-logger.d.ts.map +1 -0
  91. package/dist/src/core/logging/prompt-logger.js +247 -0
  92. package/dist/src/core/logging/prompt-logger.js.map +1 -0
  93. package/dist/src/core/status-line/status-line-manager.d.ts +15 -24
  94. package/dist/src/core/status-line/status-line-manager.d.ts.map +1 -1
  95. package/dist/src/core/status-line/status-line-manager.js +33 -70
  96. package/dist/src/core/status-line/status-line-manager.js.map +1 -1
  97. package/dist/src/core/status-line/types.d.ts +19 -31
  98. package/dist/src/core/status-line/types.d.ts.map +1 -1
  99. package/dist/src/core/status-line/types.js +5 -9
  100. package/dist/src/core/status-line/types.js.map +1 -1
  101. package/dist/src/core/sync/conflict-resolver.d.ts +66 -0
  102. package/dist/src/core/sync/conflict-resolver.d.ts.map +1 -0
  103. package/dist/src/core/sync/conflict-resolver.js +108 -0
  104. package/dist/src/core/sync/conflict-resolver.js.map +1 -0
  105. package/dist/src/core/sync/enhanced-content-builder.d.ts +77 -0
  106. package/dist/src/core/sync/enhanced-content-builder.d.ts.map +1 -0
  107. package/dist/src/core/sync/enhanced-content-builder.js +199 -0
  108. package/dist/src/core/sync/enhanced-content-builder.js.map +1 -0
  109. package/dist/src/core/sync/label-detector.d.ts +66 -0
  110. package/dist/src/core/sync/label-detector.d.ts.map +1 -0
  111. package/dist/src/core/sync/label-detector.js +211 -0
  112. package/dist/src/core/sync/label-detector.js.map +1 -0
  113. package/dist/src/core/sync/retry-logic.d.ts +64 -0
  114. package/dist/src/core/sync/retry-logic.d.ts.map +1 -0
  115. package/dist/src/core/sync/retry-logic.js +165 -0
  116. package/dist/src/core/sync/retry-logic.js.map +1 -0
  117. package/dist/src/core/sync/spec-content-sync.d.ts +88 -0
  118. package/dist/src/core/sync/spec-content-sync.d.ts.map +1 -0
  119. package/dist/src/core/sync/spec-content-sync.js +5 -0
  120. package/dist/src/core/sync/spec-content-sync.js.map +1 -0
  121. package/dist/src/core/sync/spec-increment-mapper.d.ts +100 -0
  122. package/dist/src/core/sync/spec-increment-mapper.d.ts.map +1 -0
  123. package/dist/src/core/sync/spec-increment-mapper.js +424 -0
  124. package/dist/src/core/sync/spec-increment-mapper.js.map +1 -0
  125. package/dist/src/core/sync/status-cache.d.ts +91 -0
  126. package/dist/src/core/sync/status-cache.d.ts.map +1 -0
  127. package/dist/src/core/sync/status-cache.js +140 -0
  128. package/dist/src/core/sync/status-cache.js.map +1 -0
  129. package/dist/src/core/sync/status-mapper.d.ts +69 -0
  130. package/dist/src/core/sync/status-mapper.d.ts.map +1 -0
  131. package/dist/src/core/sync/status-mapper.js +90 -0
  132. package/dist/src/core/sync/status-mapper.js.map +1 -0
  133. package/dist/src/core/sync/status-sync-engine.d.ts +162 -0
  134. package/dist/src/core/sync/status-sync-engine.d.ts.map +1 -0
  135. package/dist/src/core/sync/status-sync-engine.js +347 -0
  136. package/dist/src/core/sync/status-sync-engine.js.map +1 -0
  137. package/dist/src/core/sync/sync-event-logger.d.ts +99 -0
  138. package/dist/src/core/sync/sync-event-logger.d.ts.map +1 -0
  139. package/dist/src/core/sync/sync-event-logger.js +103 -0
  140. package/dist/src/core/sync/sync-event-logger.js.map +1 -0
  141. package/dist/src/core/sync/workflow-detector.d.ts +95 -0
  142. package/dist/src/core/sync/workflow-detector.d.ts.map +1 -0
  143. package/dist/src/core/sync/workflow-detector.js +175 -0
  144. package/dist/src/core/sync/workflow-detector.js.map +1 -0
  145. package/dist/src/core/types/config.d.ts.map +1 -1
  146. package/dist/src/core/types/config.js +31 -0
  147. package/dist/src/core/types/config.js.map +1 -1
  148. package/dist/src/utils/github-url.d.ts +53 -0
  149. package/dist/src/utils/github-url.d.ts.map +1 -0
  150. package/dist/src/utils/github-url.js +90 -0
  151. package/dist/src/utils/github-url.js.map +1 -0
  152. package/dist/src/utils/plugin-validator.d.ts +9 -0
  153. package/dist/src/utils/plugin-validator.d.ts.map +1 -1
  154. package/dist/src/utils/plugin-validator.js +86 -19
  155. package/dist/src/utils/plugin-validator.js.map +1 -1
  156. package/dist/src/utils/spec-parser.d.ts +145 -0
  157. package/dist/src/utils/spec-parser.d.ts.map +1 -0
  158. package/dist/src/utils/spec-parser.js +640 -0
  159. package/dist/src/utils/spec-parser.js.map +1 -0
  160. package/dist/tsconfig.tsbuildinfo +1 -0
  161. package/package.json +1 -1
  162. package/plugins/specweave/agents/pm/AGENT.md +1 -1
  163. package/plugins/specweave/agents/pm/templates/increment-spec.md +158 -0
  164. package/plugins/specweave/agents/pm/templates/living-docs-spec.md +113 -0
  165. package/plugins/specweave/commands/specweave-done.md +163 -0
  166. package/plugins/specweave/hooks/lib/update-status-line.sh +79 -111
  167. package/plugins/specweave/hooks/post-increment-planning.sh +107 -35
  168. package/plugins/specweave/lib/hooks/sync-living-docs.js +139 -34
  169. package/plugins/specweave/lib/hooks/sync-living-docs.ts +234 -38
  170. package/plugins/specweave/skills/SKILLS-INDEX.md +4 -24
  171. package/plugins/specweave/skills/increment-planner/SKILL.md +94 -0
  172. package/plugins/specweave/skills/increment-work-router/SKILL.md +466 -0
  173. package/plugins/specweave/skills/plugin-validator/SKILL.md +16 -13
  174. package/plugins/specweave-ado/lib/ado-status-sync.js +80 -0
  175. package/plugins/specweave-ado/lib/ado-status-sync.ts +121 -0
  176. package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
  177. package/plugins/specweave-github/commands/specweave-github-cleanup-duplicates.md +205 -0
  178. package/plugins/specweave-github/commands/specweave-github-sync-epic.md +248 -0
  179. package/plugins/specweave-github/lib/duplicate-detector.js +370 -0
  180. package/plugins/specweave-github/lib/duplicate-detector.ts +525 -0
  181. package/plugins/specweave-github/lib/enhanced-github-sync.js +220 -0
  182. package/plugins/specweave-github/lib/enhanced-github-sync.ts +322 -0
  183. package/plugins/specweave-github/lib/github-client.js +21 -10
  184. package/plugins/specweave-github/lib/github-client.ts +27 -16
  185. package/plugins/specweave-github/lib/github-epic-sync.js +489 -0
  186. package/plugins/specweave-github/lib/github-epic-sync.ts +690 -0
  187. package/plugins/specweave-github/lib/github-status-sync.js +71 -0
  188. package/plugins/specweave-github/lib/github-status-sync.ts +107 -0
  189. package/plugins/specweave-github/lib/task-sync.js +33 -2
  190. package/plugins/specweave-github/lib/task-sync.ts +44 -2
  191. package/plugins/specweave-jira/commands/specweave-jira-sync-epic.md +267 -0
  192. package/plugins/specweave-jira/lib/enhanced-jira-sync.ts.disabled +222 -0
  193. package/plugins/specweave-jira/lib/jira-epic-sync.js +304 -0
  194. package/plugins/specweave-jira/lib/jira-epic-sync.ts +459 -0
  195. package/plugins/specweave-jira/lib/jira-status-sync.js +79 -0
  196. package/plugins/specweave-jira/lib/jira-status-sync.ts +139 -0
  197. package/src/templates/AGENTS.md.template +88 -1
  198. package/src/templates/CLAUDE.md.template +49 -0
  199. package/plugins/specweave/skills/increment-quality-judge/SKILL.md +0 -524
  200. package/plugins/specweave/skills/plugin-installer/SKILL.md +0 -353
@@ -0,0 +1,220 @@
1
+ import { GitHubClientV2 } from "./github-client-v2.js";
2
+ import { EnhancedContentBuilder } from "../../../src/core/sync/enhanced-content-builder.js";
3
+ import { SpecIncrementMapper } from "../../../src/core/sync/spec-increment-mapper.js";
4
+ import { parseSpecContent } from "../../../src/core/spec-content-sync.js";
5
+ import { LabelDetector } from "../../../src/core/sync/label-detector.js";
6
+ import path from "path";
7
+ import fs from "fs/promises";
8
+ async function syncSpecWithEnhancedContent(options) {
9
+ const { specPath, owner, repo, dryRun = false, verbose = false } = options;
10
+ try {
11
+ const baseSpec = await parseSpecContent(specPath);
12
+ if (!baseSpec) {
13
+ return {
14
+ success: false,
15
+ action: "error",
16
+ error: "Failed to parse spec content"
17
+ };
18
+ }
19
+ if (verbose) {
20
+ console.log(`\u{1F4C4} Parsed spec: ${baseSpec.identifier.compact}`);
21
+ }
22
+ const specId = baseSpec.identifier.full || baseSpec.identifier.compact;
23
+ const rootDir = await findSpecWeaveRoot(specPath);
24
+ const mapper = new SpecIncrementMapper(rootDir);
25
+ const mapping = await mapper.mapSpecToIncrements(specId);
26
+ if (verbose) {
27
+ console.log(`\u{1F517} Found ${mapping.increments.length} related increments`);
28
+ console.log(`\u{1F4CB} Mapped ${Object.keys(mapping.userStoryMappings).length} user stories to tasks`);
29
+ }
30
+ const taskMapping = buildTaskMapping(mapping.increments, owner, repo);
31
+ const architectureDocs = await findArchitectureDocs(rootDir, specId);
32
+ const sourceLinks = buildSourceLinks(mapping.increments[0]?.id, owner, repo);
33
+ const enhancedSpec = {
34
+ ...baseSpec,
35
+ summary: baseSpec.description,
36
+ taskMapping,
37
+ architectureDocs,
38
+ sourceLinks
39
+ };
40
+ const builder = new EnhancedContentBuilder();
41
+ const originalBuildExternal = builder.buildExternalDescription.bind(builder);
42
+ const description = (() => {
43
+ const sections = [];
44
+ sections.push(builder.buildSummarySection(enhancedSpec));
45
+ if (enhancedSpec.userStories && enhancedSpec.userStories.length > 0) {
46
+ sections.push(builder.buildUserStoriesSection(enhancedSpec.userStories));
47
+ }
48
+ if (enhancedSpec.taskMapping) {
49
+ sections.push(builder.buildTasksSection(enhancedSpec.taskMapping, {
50
+ showCheckboxes: true,
51
+ showProgressBar: true,
52
+ showCompletionStatus: true,
53
+ provider: "github"
54
+ }));
55
+ }
56
+ if (enhancedSpec.architectureDocs && enhancedSpec.architectureDocs.length > 0) {
57
+ sections.push(builder.buildArchitectureSection(enhancedSpec.architectureDocs));
58
+ }
59
+ if (enhancedSpec.sourceLinks) {
60
+ sections.push(builder.buildSourceLinksSection(enhancedSpec.sourceLinks));
61
+ }
62
+ return sections.filter((s) => s.length > 0).join("\n\n---\n\n");
63
+ })();
64
+ if (verbose) {
65
+ console.log(`\u{1F4DD} Generated description: ${description.length} characters`);
66
+ }
67
+ if (dryRun) {
68
+ console.log("\u{1F50D} DRY RUN - Would create/update issue with:");
69
+ console.log(` Title: ${baseSpec.title}`);
70
+ console.log(` Description length: ${description.length}`);
71
+ console.log(` Tasks linked: ${taskMapping?.tasks.length || 0}`);
72
+ return {
73
+ success: true,
74
+ action: "no-change",
75
+ tasksLinked: taskMapping?.tasks.length || 0
76
+ };
77
+ }
78
+ if (!owner || !repo) {
79
+ return {
80
+ success: false,
81
+ action: "error",
82
+ error: "GitHub owner/repo not specified"
83
+ };
84
+ }
85
+ const client = GitHubClientV2.fromRepo(owner, repo);
86
+ const labelDetector = new LabelDetector(void 0, false);
87
+ const detection = labelDetector.detectType(
88
+ await fs.readFile(specPath, "utf-8"),
89
+ mapping.increments[0]?.id
90
+ );
91
+ const githubLabels = labelDetector.getGitHubLabels(detection.type);
92
+ const allLabels = ["spec", ...githubLabels];
93
+ if (verbose) {
94
+ console.log(`\u{1F3F7}\uFE0F Detected type: ${detection.type} (${detection.confidence}% confidence)`);
95
+ console.log(` Labels: ${allLabels.join(", ")}`);
96
+ }
97
+ const existingIssue = await findExistingIssue(client, baseSpec.identifier.compact);
98
+ let result;
99
+ if (existingIssue) {
100
+ await client.updateIssueBody(existingIssue.number, description);
101
+ await client.addLabels(existingIssue.number, allLabels);
102
+ result = {
103
+ success: true,
104
+ action: "updated",
105
+ issueNumber: existingIssue.number,
106
+ issueUrl: existingIssue.html_url,
107
+ tasksLinked: taskMapping?.tasks.length || 0
108
+ };
109
+ } else {
110
+ const issue = await client.createEpicIssue(
111
+ `[${baseSpec.identifier.compact}] ${baseSpec.title}`,
112
+ description,
113
+ void 0,
114
+ allLabels
115
+ // Apply labels at creation
116
+ );
117
+ result = {
118
+ success: true,
119
+ action: "created",
120
+ issueNumber: issue.number,
121
+ issueUrl: issue.html_url,
122
+ tasksLinked: taskMapping?.tasks.length || 0
123
+ };
124
+ await mapper.updateSpecWithIncrementLinks(specId, mapping.increments[0]?.id);
125
+ }
126
+ if (verbose) {
127
+ console.log(`\u2705 ${result.action === "created" ? "Created" : "Updated"} issue #${result.issueNumber}`);
128
+ console.log(` URL: ${result.issueUrl}`);
129
+ console.log(` Tasks linked: ${result.tasksLinked}`);
130
+ }
131
+ return result;
132
+ } catch (error) {
133
+ return {
134
+ success: false,
135
+ action: "error",
136
+ error: error.message
137
+ };
138
+ }
139
+ }
140
+ async function findSpecWeaveRoot(specPath) {
141
+ let currentDir = path.dirname(specPath);
142
+ while (true) {
143
+ const specweaveDir = path.join(currentDir, ".specweave");
144
+ try {
145
+ await fs.access(specweaveDir);
146
+ return currentDir;
147
+ } catch {
148
+ const parentDir = path.dirname(currentDir);
149
+ if (parentDir === currentDir) {
150
+ throw new Error(".specweave directory not found");
151
+ }
152
+ currentDir = parentDir;
153
+ }
154
+ }
155
+ }
156
+ function buildTaskMapping(increments, owner, repo) {
157
+ if (increments.length === 0) return void 0;
158
+ const firstIncrement = increments[0];
159
+ const tasks = firstIncrement.tasks.map((task) => ({
160
+ id: task.id,
161
+ title: task.title,
162
+ userStories: task.userStories,
163
+ githubIssue: task.githubIssue
164
+ }));
165
+ return {
166
+ incrementId: firstIncrement.id,
167
+ tasks,
168
+ tasksUrl: `https://github.com/${owner}/${repo}/blob/develop/.specweave/increments/${firstIncrement.id}/tasks.md`
169
+ };
170
+ }
171
+ async function findArchitectureDocs(rootDir, specId) {
172
+ const docs = [];
173
+ const archDir = path.join(rootDir, ".specweave/docs/internal/architecture");
174
+ try {
175
+ const adrDir = path.join(archDir, "adr");
176
+ try {
177
+ const adrs = await fs.readdir(adrDir);
178
+ const relatedAdrs = adrs.filter((file) => file.includes(specId.replace("spec-", "")));
179
+ for (const adr of relatedAdrs) {
180
+ docs.push({
181
+ type: "adr",
182
+ path: path.join(adrDir, adr),
183
+ title: adr.replace(".md", "").replace(/-/g, " ")
184
+ });
185
+ }
186
+ } catch {
187
+ }
188
+ const hlds = await fs.readdir(archDir);
189
+ const relatedHlds = hlds.filter((file) => file.includes("hld") && file.includes(specId.replace("spec-", "")));
190
+ for (const hld of relatedHlds) {
191
+ docs.push({
192
+ type: "hld",
193
+ path: path.join(archDir, hld),
194
+ title: hld.replace(".md", "").replace(/-/g, " ")
195
+ });
196
+ }
197
+ } catch {
198
+ }
199
+ return docs;
200
+ }
201
+ function buildSourceLinks(incrementId, owner, repo) {
202
+ if (!incrementId) return void 0;
203
+ const baseUrl = `https://github.com/${owner}/${repo}/blob/develop/.specweave`;
204
+ return {
205
+ spec: `${baseUrl}/docs/internal/specs/default/spec-${incrementId.replace(/^\d+-/, "")}.md`,
206
+ plan: `${baseUrl}/increments/${incrementId}/plan.md`,
207
+ tasks: `${baseUrl}/increments/${incrementId}/tasks.md`
208
+ };
209
+ }
210
+ async function findExistingIssue(client, specId) {
211
+ try {
212
+ const issues = await client.listIssuesInTimeRange("ALL");
213
+ return issues.find((issue) => issue.title.includes(`[${specId}]`) && issue.labels?.some((l) => l.name === "spec")) || null;
214
+ } catch {
215
+ return null;
216
+ }
217
+ }
218
+ export {
219
+ syncSpecWithEnhancedContent
220
+ };
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Enhanced GitHub Spec Content Sync
3
+ *
4
+ * Uses EnhancedContentBuilder and SpecIncrementMapper for rich external descriptions.
5
+ * NEW (v0.21.0): Supports task checkboxes and automatic labeling.
6
+ */
7
+
8
+ import { GitHubClientV2 } from './github-client-v2.js';
9
+ import { EnhancedContentBuilder, EnhancedSpecContent } from '../../../src/core/sync/enhanced-content-builder.js';
10
+ import { SpecIncrementMapper, TaskInfo } from '../../../src/core/sync/spec-increment-mapper.js';
11
+ import { parseSpecContent, SpecContent } from '../../../src/core/spec-content-sync.js';
12
+ import { LabelDetector } from '../../../src/core/sync/label-detector.js';
13
+ import path from 'path';
14
+ import fs from 'fs/promises';
15
+
16
+ export interface EnhancedGitHubSyncOptions {
17
+ specPath: string;
18
+ owner?: string;
19
+ repo?: string;
20
+ dryRun?: boolean;
21
+ verbose?: boolean;
22
+ }
23
+
24
+ export interface EnhancedSyncResult {
25
+ success: boolean;
26
+ action: 'created' | 'updated' | 'no-change' | 'error';
27
+ issueNumber?: number;
28
+ issueUrl?: string;
29
+ error?: string;
30
+ tasksLinked?: number;
31
+ }
32
+
33
+ /**
34
+ * Enhanced sync with rich content including task mappings
35
+ */
36
+ export async function syncSpecWithEnhancedContent(
37
+ options: EnhancedGitHubSyncOptions
38
+ ): Promise<EnhancedSyncResult> {
39
+ const { specPath, owner, repo, dryRun = false, verbose = false } = options;
40
+
41
+ try {
42
+ // 1. Parse spec content
43
+ const baseSpec = await parseSpecContent(specPath);
44
+ if (!baseSpec) {
45
+ return {
46
+ success: false,
47
+ action: 'error',
48
+ error: 'Failed to parse spec content',
49
+ };
50
+ }
51
+
52
+ if (verbose) {
53
+ console.log(`📄 Parsed spec: ${baseSpec.identifier.compact}`);
54
+ }
55
+
56
+ // 2. Build enhanced spec with task mappings
57
+ const specId = baseSpec.identifier.full || baseSpec.identifier.compact;
58
+ const rootDir = await findSpecWeaveRoot(specPath);
59
+ const mapper = new SpecIncrementMapper(rootDir);
60
+ const mapping = await mapper.mapSpecToIncrements(specId);
61
+
62
+ if (verbose) {
63
+ console.log(`🔗 Found ${mapping.increments.length} related increments`);
64
+ console.log(`📋 Mapped ${Object.keys(mapping.userStoryMappings).length} user stories to tasks`);
65
+ }
66
+
67
+ // 3. Build task mapping for EnhancedSpecContent
68
+ const taskMapping = buildTaskMapping(mapping.increments, owner!, repo!);
69
+
70
+ // 4. Build architecture docs references
71
+ const architectureDocs = await findArchitectureDocs(rootDir, specId);
72
+
73
+ // 5. Build source links
74
+ const sourceLinks = buildSourceLinks(mapping.increments[0]?.id, owner!, repo!);
75
+
76
+ // 6. Create enhanced spec content
77
+ const enhancedSpec: EnhancedSpecContent = {
78
+ ...baseSpec,
79
+ summary: baseSpec.description,
80
+ taskMapping,
81
+ architectureDocs,
82
+ sourceLinks
83
+ };
84
+
85
+ // 7. Build external description with task checkboxes
86
+ const builder = new EnhancedContentBuilder();
87
+
88
+ // NEW: Override buildTasksSection call to include checkboxes
89
+ const originalBuildExternal = builder.buildExternalDescription.bind(builder);
90
+ const description = (() => {
91
+ const sections: string[] = [];
92
+
93
+ // Summary
94
+ sections.push(builder.buildSummarySection(enhancedSpec));
95
+
96
+ // User stories
97
+ if (enhancedSpec.userStories && enhancedSpec.userStories.length > 0) {
98
+ sections.push(builder.buildUserStoriesSection(enhancedSpec.userStories));
99
+ }
100
+
101
+ // Tasks with checkboxes (NEW!)
102
+ if (enhancedSpec.taskMapping) {
103
+ sections.push(builder.buildTasksSection(enhancedSpec.taskMapping, {
104
+ showCheckboxes: true,
105
+ showProgressBar: true,
106
+ showCompletionStatus: true,
107
+ provider: 'github'
108
+ }));
109
+ }
110
+
111
+ // Architecture
112
+ if (enhancedSpec.architectureDocs && enhancedSpec.architectureDocs.length > 0) {
113
+ sections.push(builder.buildArchitectureSection(enhancedSpec.architectureDocs));
114
+ }
115
+
116
+ // Source links
117
+ if (enhancedSpec.sourceLinks) {
118
+ sections.push(builder.buildSourceLinksSection(enhancedSpec.sourceLinks));
119
+ }
120
+
121
+ return sections.filter(s => s.length > 0).join('\n\n---\n\n');
122
+ })();
123
+
124
+ if (verbose) {
125
+ console.log(`📝 Generated description: ${description.length} characters`);
126
+ }
127
+
128
+ if (dryRun) {
129
+ console.log('🔍 DRY RUN - Would create/update issue with:');
130
+ console.log(` Title: ${baseSpec.title}`);
131
+ console.log(` Description length: ${description.length}`);
132
+ console.log(` Tasks linked: ${taskMapping?.tasks.length || 0}`);
133
+ return {
134
+ success: true,
135
+ action: 'no-change',
136
+ tasksLinked: taskMapping?.tasks.length || 0
137
+ };
138
+ }
139
+
140
+ // 8. Create or update GitHub issue
141
+ if (!owner || !repo) {
142
+ return {
143
+ success: false,
144
+ action: 'error',
145
+ error: 'GitHub owner/repo not specified',
146
+ };
147
+ }
148
+
149
+ const client = GitHubClientV2.fromRepo(owner, repo);
150
+
151
+ // NEW: Detect increment type and apply labels
152
+ const labelDetector = new LabelDetector(undefined, false); // Use GitHub format
153
+ const detection = labelDetector.detectType(
154
+ await fs.readFile(specPath, 'utf-8'),
155
+ mapping.increments[0]?.id
156
+ );
157
+ const githubLabels = labelDetector.getGitHubLabels(detection.type);
158
+ const allLabels = ['spec', ...githubLabels]; // Include both 'spec' and type labels
159
+
160
+ if (verbose) {
161
+ console.log(`🏷️ Detected type: ${detection.type} (${detection.confidence}% confidence)`);
162
+ console.log(` Labels: ${allLabels.join(', ')}`);
163
+ }
164
+
165
+ // Check if issue already exists
166
+ const existingIssue = await findExistingIssue(client, baseSpec.identifier.compact);
167
+
168
+ let result: EnhancedSyncResult;
169
+
170
+ if (existingIssue) {
171
+ // Update existing issue (body + labels)
172
+ await client.updateIssueBody(existingIssue.number, description);
173
+
174
+ // Update labels if autoApplyLabels is enabled
175
+ // TODO: Read from config, for now always apply
176
+ await client.addLabels(existingIssue.number, allLabels);
177
+
178
+ result = {
179
+ success: true,
180
+ action: 'updated',
181
+ issueNumber: existingIssue.number,
182
+ issueUrl: existingIssue.html_url,
183
+ tasksLinked: taskMapping?.tasks.length || 0
184
+ };
185
+ } else {
186
+ // Create new issue with labels
187
+ const issue = await client.createEpicIssue(
188
+ `[${baseSpec.identifier.compact}] ${baseSpec.title}`,
189
+ description,
190
+ undefined,
191
+ allLabels // Apply labels at creation
192
+ );
193
+
194
+ result = {
195
+ success: true,
196
+ action: 'created',
197
+ issueNumber: issue.number,
198
+ issueUrl: issue.html_url,
199
+ tasksLinked: taskMapping?.tasks.length || 0
200
+ };
201
+
202
+ // Update spec with GitHub link
203
+ await mapper.updateSpecWithIncrementLinks(specId, mapping.increments[0]?.id);
204
+ }
205
+
206
+ if (verbose) {
207
+ console.log(`✅ ${result.action === 'created' ? 'Created' : 'Updated'} issue #${result.issueNumber}`);
208
+ console.log(` URL: ${result.issueUrl}`);
209
+ console.log(` Tasks linked: ${result.tasksLinked}`);
210
+ }
211
+
212
+ return result;
213
+ } catch (error: any) {
214
+ return {
215
+ success: false,
216
+ action: 'error',
217
+ error: error.message
218
+ };
219
+ }
220
+ }
221
+
222
+ // Helper functions
223
+
224
+ async function findSpecWeaveRoot(specPath: string): Promise<string> {
225
+ let currentDir = path.dirname(specPath);
226
+
227
+ while (true) {
228
+ const specweaveDir = path.join(currentDir, '.specweave');
229
+ try {
230
+ await fs.access(specweaveDir);
231
+ return currentDir;
232
+ } catch {
233
+ const parentDir = path.dirname(currentDir);
234
+ if (parentDir === currentDir) {
235
+ throw new Error('.specweave directory not found');
236
+ }
237
+ currentDir = parentDir;
238
+ }
239
+ }
240
+ }
241
+
242
+ function buildTaskMapping(
243
+ increments: any[],
244
+ owner: string,
245
+ repo: string
246
+ ): any {
247
+ if (increments.length === 0) return undefined;
248
+
249
+ const firstIncrement = increments[0];
250
+ const tasks = firstIncrement.tasks.map((task: TaskInfo) => ({
251
+ id: task.id,
252
+ title: task.title,
253
+ userStories: task.userStories,
254
+ githubIssue: task.githubIssue
255
+ }));
256
+
257
+ return {
258
+ incrementId: firstIncrement.id,
259
+ tasks,
260
+ tasksUrl: `https://github.com/${owner}/${repo}/blob/develop/.specweave/increments/${firstIncrement.id}/tasks.md`
261
+ };
262
+ }
263
+
264
+ async function findArchitectureDocs(
265
+ rootDir: string,
266
+ specId: string
267
+ ): Promise<any[]> {
268
+ const docs: any[] = [];
269
+ const archDir = path.join(rootDir, '.specweave/docs/internal/architecture');
270
+
271
+ try {
272
+ // Check for ADRs
273
+ const adrDir = path.join(archDir, 'adr');
274
+ try {
275
+ const adrs = await fs.readdir(adrDir);
276
+ const relatedAdrs = adrs.filter(file => file.includes(specId.replace('spec-', '')));
277
+
278
+ for (const adr of relatedAdrs) {
279
+ docs.push({
280
+ type: 'adr',
281
+ path: path.join(adrDir, adr),
282
+ title: adr.replace('.md', '').replace(/-/g, ' ')
283
+ });
284
+ }
285
+ } catch {}
286
+
287
+ // Check for HLD
288
+ const hlds = await fs.readdir(archDir);
289
+ const relatedHlds = hlds.filter(file => file.includes('hld') && file.includes(specId.replace('spec-', '')));
290
+
291
+ for (const hld of relatedHlds) {
292
+ docs.push({
293
+ type: 'hld',
294
+ path: path.join(archDir, hld),
295
+ title: hld.replace('.md', '').replace(/-/g, ' ')
296
+ });
297
+ }
298
+ } catch {}
299
+
300
+ return docs;
301
+ }
302
+
303
+ function buildSourceLinks(incrementId: string | undefined, owner: string, repo: string): any {
304
+ if (!incrementId) return undefined;
305
+
306
+ const baseUrl = `https://github.com/${owner}/${repo}/blob/develop/.specweave`;
307
+
308
+ return {
309
+ spec: `${baseUrl}/docs/internal/specs/default/spec-${incrementId.replace(/^\d+-/, '')}.md`,
310
+ plan: `${baseUrl}/increments/${incrementId}/plan.md`,
311
+ tasks: `${baseUrl}/increments/${incrementId}/tasks.md`
312
+ };
313
+ }
314
+
315
+ async function findExistingIssue(client: GitHubClientV2, specId: string): Promise<any | null> {
316
+ try {
317
+ const issues = await client.listIssuesInTimeRange('ALL');
318
+ return issues.find((issue: any) => issue.title.includes(`[${specId}]`) && issue.labels?.some((l: any) => l.name === 'spec')) || null;
319
+ } catch {
320
+ return null;
321
+ }
322
+ }
@@ -1,4 +1,5 @@
1
1
  import { execSync } from "child_process";
2
+ import { DuplicateDetector } from "./duplicate-detector.js";
2
3
  class GitHubClient {
3
4
  constructor(repo) {
4
5
  this.repo = repo || this.detectRepo();
@@ -70,21 +71,31 @@ class GitHubClient {
70
71
  }
71
72
  }
72
73
  /**
73
- * Create epic issue (increment-level)
74
+ * Create epic issue (increment-level) with FULL DUPLICATE PROTECTION
74
75
  */
75
76
  async createEpicIssue(title, body, milestone, labels = []) {
76
- const labelArgs = labels.map((l) => `-l "${l}"`).join(" ");
77
- const milestoneArg = milestone ? `-m "${milestone}"` : "";
78
- const createCmd = `gh issue create --repo ${this.repo} --title "${this.escapeQuotes(title)}" --body "${this.escapeQuotes(body)}" ${labelArgs} ${milestoneArg}`;
77
+ const titlePattern = DuplicateDetector.extractTitlePattern(title);
78
+ if (!titlePattern) {
79
+ throw new Error(`Epic issue title must start with pattern like [FS-XXX] or [INC-XXXX]: ${title}`);
80
+ }
79
81
  try {
80
- const issueUrl = execSync(createCmd, { encoding: "utf-8" }).trim();
81
- const issueNumber = parseInt(issueUrl.split("/").pop() || "0", 10);
82
- if (!issueNumber) {
83
- throw new Error("Failed to extract issue number from URL: " + issueUrl);
84
- }
85
- const viewCmd = `gh issue view ${issueNumber} --repo ${this.repo} --json number,title,body,state,url,labels,milestone`;
82
+ const result = await DuplicateDetector.createWithProtection({
83
+ title,
84
+ body,
85
+ titlePattern,
86
+ labels: labels.length > 0 ? labels : ["specweave", "increment"],
87
+ milestone: milestone?.toString(),
88
+ repo: this.repo
89
+ });
90
+ const viewCmd = `gh issue view ${result.issue.number} --repo ${this.repo} --json number,title,body,state,url,labels,milestone`;
86
91
  const output = execSync(viewCmd, { encoding: "utf-8" });
87
92
  const issue = JSON.parse(output);
93
+ if (result.wasReused) {
94
+ console.log(` \u267B\uFE0F Reused existing issue #${result.issue.number} (duplicate prevention)`);
95
+ }
96
+ if (result.duplicatesFound > 0) {
97
+ console.log(` \u{1F6E1}\uFE0F Duplicates detected: ${result.duplicatesFound} (auto-closed: ${result.duplicatesClosed})`);
98
+ }
88
99
  return {
89
100
  ...issue,
90
101
  html_url: issue.url,
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { execSync } from 'child_process';
7
7
  import { GitHubIssue, GitHubMilestone } from './types';
8
+ import { DuplicateDetector } from './duplicate-detector.js';
8
9
 
9
10
  export class GitHubClient {
10
11
  private repo: string;
@@ -96,7 +97,7 @@ export class GitHubClient {
96
97
  }
97
98
 
98
99
  /**
99
- * Create epic issue (increment-level)
100
+ * Create epic issue (increment-level) with FULL DUPLICATE PROTECTION
100
101
  */
101
102
  async createEpicIssue(
102
103
  title: string,
@@ -104,27 +105,37 @@ export class GitHubClient {
104
105
  milestone?: number | string,
105
106
  labels: string[] = []
106
107
  ): Promise<GitHubIssue> {
107
- const labelArgs = labels.map(l => `-l "${l}"`).join(' ');
108
- const milestoneArg = milestone ? `-m "${milestone}"` : '';
108
+ // Extract title pattern for duplicate detection (e.g., "[FS-031]" from "[FS-031] Feature Title")
109
+ const titlePattern = DuplicateDetector.extractTitlePattern(title);
109
110
 
110
- // Create issue (returns URL)
111
- const createCmd = `gh issue create --repo ${this.repo} --title "${this.escapeQuotes(title)}" --body "${this.escapeQuotes(body)}" ${labelArgs} ${milestoneArg}`;
111
+ if (!titlePattern) {
112
+ throw new Error(`Epic issue title must start with pattern like [FS-XXX] or [INC-XXXX]: ${title}`);
113
+ }
112
114
 
113
115
  try {
114
- const issueUrl = execSync(createCmd, { encoding: 'utf-8' }).trim();
115
-
116
- // Extract issue number from URL (e.g., https://github.com/owner/repo/issues/123)
117
- const issueNumber = parseInt(issueUrl.split('/').pop() || '0', 10);
118
-
119
- if (!issueNumber) {
120
- throw new Error('Failed to extract issue number from URL: ' + issueUrl);
121
- }
122
-
123
- // Fetch issue details
124
- const viewCmd = `gh issue view ${issueNumber} --repo ${this.repo} --json number,title,body,state,url,labels,milestone`;
116
+ // Use DuplicateDetector for full 3-phase protection
117
+ const result = await DuplicateDetector.createWithProtection({
118
+ title,
119
+ body,
120
+ titlePattern,
121
+ labels: labels.length > 0 ? labels : ['specweave', 'increment'],
122
+ milestone: milestone?.toString(),
123
+ repo: this.repo
124
+ });
125
+
126
+ // Fetch full issue details (DuplicateDetector returns minimal info)
127
+ const viewCmd = `gh issue view ${result.issue.number} --repo ${this.repo} --json number,title,body,state,url,labels,milestone`;
125
128
  const output = execSync(viewCmd, { encoding: 'utf-8' });
126
129
  const issue = JSON.parse(output);
127
130
 
131
+ // Log duplicate detection results if any
132
+ if (result.wasReused) {
133
+ console.log(` ♻️ Reused existing issue #${result.issue.number} (duplicate prevention)`);
134
+ }
135
+ if (result.duplicatesFound > 0) {
136
+ console.log(` 🛡️ Duplicates detected: ${result.duplicatesFound} (auto-closed: ${result.duplicatesClosed})`);
137
+ }
138
+
128
139
  return {
129
140
  ...issue,
130
141
  html_url: issue.url,