specweave 0.17.15 → 0.18.0
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.
- package/CLAUDE.md +405 -2495
- package/README.md +92 -2
- package/dist/plugins/specweave/lib/hooks/sync-living-docs.d.ts.map +1 -1
- package/dist/plugins/specweave/lib/hooks/sync-living-docs.js +188 -36
- package/dist/plugins/specweave/lib/hooks/sync-living-docs.js.map +1 -1
- package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts +54 -0
- package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-ado/lib/ado-status-sync.js +86 -0
- package/dist/plugins/specweave-ado/lib/ado-status-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts +139 -0
- package/dist/plugins/specweave-github/lib/duplicate-detector.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/duplicate-detector.js +389 -0
- package/dist/plugins/specweave-github/lib/duplicate-detector.js.map +1 -0
- package/dist/plugins/specweave-github/lib/enhanced-github-sync.d.ts +26 -0
- package/dist/plugins/specweave-github/lib/enhanced-github-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/enhanced-github-sync.js +249 -0
- package/dist/plugins/specweave-github/lib/enhanced-github-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-client.d.ts +1 -1
- package/dist/plugins/specweave-github/lib/github-client.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client.js +25 -13
- package/dist/plugins/specweave-github/lib/github-client.js.map +1 -1
- package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts +83 -0
- package/dist/plugins/specweave-github/lib/github-epic-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-epic-sync.js +451 -0
- package/dist/plugins/specweave-github/lib/github-epic-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/github-status-sync.d.ts +43 -0
- package/dist/plugins/specweave-github/lib/github-status-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-github/lib/github-status-sync.js +82 -0
- package/dist/plugins/specweave-github/lib/github-status-sync.js.map +1 -0
- package/dist/plugins/specweave-github/lib/task-sync.d.ts +5 -0
- package/dist/plugins/specweave-github/lib/task-sync.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/task-sync.js +38 -2
- package/dist/plugins/specweave-github/lib/task-sync.js.map +1 -1
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts +66 -0
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.js +274 -0
- package/dist/plugins/specweave-jira/lib/jira-epic-sync.js.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +56 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js +93 -0
- package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -0
- package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/index.js +48 -3
- package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
- package/dist/src/core/living-docs/hierarchy-mapper.d.ts +142 -0
- package/dist/src/core/living-docs/hierarchy-mapper.d.ts.map +1 -0
- package/dist/src/core/living-docs/hierarchy-mapper.js +453 -0
- package/dist/src/core/living-docs/hierarchy-mapper.js.map +1 -0
- package/dist/src/core/living-docs/index.d.ts +10 -84
- package/dist/src/core/living-docs/index.d.ts.map +1 -1
- package/dist/src/core/living-docs/index.js +10 -164
- package/dist/src/core/living-docs/index.js.map +1 -1
- package/dist/src/core/living-docs/spec-distributor.d.ts +106 -0
- package/dist/src/core/living-docs/spec-distributor.d.ts.map +1 -0
- package/dist/src/core/living-docs/spec-distributor.js +823 -0
- package/dist/src/core/living-docs/spec-distributor.js.map +1 -0
- package/dist/src/core/living-docs/types.d.ts +201 -0
- package/dist/src/core/living-docs/types.d.ts.map +1 -0
- package/dist/src/core/living-docs/types.js +15 -0
- package/dist/src/core/living-docs/types.js.map +1 -0
- package/dist/src/core/logging/prompt-logger.d.ts +70 -0
- package/dist/src/core/logging/prompt-logger.d.ts.map +1 -0
- package/dist/src/core/logging/prompt-logger.js +247 -0
- package/dist/src/core/logging/prompt-logger.js.map +1 -0
- package/dist/src/core/status-line/status-line-manager.d.ts +15 -24
- package/dist/src/core/status-line/status-line-manager.d.ts.map +1 -1
- package/dist/src/core/status-line/status-line-manager.js +33 -70
- package/dist/src/core/status-line/status-line-manager.js.map +1 -1
- package/dist/src/core/status-line/types.d.ts +19 -31
- package/dist/src/core/status-line/types.d.ts.map +1 -1
- package/dist/src/core/status-line/types.js +5 -9
- package/dist/src/core/status-line/types.js.map +1 -1
- package/dist/src/core/sync/conflict-resolver.d.ts +66 -0
- package/dist/src/core/sync/conflict-resolver.d.ts.map +1 -0
- package/dist/src/core/sync/conflict-resolver.js +108 -0
- package/dist/src/core/sync/conflict-resolver.js.map +1 -0
- package/dist/src/core/sync/enhanced-content-builder.d.ts +77 -0
- package/dist/src/core/sync/enhanced-content-builder.d.ts.map +1 -0
- package/dist/src/core/sync/enhanced-content-builder.js +199 -0
- package/dist/src/core/sync/enhanced-content-builder.js.map +1 -0
- package/dist/src/core/sync/label-detector.d.ts +66 -0
- package/dist/src/core/sync/label-detector.d.ts.map +1 -0
- package/dist/src/core/sync/label-detector.js +211 -0
- package/dist/src/core/sync/label-detector.js.map +1 -0
- package/dist/src/core/sync/retry-logic.d.ts +64 -0
- package/dist/src/core/sync/retry-logic.d.ts.map +1 -0
- package/dist/src/core/sync/retry-logic.js +165 -0
- package/dist/src/core/sync/retry-logic.js.map +1 -0
- package/dist/src/core/sync/spec-increment-mapper.d.ts +100 -0
- package/dist/src/core/sync/spec-increment-mapper.d.ts.map +1 -0
- package/dist/src/core/sync/spec-increment-mapper.js +424 -0
- package/dist/src/core/sync/spec-increment-mapper.js.map +1 -0
- package/dist/src/core/sync/status-cache.d.ts +91 -0
- package/dist/src/core/sync/status-cache.d.ts.map +1 -0
- package/dist/src/core/sync/status-cache.js +140 -0
- package/dist/src/core/sync/status-cache.js.map +1 -0
- package/dist/src/core/sync/status-mapper.d.ts +69 -0
- package/dist/src/core/sync/status-mapper.d.ts.map +1 -0
- package/dist/src/core/sync/status-mapper.js +90 -0
- package/dist/src/core/sync/status-mapper.js.map +1 -0
- package/dist/src/core/sync/status-sync-engine.d.ts +162 -0
- package/dist/src/core/sync/status-sync-engine.d.ts.map +1 -0
- package/dist/src/core/sync/status-sync-engine.js +347 -0
- package/dist/src/core/sync/status-sync-engine.js.map +1 -0
- package/dist/src/core/sync/sync-event-logger.d.ts +99 -0
- package/dist/src/core/sync/sync-event-logger.d.ts.map +1 -0
- package/dist/src/core/sync/sync-event-logger.js +103 -0
- package/dist/src/core/sync/sync-event-logger.js.map +1 -0
- package/dist/src/core/sync/workflow-detector.d.ts +95 -0
- package/dist/src/core/sync/workflow-detector.d.ts.map +1 -0
- package/dist/src/core/sync/workflow-detector.js +175 -0
- package/dist/src/core/sync/workflow-detector.js.map +1 -0
- package/dist/src/core/types/config.d.ts.map +1 -1
- package/dist/src/core/types/config.js +31 -0
- package/dist/src/core/types/config.js.map +1 -1
- package/dist/src/utils/github-url.d.ts +53 -0
- package/dist/src/utils/github-url.d.ts.map +1 -0
- package/dist/src/utils/github-url.js +90 -0
- package/dist/src/utils/github-url.js.map +1 -0
- package/dist/src/utils/plugin-validator.d.ts +9 -0
- package/dist/src/utils/plugin-validator.d.ts.map +1 -1
- package/dist/src/utils/plugin-validator.js +86 -19
- package/dist/src/utils/plugin-validator.js.map +1 -1
- package/dist/src/utils/spec-parser.d.ts +145 -0
- package/dist/src/utils/spec-parser.d.ts.map +1 -0
- package/dist/src/utils/spec-parser.js +640 -0
- package/dist/src/utils/spec-parser.js.map +1 -0
- package/package.json +1 -1
- package/plugins/specweave/agents/pm/AGENT.md +1 -1
- package/plugins/specweave/agents/pm/templates/increment-spec.md +158 -0
- package/plugins/specweave/agents/pm/templates/living-docs-spec.md +113 -0
- package/plugins/specweave/commands/specweave-done.md +163 -0
- package/plugins/specweave/hooks/lib/update-status-line.sh +79 -111
- package/plugins/specweave/hooks/post-increment-planning.sh +107 -35
- package/plugins/specweave/lib/hooks/sync-living-docs.js +139 -34
- package/plugins/specweave/lib/hooks/sync-living-docs.ts +234 -38
- package/plugins/specweave/skills/SKILLS-INDEX.md +4 -24
- package/plugins/specweave/skills/increment-planner/SKILL.md +94 -0
- package/plugins/specweave/skills/increment-work-router/SKILL.md +466 -0
- package/plugins/specweave/skills/plugin-validator/SKILL.md +16 -13
- package/plugins/specweave-ado/lib/ado-status-sync.js +80 -0
- package/plugins/specweave-ado/lib/ado-status-sync.ts +121 -0
- package/plugins/specweave-github/commands/specweave-github-cleanup-duplicates.md +205 -0
- package/plugins/specweave-github/commands/specweave-github-sync-epic.md +248 -0
- package/plugins/specweave-github/lib/duplicate-detector.js +370 -0
- package/plugins/specweave-github/lib/duplicate-detector.ts +525 -0
- package/plugins/specweave-github/lib/enhanced-github-sync.js +220 -0
- package/plugins/specweave-github/lib/enhanced-github-sync.ts +322 -0
- package/plugins/specweave-github/lib/github-client.js +21 -10
- package/plugins/specweave-github/lib/github-client.ts +27 -16
- package/plugins/specweave-github/lib/github-epic-sync.js +489 -0
- package/plugins/specweave-github/lib/github-epic-sync.ts +690 -0
- package/plugins/specweave-github/lib/github-status-sync.js +71 -0
- package/plugins/specweave-github/lib/github-status-sync.ts +107 -0
- package/plugins/specweave-github/lib/task-sync.js +33 -2
- package/plugins/specweave-github/lib/task-sync.ts +44 -2
- package/plugins/specweave-jira/commands/specweave-jira-sync-epic.md +267 -0
- package/plugins/specweave-jira/lib/enhanced-jira-sync.ts.disabled +222 -0
- package/plugins/specweave-jira/lib/jira-epic-sync.js +304 -0
- package/plugins/specweave-jira/lib/jira-epic-sync.ts +459 -0
- package/plugins/specweave-jira/lib/jira-status-sync.js +79 -0
- package/plugins/specweave-jira/lib/jira-status-sync.ts +139 -0
- package/src/templates/AGENTS.md.template +88 -1
- package/src/templates/CLAUDE.md.template +49 -0
- package/plugins/specweave/skills/increment-quality-judge/SKILL.md +0 -524
- 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
|
|
6
|
-
#
|
|
5
|
+
# Updates status line cache with current increment progress.
|
|
6
|
+
# Shows: [increment-name] ████░░░░ X/Y tasks (Z open)
|
|
7
7
|
#
|
|
8
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
if [[ -
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
+
# Step 2: Count open increments
|
|
62
|
+
OPEN_COUNT=$(wc -l < "$TMP_FILE" | tr -d ' ')
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
#
|
|
70
|
-
|
|
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
|
-
#
|
|
79
|
-
|
|
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
|
-
#
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
#
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
132
|
-
INCREMENT_NAME=$(echo "$
|
|
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
|
|
112
|
+
# Step 6: Write cache
|
|
145
113
|
jq -n \
|
|
146
|
-
--arg 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
|
|
152
|
-
--argjson mtime "$MTIME" \
|
|
119
|
+
--argjson openCount "$OPEN_COUNT" \
|
|
153
120
|
'{
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
400
|
-
local
|
|
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: [
|
|
419
|
+
log_debug "Title: [$issue_prefix] $title"
|
|
404
420
|
|
|
405
421
|
# Generate issue body
|
|
406
422
|
local issue_body=$(cat <<EOF
|
|
407
|
-
# [
|
|
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 "
|
|
455
|
+
# Create GitHub issue with FULL DUPLICATE PROTECTION
|
|
456
|
+
log_debug "Creating issue with DuplicateDetector (global protection)..."
|
|
441
457
|
|
|
442
|
-
|
|
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
|
|
467
|
+
local node_status=$?
|
|
450
468
|
|
|
451
469
|
# Clean up temp file
|
|
452
470
|
rm -f "$temp_body"
|
|
453
471
|
|
|
454
|
-
if [ $
|
|
455
|
-
log_error "
|
|
472
|
+
if [ $node_status -ne 0 ]; then
|
|
473
|
+
log_error "DuplicateDetector failed: $node_output"
|
|
456
474
|
return 1
|
|
457
475
|
fi
|
|
458
476
|
|
|
459
|
-
#
|
|
460
|
-
local issue_number
|
|
461
|
-
local issue_url
|
|
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
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
#
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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 "
|
|
675
|
-
|
|
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{
|
|
31
|
-
|
|
32
|
-
|
|
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 {
|
|
48
|
-
console.log(" \u{
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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:
|
|
82
|
+
success: true,
|
|
79
83
|
changedFiles
|
|
80
84
|
};
|
|
81
85
|
} catch (error) {
|
|
82
|
-
console.error(` \u274C
|
|
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`);
|