specweave 1.0.301 → 1.0.304

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 (74) hide show
  1. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.d.ts.map +1 -1
  2. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js +44 -25
  3. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js.map +1 -1
  4. package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js +6 -0
  5. package/dist/plugins/specweave-github/lib/github-feature-sync-cli.js.map +1 -1
  6. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +36 -1
  7. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
  8. package/dist/plugins/specweave-github/lib/github-feature-sync.js +266 -5
  9. package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
  10. package/dist/plugins/specweave-github/lib/user-story-content-builder.d.ts +2 -1
  11. package/dist/plugins/specweave-github/lib/user-story-content-builder.d.ts.map +1 -1
  12. package/dist/plugins/specweave-github/lib/user-story-content-builder.js +6 -4
  13. package/dist/plugins/specweave-github/lib/user-story-content-builder.js.map +1 -1
  14. package/dist/plugins/specweave-github/lib/user-story-issue-builder.d.ts.map +1 -1
  15. package/dist/plugins/specweave-github/lib/user-story-issue-builder.js +37 -17
  16. package/dist/plugins/specweave-github/lib/user-story-issue-builder.js.map +1 -1
  17. package/dist/src/cli/commands/refresh-plugins.d.ts.map +1 -1
  18. package/dist/src/cli/commands/refresh-plugins.js +9 -0
  19. package/dist/src/cli/commands/refresh-plugins.js.map +1 -1
  20. package/dist/src/cli/commands/sync-progress.d.ts.map +1 -1
  21. package/dist/src/cli/commands/sync-progress.js +72 -2
  22. package/dist/src/cli/commands/sync-progress.js.map +1 -1
  23. package/dist/src/config/types.d.ts +2 -2
  24. package/dist/src/core/increment/increment-utils.d.ts +27 -4
  25. package/dist/src/core/increment/increment-utils.d.ts.map +1 -1
  26. package/dist/src/core/increment/increment-utils.js +44 -17
  27. package/dist/src/core/increment/increment-utils.js.map +1 -1
  28. package/dist/src/core/increment/template-creator.d.ts +26 -0
  29. package/dist/src/core/increment/template-creator.d.ts.map +1 -1
  30. package/dist/src/core/increment/template-creator.js +179 -20
  31. package/dist/src/core/increment/template-creator.js.map +1 -1
  32. package/dist/src/importers/import-to-increment.d.ts +111 -0
  33. package/dist/src/importers/import-to-increment.d.ts.map +1 -0
  34. package/dist/src/importers/import-to-increment.js +223 -0
  35. package/dist/src/importers/import-to-increment.js.map +1 -0
  36. package/dist/src/importers/increment-external-ref-detector.d.ts +78 -0
  37. package/dist/src/importers/increment-external-ref-detector.d.ts.map +1 -0
  38. package/dist/src/importers/increment-external-ref-detector.js +130 -0
  39. package/dist/src/importers/increment-external-ref-detector.js.map +1 -0
  40. package/dist/src/init/research/types.d.ts +1 -1
  41. package/dist/src/sync/external-issue-auto-creator.d.ts.map +1 -1
  42. package/dist/src/sync/external-issue-auto-creator.js +28 -1
  43. package/dist/src/sync/external-issue-auto-creator.js.map +1 -1
  44. package/dist/src/sync/sync-coordinator.d.ts +6 -0
  45. package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
  46. package/dist/src/sync/sync-coordinator.js +42 -2
  47. package/dist/src/sync/sync-coordinator.js.map +1 -1
  48. package/package.json +1 -1
  49. package/plugins/specweave/hooks/lib/update-active-increment.sh +2 -2
  50. package/plugins/specweave/hooks/lib/update-status-line.sh +2 -2
  51. package/plugins/specweave/hooks/stop-auto-v5.sh +28 -8
  52. package/plugins/specweave/hooks/stop-sync.sh +10 -5
  53. package/plugins/specweave/hooks/universal/fail-fast-wrapper.sh +8 -4
  54. package/plugins/specweave/hooks/user-prompt-submit.sh +130 -112
  55. package/plugins/specweave/hooks/v2/handlers/github-sync-handler.sh +6 -3
  56. package/plugins/specweave/hooks/v2/handlers/project-bridge-handler.sh +4 -3
  57. package/plugins/specweave/skills/auto/SKILL.md +5 -3
  58. package/plugins/specweave/skills/done/SKILL.md +9 -3
  59. package/plugins/specweave/skills/import/SKILL.md +186 -0
  60. package/plugins/specweave/skills/increment/SKILL.md +30 -16
  61. package/plugins/specweave/skills/pm/SKILL.md +29 -2
  62. package/plugins/specweave/skills/pm/phases/00-deep-interview.md +12 -0
  63. package/plugins/specweave/skills/team-lead/SKILL.md +4 -2
  64. package/plugins/specweave/skills/team-merge/SKILL.md +2 -2
  65. package/plugins/specweave-github/lib/github-ac-comment-poster.js +31 -19
  66. package/plugins/specweave-github/lib/github-ac-comment-poster.ts +44 -27
  67. package/plugins/specweave-github/lib/github-feature-sync-cli.js +5 -0
  68. package/plugins/specweave-github/lib/github-feature-sync-cli.ts +7 -1
  69. package/plugins/specweave-github/lib/github-feature-sync.js +274 -6
  70. package/plugins/specweave-github/lib/github-feature-sync.ts +353 -5
  71. package/plugins/specweave-github/lib/user-story-content-builder.js +6 -4
  72. package/plugins/specweave-github/lib/user-story-content-builder.ts +6 -4
  73. package/plugins/specweave-github/lib/user-story-issue-builder.js +26 -11
  74. package/plugins/specweave-github/lib/user-story-issue-builder.ts +37 -19
@@ -1,4 +1,4 @@
1
- import { readdir, readFile, writeFile } from "fs/promises";
1
+ import { readdir, readFile, writeFile, mkdir } from "fs/promises";
2
2
  import { existsSync } from "fs";
3
3
  import * as path from "path";
4
4
  import * as yaml from "yaml";
@@ -10,12 +10,39 @@ import { getGitHubAuthFromProject } from "../../../src/utils/auth-helpers.js";
10
10
  const _GitHubFeatureSync = class _GitHubFeatureSync {
11
11
  // 30 seconds
12
12
  constructor(client, specsDir, projectRoot) {
13
+ // Cached default branch for the sync session (one API call per session)
14
+ this.defaultBranch = null;
13
15
  this.client = client;
14
16
  this.specsDir = specsDir;
15
17
  this.projectRoot = projectRoot;
16
18
  this.calculator = new CompletionCalculator(projectRoot);
17
19
  this.token = getGitHubAuthFromProject(projectRoot).token;
18
20
  }
21
+ /**
22
+ * Detect the default branch from the GitHub API.
23
+ * Caches the result per sync session to avoid repeated API calls.
24
+ * Falls back to 'main' if API call fails.
25
+ */
26
+ async detectDefaultBranch() {
27
+ if (this.defaultBranch) {
28
+ return this.defaultBranch;
29
+ }
30
+ const owner = this.client.getOwner();
31
+ const repo = this.client.getRepo();
32
+ const result = await execFileNoThrow("gh", [
33
+ "api",
34
+ `repos/${owner}/${repo}`,
35
+ "--jq",
36
+ ".default_branch"
37
+ ], { env: this.getGhEnv() });
38
+ if (result.exitCode === 0 && result.stdout.trim()) {
39
+ this.defaultBranch = result.stdout.trim();
40
+ } else {
41
+ console.warn(` \u26A0\uFE0F Failed to detect default branch, falling back to 'main': ${result.stderr}`);
42
+ this.defaultBranch = "main";
43
+ }
44
+ return this.defaultBranch;
45
+ }
19
46
  /**
20
47
  * Get environment object with GH_TOKEN for gh CLI commands.
21
48
  * This ensures the token from .env is passed to all gh operations,
@@ -83,14 +110,15 @@ const _GitHubFeatureSync = class _GitHubFeatureSync {
83
110
  \u{1F4DD} Found ${userStories.length} User Stories to sync...`);
84
111
  let issuesCreated = 0;
85
112
  let issuesUpdated = 0;
113
+ const detectedBranch = await this.detectDefaultBranch();
114
+ console.log(` \u{1F33F} Default branch: ${detectedBranch}`);
86
115
  for (const userStory of userStories) {
87
116
  console.log(`
88
117
  \u{1F539} Processing ${userStory.id}: ${userStory.title}`);
89
118
  const repoInfo = {
90
119
  owner: this.client.getOwner(),
91
120
  repo: this.client.getRepo(),
92
- branch: "develop"
93
- // TODO: detect from git
121
+ branch: detectedBranch
94
122
  };
95
123
  const builder = new UserStoryIssueBuilder(
96
124
  userStory.filePath,
@@ -160,7 +188,8 @@ const _GitHubFeatureSync = class _GitHubFeatureSync {
160
188
  };
161
189
  }
162
190
  /**
163
- * Find Feature folder in specs directory
191
+ * Find Feature folder in specs directory.
192
+ * Falls back to auto-creating from increment spec.md if living docs don't exist.
164
193
  */
165
194
  async findFeatureFolder(featureId) {
166
195
  const projectFolders = await this.findProjectFolders();
@@ -175,8 +204,230 @@ const _GitHubFeatureSync = class _GitHubFeatureSync {
175
204
  console.log(` \u26A0\uFE0F Found feature in legacy _features folder - consider migrating to project folder`);
176
205
  return legacyFolder;
177
206
  }
207
+ console.log(` \u2139\uFE0F Feature folder not found in living docs, attempting auto-create from spec.md...`);
208
+ const created = await this.createFeatureFolderFromSpec(featureId, projectFolders);
209
+ if (created) {
210
+ return created;
211
+ }
178
212
  return null;
179
213
  }
214
+ /**
215
+ * Find the increment folder for a given feature ID.
216
+ * Converts FS-271 -> finds 0271-xxx-xxx/ in .specweave/increments/
217
+ */
218
+ async findIncrementFolder(featureId) {
219
+ const numMatch = featureId.match(/FS-0*(\d+)E?/i);
220
+ if (!numMatch) return null;
221
+ const num = parseInt(numMatch[1], 10);
222
+ const paddedNum = String(num).padStart(4, "0");
223
+ const incrementsDir = path.join(this.projectRoot, ".specweave/increments");
224
+ if (!existsSync(incrementsDir)) return null;
225
+ const entries = await readdir(incrementsDir);
226
+ const match = entries.find((e) => e.startsWith(paddedNum + "-"));
227
+ if (!match) return null;
228
+ return path.join(incrementsDir, match);
229
+ }
230
+ /**
231
+ * Auto-create a feature folder (FEATURE.md + us-NNN.md files) from an
232
+ * increment's spec.md. This enables GitHub sync even when the living docs
233
+ * builder hasn't run yet.
234
+ */
235
+ async createFeatureFolderFromSpec(featureId, projectFolders) {
236
+ try {
237
+ const incrementFolder = await this.findIncrementFolder(featureId);
238
+ if (!incrementFolder) {
239
+ console.log(` \u26A0\uFE0F No increment folder found for ${featureId}`);
240
+ return null;
241
+ }
242
+ const specPath = path.join(incrementFolder, "spec.md");
243
+ if (!existsSync(specPath)) {
244
+ console.log(` \u26A0\uFE0F No spec.md found in ${path.basename(incrementFolder)}`);
245
+ return null;
246
+ }
247
+ const specContent = await readFile(specPath, "utf-8");
248
+ const fmMatch = specContent.match(/^---\n([\s\S]*?)\n---/);
249
+ if (!fmMatch) {
250
+ console.log(` \u26A0\uFE0F spec.md has no YAML frontmatter`);
251
+ return null;
252
+ }
253
+ const frontmatter = yaml.parse(fmMatch[1]);
254
+ const title = frontmatter.title || path.basename(incrementFolder).replace(/^\d+-/, "");
255
+ const status = frontmatter.status || "active";
256
+ const priority = frontmatter.priority || "P2";
257
+ const created = frontmatter.created || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
258
+ const incrementId = frontmatter.increment || path.basename(incrementFolder);
259
+ let targetProjectFolder = projectFolders[0];
260
+ const projectMatch = specContent.match(/\*\*Project\*\*:\s*(\S+)/);
261
+ if (projectMatch) {
262
+ const projectName = projectMatch[1];
263
+ const matchingFolder = projectFolders.find((f) => path.basename(f) === projectName);
264
+ if (matchingFolder) {
265
+ targetProjectFolder = matchingFolder;
266
+ }
267
+ }
268
+ if (!targetProjectFolder) {
269
+ console.log(` \u26A0\uFE0F No project folder available for feature creation`);
270
+ return null;
271
+ }
272
+ const featureFolder = path.join(targetProjectFolder, featureId);
273
+ await mkdir(featureFolder, { recursive: true });
274
+ const userStories = this.parseUserStoriesFromSpec(specContent, featureId);
275
+ const featureMd = this.buildFeatureMd(featureId, title, status, priority, created, incrementId, userStories);
276
+ await writeFile(path.join(featureFolder, "FEATURE.md"), featureMd, "utf-8");
277
+ for (const us of userStories) {
278
+ const usFilename = `us-${us.id.replace("US-", "").padStart(3, "0")}-${this.slugify(us.title)}.md`;
279
+ const usMd = this.buildUserStoryMd(us, featureId, incrementId);
280
+ await writeFile(path.join(featureFolder, usFilename), usMd, "utf-8");
281
+ }
282
+ console.log(` \u2705 Auto-created feature folder with ${userStories.length} user stories`);
283
+ return featureFolder;
284
+ } catch (error) {
285
+ console.log(` \u26A0\uFE0F Failed to auto-create feature folder: ${error.message}`);
286
+ return null;
287
+ }
288
+ }
289
+ /**
290
+ * Parse user stories from spec.md markdown content.
291
+ */
292
+ parseUserStoriesFromSpec(specContent, featureId) {
293
+ const stories = [];
294
+ const usRegex = /### (US-\d+):\s*(.+?)(?:\s*\((P\d)\))?\s*\n([\s\S]*?)(?=\n### US-|\n## |\n---\s*\n### US-|$)/g;
295
+ let match;
296
+ while ((match = usRegex.exec(specContent)) !== null) {
297
+ const usId = match[1];
298
+ const rawTitle = match[2].trim();
299
+ const priority = match[3] || "P2";
300
+ const body = match[4];
301
+ if (rawTitle === "[Story Title]") continue;
302
+ const projectMatch = body.match(/\*\*Project\*\*:\s*(\S+)/);
303
+ const project = projectMatch ? projectMatch[1] : "specweave";
304
+ const storyMatch = body.match(/\*\*As a\*\*\s+([\s\S]*?)(?=\n\*\*Acceptance Criteria|$)/);
305
+ const storyText = storyMatch ? storyMatch[1].trim() : "";
306
+ const acs = [];
307
+ const acRegex = /- \[[ x]\] \*\*AC-[^*]+\*\*:\s*(.+)/g;
308
+ let acMatch;
309
+ while ((acMatch = acRegex.exec(body)) !== null) {
310
+ acs.push(acMatch[0]);
311
+ }
312
+ const totalAcs = acs.length;
313
+ const completedAcs = acs.filter((ac) => ac.startsWith("- [x]")).length;
314
+ let status = "not-started";
315
+ if (totalAcs > 0 && completedAcs === totalAcs) status = "complete";
316
+ else if (completedAcs > 0) status = "active";
317
+ stories.push({
318
+ id: usId,
319
+ title: rawTitle,
320
+ priority,
321
+ project,
322
+ storyText,
323
+ acceptanceCriteria: acs,
324
+ status
325
+ });
326
+ }
327
+ return stories;
328
+ }
329
+ /**
330
+ * Build FEATURE.md content matching the living docs format.
331
+ */
332
+ buildFeatureMd(featureId, title, status, priority, created, incrementId, userStories) {
333
+ const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
334
+ const mappedStatus = status === "planned" ? "planning" : status === "completed" || status === "done" ? "complete" : status === "active" || status === "in-progress" ? "active" : "planning";
335
+ const fm = {
336
+ id: featureId,
337
+ title,
338
+ type: "feature",
339
+ status: mappedStatus,
340
+ priority,
341
+ created,
342
+ lastUpdated: now,
343
+ tldr: title,
344
+ complexity: "medium",
345
+ auto_created: true
346
+ };
347
+ const yamlFm = yaml.stringify(fm);
348
+ let body = `
349
+ # ${title}
350
+
351
+ ## TL;DR
352
+
353
+ **What**: ${title}
354
+ **Status**: ${mappedStatus} | **Priority**: ${priority}
355
+ **User Stories**: ${userStories.length}
356
+
357
+ ## Overview
358
+
359
+ ${title}
360
+
361
+ ## Implementation History
362
+
363
+ | Increment | Status |
364
+ |-----------|--------|
365
+ | [${incrementId}](../../../../../increments/${incrementId}/spec.md) | ${mappedStatus} |
366
+
367
+ ## User Stories
368
+ `;
369
+ for (const us of userStories) {
370
+ body += `
371
+ - [${us.id}: ${us.title}](./${us.id.toLowerCase()}.md)`;
372
+ }
373
+ return `---
374
+ ${yamlFm}---${body}
375
+ `;
376
+ }
377
+ /**
378
+ * Build us-NNN.md content matching the living docs format.
379
+ */
380
+ buildUserStoryMd(us, featureId, incrementId) {
381
+ const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
382
+ const fm = {
383
+ id: us.id,
384
+ feature: featureId,
385
+ title: us.title,
386
+ status: us.status,
387
+ priority: us.priority,
388
+ created: now,
389
+ project: us.project
390
+ };
391
+ const yamlFm = yaml.stringify(fm);
392
+ let body = `
393
+ # ${us.id}: ${us.title}
394
+
395
+ **Feature**: [${featureId}](./FEATURE.md)
396
+
397
+ `;
398
+ if (us.storyText) {
399
+ body += `${us.storyText}
400
+
401
+ `;
402
+ }
403
+ body += `---
404
+
405
+ ## Acceptance Criteria
406
+
407
+ `;
408
+ if (us.acceptanceCriteria.length > 0) {
409
+ body += us.acceptanceCriteria.join("\n") + "\n";
410
+ } else {
411
+ body += `- [ ] **AC-${us.id.replace("US-", "US")}-01**: Pending specification
412
+ `;
413
+ }
414
+ body += `
415
+ ---
416
+
417
+ ## Implementation
418
+
419
+ **Increment**: [${incrementId}](../../../../../increments/${incrementId}/spec.md)
420
+ `;
421
+ return `---
422
+ ${yamlFm}---${body}
423
+ `;
424
+ }
425
+ /**
426
+ * Convert a title to a URL-safe slug.
427
+ */
428
+ slugify(text) {
429
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").substring(0, 60);
430
+ }
180
431
  /**
181
432
  * Backfill increment metadata.json with GitHub issue reference (v1.0.240)
182
433
  *
@@ -282,9 +533,11 @@ const _GitHubFeatureSync = class _GitHubFeatureSync {
282
533
  */
283
534
  async createMilestone(featureData) {
284
535
  const title = `${featureData.id}: ${featureData.title}`;
536
+ const owner = this.client.getOwner();
537
+ const repo = this.client.getRepo();
285
538
  const existingResult = await execFileNoThrow("gh", [
286
539
  "api",
287
- "repos/:owner/:repo/milestones?per_page=100&state=all",
540
+ `repos/${owner}/${repo}/milestones?per_page=100&state=all`,
288
541
  "--jq",
289
542
  `.[] | select(.title == "${title}") | {number, html_url}`
290
543
  ], { env: this.getGhEnv() });
@@ -307,7 +560,7 @@ Status: ${featureData.status}
307
560
  Created: ${featureData.created}`;
308
561
  const result = await execFileNoThrow("gh", [
309
562
  "api",
310
- "repos/:owner/:repo/milestones",
563
+ `repos/${owner}/${repo}/milestones`,
311
564
  "-X",
312
565
  "POST",
313
566
  "-f",
@@ -490,6 +743,21 @@ Created: ${featureData.created}`;
490
743
  } else {
491
744
  console.warn(` \u26A0\uFE0F Failed to add label ${newStatusLabel}: ${result.stderr}`);
492
745
  }
746
+ if (newStatusLabel === "status:complete" && issueData.state.toLowerCase() !== "closed") {
747
+ try {
748
+ const completionComment = this.calculator.buildCompletionComment(completion);
749
+ await execFileNoThrow("gh", [
750
+ "issue",
751
+ "close",
752
+ issueNumber.toString(),
753
+ "--comment",
754
+ completionComment
755
+ ], { env: this.getGhEnv() });
756
+ console.log(` \u2705 Auto-closed issue #${issueNumber} (status:complete)`);
757
+ } catch (closeError) {
758
+ console.warn(` \u26A0\uFE0F Failed to auto-close issue #${issueNumber}: ${closeError.message}`);
759
+ }
760
+ }
493
761
  } catch (error) {
494
762
  console.warn(` \u26A0\uFE0F Failed to update status labels: ${error.message}`);
495
763
  }