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
@@ -1,26 +1,18 @@
1
1
  #!/usr/bin/env bash
2
2
  #
3
- # update-status-line.sh
3
+ # update-status-line.sh (Simplified)
4
4
  #
5
- # Updates the status line cache with current increment progress.
6
- # Called by post-task-completion hook.
5
+ # Updates status line cache with current increment progress.
6
+ # Shows: [increment-name] ████░░░░ X/Y tasks (Z open)
7
7
  #
8
- # Performance: 10-50ms (runs async in hook, user doesn't wait)
8
+ # Logic:
9
+ # 1. Scan all metadata.json for status=active/in-progress/planning
10
+ # 2. Take first (oldest) as current increment
11
+ # 3. Count all active/in-progress/planning as openCount
12
+ # 4. Parse current increment's tasks.md for progress
13
+ # 5. Write to cache
9
14
  #
10
- # Cache format (.specweave/state/status-line.json):
11
- # {
12
- # "incrementId": "0017-sync-architecture-fix",
13
- # "incrementName": "sync-architecture-fix",
14
- # "totalTasks": 30,
15
- # "completedTasks": 15,
16
- # "percentage": 50,
17
- # "currentTask": {
18
- # "id": "T-016",
19
- # "title": "Update documentation"
20
- # },
21
- # "lastUpdate": "2025-11-10T15:30:00Z",
22
- # "lastModified": 1699632600
23
- # }
15
+ # Performance: 50-100ms (runs async, user doesn't wait)
24
16
 
25
17
  set -euo pipefail
26
18
 
@@ -39,126 +31,102 @@ find_project_root() {
39
31
 
40
32
  PROJECT_ROOT=$(find_project_root)
41
33
  CACHE_FILE="$PROJECT_ROOT/.specweave/state/status-line.json"
42
- STATE_FILE="$PROJECT_ROOT/.specweave/state/active-increment.json"
34
+ INCREMENTS_DIR="$PROJECT_ROOT/.specweave/increments"
35
+ TMP_FILE="$PROJECT_ROOT/.specweave/state/.status-line-tmp.txt"
43
36
 
44
37
  # Ensure state directory exists
45
38
  mkdir -p "$PROJECT_ROOT/.specweave/state"
46
39
 
47
- # Check if there's an active increment
48
- if [[ ! -f "$STATE_FILE" ]]; then
49
- # No active increment = clear cache
50
- echo '{}' > "$CACHE_FILE"
51
- exit 0
52
- fi
40
+ # Step 1: Find all open increments (active/in-progress/planning)
41
+ # Write to temp file: "timestamp increment_id"
42
+ > "$TMP_FILE"
53
43
 
54
- # Get active increment ID
55
- INCREMENT_ID=$(jq -r '.id // empty' "$STATE_FILE" 2>/dev/null || echo "")
56
- if [[ -z "$INCREMENT_ID" ]]; then
57
- echo '{}' > "$CACHE_FILE"
58
- exit 0
44
+ if [[ -d "$INCREMENTS_DIR" ]]; then
45
+ for metadata in "$INCREMENTS_DIR"/*/metadata.json; do
46
+ if [[ -f "$metadata" ]]; then
47
+ status=$(jq -r '.status // ""' "$metadata" 2>/dev/null || echo "")
48
+
49
+ # Check if increment is open (active, in-progress, or planning)
50
+ if [[ "$status" == "active" ]] || [[ "$status" == "in-progress" ]] || [[ "$status" == "planning" ]]; then
51
+ increment_id=$(basename "$(dirname "$metadata")")
52
+ created=$(jq -r '.created // ""' "$metadata" 2>/dev/null || echo "1970-01-01T00:00:00Z")
53
+
54
+ # Write to temp file
55
+ echo "$created $increment_id" >> "$TMP_FILE"
56
+ fi
57
+ fi
58
+ done
59
59
  fi
60
60
 
61
- TASKS_FILE="$PROJECT_ROOT/.specweave/increments/$INCREMENT_ID/tasks.md"
61
+ # Step 2: Count open increments
62
+ OPEN_COUNT=$(wc -l < "$TMP_FILE" | tr -d ' ')
62
63
 
63
- # No tasks file yet? (Planning phase)
64
- if [[ ! -f "$TASKS_FILE" ]]; then
65
- echo '{}' > "$CACHE_FILE"
64
+ if [[ $OPEN_COUNT -eq 0 ]]; then
65
+ # No open increments
66
+ jq -n '{
67
+ current: null,
68
+ openCount: 0,
69
+ lastUpdate: (now | strftime("%Y-%m-%dT%H:%M:%SZ"))
70
+ }' > "$CACHE_FILE"
71
+ rm -f "$TMP_FILE"
66
72
  exit 0
67
73
  fi
68
74
 
69
- # Get tasks.md mtime for invalidation detection
70
- if [[ "$OSTYPE" == "darwin"* ]]; then
71
- # macOS
72
- MTIME=$(stat -f %m "$TASKS_FILE" 2>/dev/null || echo 0)
73
- else
74
- # Linux
75
- MTIME=$(stat -c %Y "$TASKS_FILE" 2>/dev/null || echo 0)
76
- fi
75
+ # Step 3: Sort by timestamp (oldest first) and take first
76
+ CURRENT_INCREMENT=$(sort "$TMP_FILE" | head -1 | awk '{print $2}')
77
77
 
78
- # Parse tasks.md (THIS is the slow part: 10-50ms)
79
- # Support both ## T- and ### T- formats (flexible task heading levels)
80
- TOTAL_TASKS=$(grep -cE '^##+ T-' "$TASKS_FILE" 2>/dev/null || echo 0)
78
+ # Clean up temp file
79
+ rm -f "$TMP_FILE"
81
80
 
82
- # Remove any whitespace/newlines and ensure integer
83
- TOTAL_TASKS=$(echo "$TOTAL_TASKS" | tr -d '\n\r ' | grep -E '^[0-9]+$' || echo 0)
81
+ # Step 4: Parse current increment's tasks.md for progress
82
+ TASKS_FILE="$INCREMENTS_DIR/$CURRENT_INCREMENT/tasks.md"
83
+ TOTAL_TASKS=0
84
+ COMPLETED_TASKS=0
85
+ PERCENTAGE=0
84
86
 
85
- # Support both checkbox formats:
86
- # 1. Standard: [x] at line start
87
- # 2. Inline: **Status**: [x] (in task body)
88
- COMPLETED_TASKS_STANDARD=$(grep -c '^\[x\]' "$TASKS_FILE" 2>/dev/null || echo 0)
89
- COMPLETED_TASKS_INLINE=$(grep -c 'Status\*\*: \[x\]' "$TASKS_FILE" 2>/dev/null || echo 0)
87
+ if [[ -f "$TASKS_FILE" ]]; then
88
+ # Count total tasks (## T- or ### T- headings)
89
+ TOTAL_TASKS=$(grep -cE '^##+ T-' "$TASKS_FILE" 2>/dev/null || echo 0)
90
+ TOTAL_TASKS=$(echo "$TOTAL_TASKS" | tr -d '\n\r ' || echo 0)
90
91
 
91
- # Remove any whitespace/newlines and ensure integer
92
- COMPLETED_TASKS_STANDARD=$(echo "$COMPLETED_TASKS_STANDARD" | tr -d '\n\r ' | grep -E '^[0-9]+$' || echo 0)
93
- COMPLETED_TASKS_INLINE=$(echo "$COMPLETED_TASKS_INLINE" | tr -d '\n\r ' | grep -E '^[0-9]+$' || echo 0)
92
+ # Count completed tasks (both checkbox formats)
93
+ # Format 1: [x] at line start
94
+ COMPLETED_STANDARD=$(grep -c '^\[x\]' "$TASKS_FILE" 2>/dev/null || echo 0)
95
+ COMPLETED_STANDARD=$(echo "$COMPLETED_STANDARD" | tr -d '\n\r ' || echo 0)
94
96
 
95
- COMPLETED_TASKS=$((COMPLETED_TASKS_STANDARD + COMPLETED_TASKS_INLINE))
97
+ # Format 2: **Status**: [x] inline
98
+ COMPLETED_INLINE=$(grep -c '\*\*Status\*\*: \[x\]' "$TASKS_FILE" 2>/dev/null || echo 0)
99
+ COMPLETED_INLINE=$(echo "$COMPLETED_INLINE" | tr -d '\n\r ' || echo 0)
96
100
 
97
- # Calculate percentage
98
- if [[ "$TOTAL_TASKS" -gt 0 ]]; then
99
- PERCENTAGE=$(( COMPLETED_TASKS * 100 / TOTAL_TASKS ))
100
- else
101
- PERCENTAGE=0
102
- fi
101
+ COMPLETED_TASKS=$((COMPLETED_STANDARD + COMPLETED_INLINE))
103
102
 
104
- # Find current task (first incomplete task)
105
- # Strategy: Find first [ ] checkbox (either format), then get the task heading above it
106
- # Try standard format first (checkbox at line start)
107
- CURRENT_TASK_LINE=$(grep -B1 '^\[ \]' "$TASKS_FILE" 2>/dev/null | grep -E '^##+ T-' | head -1 || echo "")
108
-
109
- # If not found, try inline format (**Status**: [ ])
110
- if [[ -z "$CURRENT_TASK_LINE" ]]; then
111
- # Find line with **Status**: [ ], then look backward for task heading
112
- TASK_LINE_NUM=$(grep -n '\*\*Status\*\*: \[ \]' "$TASKS_FILE" 2>/dev/null | head -1 | cut -d: -f1 || echo "")
113
- if [[ -n "$TASK_LINE_NUM" ]]; then
114
- # Get lines before the status line and find the task heading
115
- CURRENT_TASK_LINE=$(head -n "$TASK_LINE_NUM" "$TASKS_FILE" | grep -E '^##+ T-' | tail -1 || echo "")
103
+ # Calculate percentage
104
+ if [[ $TOTAL_TASKS -gt 0 ]]; then
105
+ PERCENTAGE=$((COMPLETED_TASKS * 100 / TOTAL_TASKS))
116
106
  fi
117
107
  fi
118
- CURRENT_TASK_ID=""
119
- CURRENT_TASK_TITLE=""
120
-
121
- if [[ -n "$CURRENT_TASK_LINE" ]]; then
122
- # Extract task ID (T-NNN)
123
- CURRENT_TASK_ID=$(echo "$CURRENT_TASK_LINE" | grep -o 'T-[0-9][0-9]*' || echo "")
124
-
125
- # Extract task title (after "## T-NNN: ")
126
- # Use parameter expansion to remove prefix
127
- TEMP="${CURRENT_TASK_LINE#*: }"
128
- CURRENT_TASK_TITLE=$(echo "$TEMP" | head -c 50)
129
- fi
130
108
 
131
- # Extract increment name (remove leading 4-digit number and dash)
132
- INCREMENT_NAME=$(echo "$INCREMENT_ID" | sed 's/^[0-9]\{4\}-//')
133
-
134
- # Build current task JSON
135
- if [[ -n "$CURRENT_TASK_ID" ]]; then
136
- CURRENT_TASK_JSON=$(jq -n \
137
- --arg id "$CURRENT_TASK_ID" \
138
- --arg title "$CURRENT_TASK_TITLE" \
139
- '{id: $id, title: $title}')
140
- else
141
- CURRENT_TASK_JSON="null"
142
- fi
109
+ # Step 5: Extract increment name (remove 4-digit prefix)
110
+ INCREMENT_NAME=$(echo "$CURRENT_INCREMENT" | sed 's/^[0-9]\{4\}-//')
143
111
 
144
- # Write cache atomically using jq
112
+ # Step 6: Write cache
145
113
  jq -n \
146
- --arg id "$INCREMENT_ID" \
114
+ --arg id "$CURRENT_INCREMENT" \
147
115
  --arg name "$INCREMENT_NAME" \
148
- --argjson total "$TOTAL_TASKS" \
149
116
  --argjson completed "$COMPLETED_TASKS" \
117
+ --argjson total "$TOTAL_TASKS" \
150
118
  --argjson percentage "$PERCENTAGE" \
151
- --argjson task "$CURRENT_TASK_JSON" \
152
- --argjson mtime "$MTIME" \
119
+ --argjson openCount "$OPEN_COUNT" \
153
120
  '{
154
- incrementId: $id,
155
- incrementName: $name,
156
- totalTasks: $total,
157
- completedTasks: $completed,
158
- percentage: $percentage,
159
- currentTask: $task,
160
- lastUpdate: (now | strftime("%Y-%m-%dT%H:%M:%SZ")),
161
- lastModified: $mtime
121
+ current: {
122
+ id: $id,
123
+ name: $name,
124
+ completed: $completed,
125
+ total: $total,
126
+ percentage: $percentage
127
+ },
128
+ openCount: $openCount,
129
+ lastUpdate: (now | strftime("%Y-%m-%dT%H:%M:%SZ"))
162
130
  }' > "$CACHE_FILE"
163
131
 
164
132
  exit 0
@@ -396,15 +396,31 @@ create_github_issue() {
396
396
  return 1
397
397
  fi
398
398
 
399
- # Extract increment number from ID (e.g., "0016-self-reflection-system" -> "0016")
400
- local inc_number=$(echo "$increment_id" | grep -o '^[0-9]*')
399
+ # Extract creation date from metadata.json and format as FS-YY-MM-DD
400
+ local issue_prefix="FS-UNKNOWN"
401
+ if [ -f "$metadata_file" ]; then
402
+ # Extract created date (format: "2025-11-12T12:46:00Z")
403
+ local created_date=$(cat "$metadata_file" 2>/dev/null | grep -o '"created"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)".*/\1/')
404
+ if [ -n "$created_date" ]; then
405
+ # Extract YY-MM-DD from date (e.g., "2025-11-12" -> "25-11-12")
406
+ local year=$(echo "$created_date" | cut -d'-' -f1 | cut -c3-4) # "2025" -> "25"
407
+ local month=$(echo "$created_date" | cut -d'-' -f2) # "11"
408
+ local day=$(echo "$created_date" | cut -d'-' -f3 | cut -d'T' -f1) # "12T..." -> "12"
409
+ issue_prefix="FS-${year}-${month}-${day}"
410
+ log_debug "Using date-based prefix from metadata: $issue_prefix"
411
+ else
412
+ log_debug "No created date in metadata, using fallback"
413
+ fi
414
+ else
415
+ log_debug "No metadata.json found, using fallback prefix"
416
+ fi
401
417
 
402
418
  log_debug "Creating issue for repo: $repo"
403
- log_debug "Title: [INC-$inc_number] $title"
419
+ log_debug "Title: [$issue_prefix] $title"
404
420
 
405
421
  # Generate issue body
406
422
  local issue_body=$(cat <<EOF
407
- # [INC-$inc_number] $title
423
+ # [$issue_prefix] $title
408
424
 
409
425
  **Status**: Planning → Implementation
410
426
  **Priority**: P1
@@ -436,43 +452,65 @@ EOF
436
452
  local temp_body=$(mktemp)
437
453
  echo "$issue_body" > "$temp_body"
438
454
 
439
- # Create GitHub issue
440
- log_debug "Calling gh issue create..."
455
+ # Create GitHub issue with FULL DUPLICATE PROTECTION
456
+ log_debug "Creating issue with DuplicateDetector (global protection)..."
441
457
 
442
- local gh_output=$(gh issue create \
458
+ # Call Node.js wrapper script with DuplicateDetector
459
+ local node_output=$(node scripts/create-github-issue-with-protection.js \
460
+ --title "[$issue_prefix] $title" \
461
+ --body "$issue_body" \
462
+ --pattern "[$issue_prefix]" \
463
+ --labels "specweave,increment" \
443
464
  --repo "$repo" \
444
- --title "[INC-$inc_number] $title" \
445
- --body-file "$temp_body" \
446
- --label "specweave,increment" \
447
465
  2>&1)
448
466
 
449
- local gh_status=$?
467
+ local node_status=$?
450
468
 
451
469
  # Clean up temp file
452
470
  rm -f "$temp_body"
453
471
 
454
- if [ $gh_status -ne 0 ]; then
455
- log_error "gh issue create failed: $gh_output"
472
+ if [ $node_status -ne 0 ]; then
473
+ log_error "DuplicateDetector failed: $node_output"
456
474
  return 1
457
475
  fi
458
476
 
459
- # Extract issue number from output (format: "https://github.com/owner/repo/issues/123")
460
- local issue_number=$(echo "$gh_output" | grep -o '/issues/[0-9]*' | grep -o '[0-9]*')
461
- local issue_url=$(echo "$gh_output" | grep 'https://github.com')
477
+ # Parse JSON output using jq (if available) or fallback to grep
478
+ local issue_number=""
479
+ local issue_url=""
480
+ local duplicates_found=0
481
+ local duplicates_closed=0
482
+ local was_reused="false"
483
+
484
+ if command -v jq >/dev/null 2>&1; then
485
+ issue_number=$(echo "$node_output" | jq -r '.issue.number')
486
+ issue_url=$(echo "$node_output" | jq -r '.issue.url')
487
+ duplicates_found=$(echo "$node_output" | jq -r '.duplicatesFound // 0')
488
+ duplicates_closed=$(echo "$node_output" | jq -r '.duplicatesClosed // 0')
489
+ was_reused=$(echo "$node_output" | jq -r '.wasReused // false')
490
+ else
491
+ # Fallback: grep-based parsing
492
+ issue_number=$(echo "$node_output" | grep -o '"number"[[:space:]]*:[[:space:]]*[0-9]*' | grep -o '[0-9]*')
493
+ issue_url=$(echo "$node_output" | grep -o '"url"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)".*/\1/')
494
+ fi
462
495
 
463
496
  if [ -z "$issue_number" ]; then
464
- log_error "Could not extract issue number from gh output: $gh_output"
497
+ log_error "Could not extract issue number from DuplicateDetector output"
498
+ log_debug "Output was: $node_output"
465
499
  return 1
466
500
  fi
467
501
 
468
- if [ -z "$issue_url" ]; then
469
- # Construct URL manually if extraction failed
470
- issue_url="https://github.com/$repo/issues/$issue_number"
502
+ # Log results with duplicate detection info
503
+ if [ "$was_reused" = "true" ]; then
504
+ log_info " ♻️ Using existing issue #$issue_number (duplicate prevention)"
505
+ else
506
+ log_info " 📝 Issue #$issue_number created"
471
507
  fi
472
-
473
- log_info " 📝 Issue #$issue_number created"
474
508
  log_info " 🔗 $issue_url"
475
509
 
510
+ if [ "$duplicates_found" -gt 0 ]; then
511
+ log_info " 🛡️ Duplicates detected: $duplicates_found (auto-closed: $duplicates_closed)"
512
+ fi
513
+
476
514
  # Create or update metadata.json
477
515
  local current_timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
478
516
 
@@ -658,21 +696,55 @@ EOF
658
696
  if [ "$auto_create" = "true" ]; then
659
697
  log_info " 📦 Auto-create enabled, checking for GitHub CLI..."
660
698
 
661
- # Check if gh CLI is available
662
- if ! command -v gh >/dev/null 2>&1; then
663
- log_info " ⚠️ GitHub CLI (gh) not found, skipping issue creation"
664
- log_debug "Install: https://cli.github.com/"
665
- else
666
- log_info " ✓ GitHub CLI found"
667
- log_info ""
668
- log_info "🚀 Creating GitHub issue for $increment_id..."
699
+ # ============================================================================
700
+ # DUPLICATE DETECTION (v0.14.1+)
701
+ # ============================================================================
702
+ # Check if GitHub issue already exists in metadata.json
703
+ # If exists, skip creation (idempotent operation)
704
+
705
+ local metadata_file="$increment_dir/metadata.json"
706
+ local existing_issue=""
707
+
708
+ if [ -f "$metadata_file" ]; then
709
+ # Extract existing GitHub issue number from metadata
710
+ existing_issue=$(cat "$metadata_file" 2>/dev/null | \
711
+ grep -o '"github"[[:space:]]*:[[:space:]]*{[^}]*"issue"[[:space:]]*:[[:space:]]*[0-9]*' | \
712
+ grep -o '[0-9]*$')
713
+
714
+ if [ -n "$existing_issue" ]; then
715
+ log_info " ✅ GitHub issue already exists: #$existing_issue"
716
+ log_info " ⏭️ Skipping creation (idempotent)"
717
+ log_debug "Metadata already contains github.issue = $existing_issue"
718
+
719
+ # Extract URL if available
720
+ local existing_url=$(cat "$metadata_file" 2>/dev/null | \
721
+ grep -o '"url"[[:space:]]*:[[:space:]]*"[^"]*"' | \
722
+ sed 's/.*"\([^"]*\)".*/\1/')
723
+
724
+ if [ -n "$existing_url" ]; then
725
+ log_info " 🔗 $existing_url"
726
+ fi
727
+ fi
728
+ fi
669
729
 
670
- # Create issue (non-blocking)
671
- if create_github_issue "$increment_id" "$increment_dir"; then
672
- log_info " ✅ GitHub issue created successfully"
730
+ # Only create if no existing issue found
731
+ if [ -z "$existing_issue" ]; then
732
+ # Check if gh CLI is available
733
+ if ! command -v gh >/dev/null 2>&1; then
734
+ log_info " ⚠️ GitHub CLI (gh) not found, skipping issue creation"
735
+ log_debug "Install: https://cli.github.com/"
673
736
  else
674
- log_info " ⚠️ GitHub issue creation failed (non-blocking)"
675
- log_debug "Issue creation failed, but continuing execution"
737
+ log_info " GitHub CLI found"
738
+ log_info ""
739
+ log_info "🚀 Creating GitHub issue for $increment_id..."
740
+
741
+ # Create issue (non-blocking)
742
+ if create_github_issue "$increment_id" "$increment_dir"; then
743
+ log_info " ✅ GitHub issue created successfully"
744
+ else
745
+ log_info " ⚠️ GitHub issue creation failed (non-blocking)"
746
+ log_debug "Issue creation failed, but continuing execution"
747
+ fi
676
748
  fi
677
749
  fi
678
750
  else
@@ -27,9 +27,10 @@ async function syncLivingDocs(incrementId) {
27
27
  specCopied = result.success;
28
28
  changedDocs = result.changedFiles;
29
29
  } else {
30
- console.log("\u{1F4CB} Using simple sync mode (legacy)");
31
- specCopied = await copyIncrementSpecToLivingDocs(incrementId);
32
- changedDocs = detectChangedDocs();
30
+ console.log("\u{1F4CA} Using hierarchical distribution mode (v2.1 - Epic + User Stories)");
31
+ const result = await hierarchicalDistribution(incrementId);
32
+ specCopied = result.success;
33
+ changedDocs = result.changedFiles;
33
34
  }
34
35
  if (changedDocs.length === 0 && !specCopied) {
35
36
  console.log("\u2139\uFE0F No living docs changed");
@@ -43,43 +44,47 @@ async function syncLivingDocs(incrementId) {
43
44
  }
44
45
  }
45
46
  async function intelligentSyncLivingDocs(incrementId, config) {
47
+ console.log(" \u26A0\uFE0F Intelligent sync not yet fully implemented");
48
+ console.log(" Falling back to hierarchical distribution mode...");
49
+ return await hierarchicalDistribution(incrementId);
50
+ }
51
+ async function hierarchicalDistribution(incrementId) {
46
52
  try {
47
- const { syncIncrement } = await import("../../../../src/core/living-docs/index.js");
48
- console.log(" \u{1F4D6} Parsing and classifying spec sections...");
49
- const result = await syncIncrement(incrementId, {
50
- verbose: false,
51
- // We'll log our own summary
52
- dryRun: false,
53
- parser: {
54
- preserveCodeBlocks: true,
55
- preserveLinks: true,
56
- preserveImages: true
57
- },
58
- distributor: {
59
- generateFrontmatter: true,
60
- preserveOriginal: config.livingDocs?.intelligent?.preserveOriginal ?? true
61
- },
62
- linker: {
63
- generateBacklinks: config.livingDocs?.intelligent?.generateCrossLinks ?? true,
64
- updateExisting: true
65
- }
53
+ const { SpecDistributor } = await import("../../../../src/core/living-docs/index.js");
54
+ console.log(" \u{1F4CA} Parsing and distributing spec into hierarchical structure...");
55
+ const projectRoot = process.cwd();
56
+ const distributor = new SpecDistributor(projectRoot, {
57
+ overwriteExisting: false,
58
+ createBackups: true
66
59
  });
67
- console.log(` \u2705 Intelligent sync complete:`);
68
- console.log(` Project: ${result.project.name} (${(result.project.confidence * 100).toFixed(0)}% confidence)`);
69
- console.log(` Files created: ${result.distribution.summary.filesCreated}`);
70
- console.log(` Files updated: ${result.distribution.summary.filesUpdated}`);
71
- console.log(` Cross-links: ${result.links.length}`);
72
- console.log(` Duration: ${result.duration}ms`);
73
- const changedFiles = [
74
- ...result.distribution.created.map((f) => f.path),
75
- ...result.distribution.updated.map((f) => f.path)
76
- ];
60
+ const result = await distributor.distribute(incrementId);
61
+ if (!result.success) {
62
+ console.error(` \u274C Distribution failed with errors:`);
63
+ for (const error of result.errors) {
64
+ console.error(` - ${error}`);
65
+ }
66
+ return { success: false, changedFiles: [] };
67
+ }
68
+ console.log(` \u2705 Hierarchical distribution complete:`);
69
+ console.log(` Epic ID: ${result.specId}`);
70
+ console.log(` User Stories: ${result.totalStories}`);
71
+ console.log(` Files created: ${result.totalFiles}`);
72
+ console.log(` Epic: ${path.basename(result.epicPath)}`);
73
+ console.log(` User story files: ${result.userStoryPaths.length}`);
74
+ if (result.warnings.length > 0) {
75
+ console.log(` \u26A0\uFE0F Warnings:`);
76
+ for (const warning of result.warnings) {
77
+ console.log(` - ${warning}`);
78
+ }
79
+ }
80
+ const changedFiles = [result.epicPath, ...result.userStoryPaths];
77
81
  return {
78
- success: result.success,
82
+ success: true,
79
83
  changedFiles
80
84
  };
81
85
  } catch (error) {
82
- console.error(` \u274C Intelligent sync failed: ${error}`);
86
+ console.error(` \u274C Hierarchical distribution failed: ${error}`);
87
+ console.error(error.stack);
83
88
  console.error(" Falling back to simple sync mode...");
84
89
  const copied = await copyIncrementSpecToLivingDocs(incrementId);
85
90
  return {
@@ -88,7 +93,107 @@ async function intelligentSyncLivingDocs(incrementId, config) {
88
93
  };
89
94
  }
90
95
  }
96
+ async function extractAndMergeLivingDocs(incrementId) {
97
+ try {
98
+ const {
99
+ parseIncrementSpec,
100
+ parseLivingDocsSpec,
101
+ extractSpecId,
102
+ mergeUserStories,
103
+ generateRelatedDocsLinks,
104
+ writeLivingDocsSpec
105
+ } = await import("../../../../src/utils/spec-parser.js");
106
+ const projectRoot = process.cwd();
107
+ const incrementSpecPath = path.join(projectRoot, ".specweave", "increments", incrementId, "spec.md");
108
+ if (!fs.existsSync(incrementSpecPath)) {
109
+ console.log(`\u26A0\uFE0F Increment spec not found: ${incrementSpecPath}`);
110
+ return false;
111
+ }
112
+ console.log(` \u{1F4D6} Parsing increment spec: ${incrementId}`);
113
+ const incrementSpec = await parseIncrementSpec(incrementSpecPath);
114
+ if (incrementSpec.userStories.length === 0) {
115
+ console.log(`\u2139\uFE0F No user stories found in increment spec, skipping sync`);
116
+ return false;
117
+ }
118
+ console.log(` \u2705 Found ${incrementSpec.userStories.length} user stories in increment`);
119
+ const specId = incrementSpec.implementsSpec || extractSpecId(incrementId);
120
+ const livingDocsDir = path.join(projectRoot, ".specweave", "docs", "internal", "specs", "default");
121
+ const livingDocsPath = path.join(livingDocsDir, `${specId}-${incrementId.replace(/^\d+-/, "")}.md`);
122
+ const livingDocsExists = fs.existsSync(livingDocsPath);
123
+ if (livingDocsExists) {
124
+ console.log(` \u{1F4DA} Living docs spec exists, merging user stories...`);
125
+ const livingSpec = await parseLivingDocsSpec(livingDocsPath);
126
+ const mergedStories = mergeUserStories(
127
+ livingSpec.userStories,
128
+ incrementSpec.userStories,
129
+ incrementId
130
+ );
131
+ const newStoriesCount = mergedStories.length - livingSpec.userStories.length;
132
+ const existingEntry = livingSpec.implementationHistory.find((e) => e.increment === incrementId);
133
+ if (!existingEntry) {
134
+ livingSpec.implementationHistory.push({
135
+ increment: incrementId,
136
+ stories: incrementSpec.userStories.map((s) => s.id),
137
+ status: "complete",
138
+ date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
139
+ });
140
+ } else {
141
+ existingEntry.status = "complete";
142
+ existingEntry.date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
143
+ }
144
+ livingSpec.userStories = mergedStories;
145
+ livingSpec.lastUpdated = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
146
+ await writeLivingDocsSpec(livingDocsPath, livingSpec);
147
+ console.log(` \u2705 Merged ${newStoriesCount} new user stories into living docs`);
148
+ console.log(` \u2705 Updated implementation history for ${incrementId}`);
149
+ } else {
150
+ console.log(` \u{1F4DD} Creating new living docs spec: ${specId}`);
151
+ const relatedDocs = generateRelatedDocsLinks(incrementSpec, projectRoot);
152
+ const livingSpec = {
153
+ id: specId,
154
+ title: incrementSpec.title,
155
+ featureArea: extractFeatureArea(incrementSpec.title),
156
+ overview: incrementSpec.overview,
157
+ userStories: incrementSpec.userStories.map((story) => ({
158
+ ...story,
159
+ implementedIn: incrementId,
160
+ status: "complete"
161
+ })),
162
+ implementationHistory: [
163
+ {
164
+ increment: incrementId,
165
+ stories: incrementSpec.userStories.map((s) => s.id),
166
+ status: "complete",
167
+ date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
168
+ }
169
+ ],
170
+ relatedDocs,
171
+ externalLinks: {},
172
+ priority: incrementSpec.priority,
173
+ status: "active",
174
+ created: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
175
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
176
+ };
177
+ await fs.ensureDir(livingDocsDir);
178
+ await writeLivingDocsSpec(livingDocsPath, livingSpec);
179
+ console.log(` \u2705 Created new living docs spec: ${specId}`);
180
+ console.log(` \u2705 Added ${incrementSpec.userStories.length} user stories`);
181
+ console.log(` \u2705 Generated links to ${relatedDocs.architecture.length} architecture docs`);
182
+ console.log(` \u2705 Generated links to ${relatedDocs.adrs.length} ADRs`);
183
+ }
184
+ return true;
185
+ } catch (error) {
186
+ console.error(`\u274C Error extracting/merging living docs: ${error}`);
187
+ console.error(error.stack);
188
+ return false;
189
+ }
190
+ }
191
+ function extractFeatureArea(title) {
192
+ return title.replace(/^(Increment \d+:\s*)?/, "").trim();
193
+ }
91
194
  async function copyIncrementSpecToLivingDocs(incrementId) {
195
+ console.warn("\u26A0\uFE0F Using deprecated copyIncrementSpecToLivingDocs (simple mode)");
196
+ console.warn(" Consider enabling intelligent mode to avoid duplication");
92
197
  try {
93
198
  const incrementSpecPath = path.join(process.cwd(), ".specweave", "increments", incrementId, "spec.md");
94
199
  const livingDocsPath = path.join(process.cwd(), ".specweave", "docs", "internal", "specs", `spec-${incrementId}.md`);