specweave 0.26.10 → 0.26.13

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 (33) hide show
  1. package/CLAUDE.md +95 -520
  2. package/dist/plugins/specweave-github/lib/completion-calculator.d.ts +4 -1
  3. package/dist/plugins/specweave-github/lib/completion-calculator.d.ts.map +1 -1
  4. package/dist/plugins/specweave-github/lib/completion-calculator.js +49 -29
  5. package/dist/plugins/specweave-github/lib/completion-calculator.js.map +1 -1
  6. package/dist/src/cli/commands/init.js +2 -2
  7. package/dist/src/cli/commands/init.js.map +1 -1
  8. package/dist/src/core/increment/increment-archiver.d.ts +3 -0
  9. package/dist/src/core/increment/increment-archiver.d.ts.map +1 -1
  10. package/dist/src/core/increment/increment-archiver.js +35 -4
  11. package/dist/src/core/increment/increment-archiver.js.map +1 -1
  12. package/dist/src/core/living-docs/feature-archiver.d.ts +5 -0
  13. package/dist/src/core/living-docs/feature-archiver.d.ts.map +1 -1
  14. package/dist/src/core/living-docs/feature-archiver.js +66 -18
  15. package/dist/src/core/living-docs/feature-archiver.js.map +1 -1
  16. package/package.json +1 -1
  17. package/plugins/specweave/commands/specweave-archive.md +10 -1
  18. package/plugins/specweave/hooks/hooks.json +10 -0
  19. package/plugins/specweave/hooks/lib/update-active-increment.sh +96 -0
  20. package/plugins/specweave/hooks/lib/update-status-line.sh +153 -189
  21. package/plugins/specweave/hooks/post-edit-write-consolidated.sh +6 -0
  22. package/plugins/specweave/hooks/post-metadata-change.sh +9 -0
  23. package/plugins/specweave/hooks/post-task-completion.sh +8 -0
  24. package/plugins/specweave/hooks/post-task-edit.sh +37 -0
  25. package/plugins/specweave/hooks/pre-command-deduplication.sh +43 -53
  26. package/plugins/specweave/hooks/pre-tool-use.sh +5 -0
  27. package/plugins/specweave/hooks/user-prompt-submit.sh +143 -289
  28. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +18 -0
  29. package/plugins/specweave-github/lib/completion-calculator.js +34 -16
  30. package/plugins/specweave-github/lib/completion-calculator.ts +54 -32
  31. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +27 -0
  32. package/src/templates/AGENTS.md.template +301 -2452
  33. package/src/templates/CLAUDE.md.template +99 -667
@@ -1,225 +1,189 @@
1
1
  #!/usr/bin/env bash
2
2
  #
3
- # update-status-line.sh (Enhanced with AC Metrics)
3
+ # update-status-line.sh (v0.26.13 - ULTRA-OPTIMIZED for crash prevention)
4
4
  #
5
5
  # Updates status line cache with current increment progress.
6
6
  # Shows: [increment-name] ████░░░░ X/Y tasks | A/B ACs (Z open)
7
7
  #
8
- # Logic:
9
- # 1. Scan all spec.md for status=active/planning (SOURCE OF TRUTH!)
10
- # 2. Take first (oldest) as current increment
11
- # 3. Count all active/planning as openCount
12
- # 4. Parse current increment's tasks.md for task progress
13
- # 5. Parse current increment's spec.md for AC progress
14
- # 6. Write to cache
8
+ # OPTIMIZATIONS (v0.26.13):
9
+ # 1. TTL-based throttling (10s) - longer cache = fewer runs
10
+ # 2. Mtime checking via find -newer (no stat loops!)
11
+ # 3. Pure bash counting + JSON generation (NO jq!)
12
+ # 4. Single-pass awk for all counting (1 process vs 5 greps)
13
+ # 5. Exclude _archive/ with find -not -path
14
+ # 6. Lock file to prevent concurrent runs
15
15
  #
16
- # Performance: 50-100ms (runs async, user doesn't wait)
16
+ # Performance: <5ms (cached) / 15-25ms (full scan)
17
17
  #
18
- # EMERGENCY FIX (v0.24.4): Changed from set -euo pipefail to set +e
19
- # CRITICAL: Hooks MUST use set +e to prevent Claude Code crashes!
20
- # See: CLAUDE.md section 9a - Hook Performance & Safety
21
-
22
18
  set +e
23
19
 
24
- # Find project root
25
- find_project_root() {
26
- local dir="$PWD"
27
- while [[ "$dir" != "/" ]]; do
28
- if [[ -d "$dir/.specweave" ]]; then
29
- echo "$dir"
30
- return 0
31
- fi
32
- dir=$(dirname "$dir")
20
+ # ============================================================================
21
+ # PROJECT ROOT (FAST - cached in env if available)
22
+ # ============================================================================
23
+ if [[ -n "$SPECWEAVE_PROJECT_ROOT" ]] && [[ -d "$SPECWEAVE_PROJECT_ROOT/.specweave" ]]; then
24
+ PROJECT_ROOT="$SPECWEAVE_PROJECT_ROOT"
25
+ else
26
+ PROJECT_ROOT="$PWD"
27
+ while [[ "$PROJECT_ROOT" != "/" ]] && [[ ! -d "$PROJECT_ROOT/.specweave" ]]; do
28
+ PROJECT_ROOT=$(dirname "$PROJECT_ROOT")
33
29
  done
34
- echo "$PWD"
35
- }
36
-
37
- PROJECT_ROOT=$(find_project_root)
30
+ [[ ! -d "$PROJECT_ROOT/.specweave" ]] && PROJECT_ROOT="$PWD"
31
+ fi
38
32
 
39
33
  # ============================================================================
40
- # RECURSION PREVENTION (CRITICAL - v0.26.0 - FILE-BASED GUARD)
34
+ # ULTRA-FAST EXITS
41
35
  # ============================================================================
42
- # FIX: Don't update status line from within hook chain
43
- # WHY: Status line writes status-line.json which triggers post-edit-write hook
44
- # which tries to update status line AGAIN → infinite recursion!
45
- #
46
- # This is a CRITICAL part of Fix #4 in the root cause analysis:
47
- # See: .specweave/increments/0051-*/reports/GITHUB-COMMENT-RECURSION-ROOT-CAUSE-2025-11-24.md
36
+ STATE_DIR="$PROJECT_ROOT/.specweave/state"
37
+ CACHE_FILE="$STATE_DIR/status-line.json"
38
+ INCREMENTS_DIR="$PROJECT_ROOT/.specweave/increments"
39
+ LOCK_FILE="$STATE_DIR/.status-update.lock"
48
40
 
49
- RECURSION_GUARD_FILE="$PROJECT_ROOT/.specweave/state/.hook-recursion-guard"
41
+ # No .specweave? Exit immediately
42
+ [[ ! -d "$PROJECT_ROOT/.specweave" ]] && exit 0
50
43
 
51
- if [[ -f "$RECURSION_GUARD_FILE" ]]; then
52
- # We're inside a hook chain - skip status line update to prevent recursion
53
- # This is NORMAL behavior (not an error!)
54
- exit 0
44
+ # Recursion guard
45
+ [[ -f "$STATE_DIR/.hook-recursion-guard" ]] && exit 0
46
+
47
+ # Lock check (prevent concurrent runs - causes crashes!)
48
+ if [[ -f "$LOCK_FILE" ]]; then
49
+ # Check if lock is stale (>30s)
50
+ if [[ "$(uname)" == "Darwin" ]]; then
51
+ LOCK_AGE=$(( $(date +%s) - $(stat -f %m "$LOCK_FILE" 2>/dev/null || echo 0) ))
52
+ else
53
+ LOCK_AGE=$(( $(date +%s) - $(stat -c %Y "$LOCK_FILE" 2>/dev/null || echo 0) ))
54
+ fi
55
+ [[ $LOCK_AGE -lt 30 ]] && exit 0
55
56
  fi
56
57
 
57
- CACHE_FILE="$PROJECT_ROOT/.specweave/state/status-line.json"
58
- INCREMENTS_DIR="$PROJECT_ROOT/.specweave/increments"
59
- TMP_FILE="$PROJECT_ROOT/.specweave/state/.status-line-tmp.txt"
60
-
61
- # Ensure state directory exists
62
- mkdir -p "$PROJECT_ROOT/.specweave/state"
63
-
64
- # Step 1: Find all open increments (active/planning)
65
- # Read from spec.md (source of truth), not metadata.json
66
- # ONLY accepts official IncrementStatus enum values
67
- # Write to temp file: "timestamp increment_id"
68
- > "$TMP_FILE"
69
-
70
- if [[ -d "$INCREMENTS_DIR" ]]; then
71
- for spec_file in "$INCREMENTS_DIR"/*/spec.md; do
72
- if [[ -f "$spec_file" ]]; then
73
- # Parse YAML frontmatter for status (source of truth)
74
- status=$(grep -m1 "^status:" "$spec_file" 2>/dev/null | cut -d: -f2 | tr -d ' ' || echo "")
75
-
76
- # Check if increment is open (active, planning, or in-progress)
77
- # ONLY accepts official IncrementStatus enum values
78
- if [[ "$status" == "active" ]] || [[ "$status" == "planning" ]] || [[ "$status" == "in-progress" ]]; then
79
- increment_id=$(basename "$(dirname "$spec_file")")
80
- # Parse created date from spec.md YAML frontmatter
81
- created=$(grep -m1 "^created:" "$spec_file" 2>/dev/null | cut -d: -f2- | tr -d ' ' || echo "1970-01-01")
82
-
83
- # Write to temp file
84
- echo "$created $increment_id" >> "$TMP_FILE"
85
- fi
86
- fi
87
- done
58
+ # ============================================================================
59
+ # TTL CHECK (10 seconds - balanced for UX vs performance)
60
+ # ============================================================================
61
+ TTL_SECONDS=10
62
+
63
+ if [[ -f "$CACHE_FILE" ]]; then
64
+ if [[ "$(uname)" == "Darwin" ]]; then
65
+ CACHE_AGE=$(( $(date +%s) - $(stat -f %m "$CACHE_FILE" 2>/dev/null || echo 0) ))
66
+ else
67
+ CACHE_AGE=$(( $(date +%s) - $(stat -c %Y "$CACHE_FILE" 2>/dev/null || echo 0) ))
68
+ fi
69
+ [[ $CACHE_AGE -lt $TTL_SECONDS ]] && exit 0
88
70
  fi
89
71
 
90
- # Step 2: Count open increments
91
- OPEN_COUNT=$(wc -l < "$TMP_FILE" | tr -d ' ')
92
-
93
- if [[ $OPEN_COUNT -eq 0 ]]; then
94
- # No open increments
95
- jq -n '{
96
- current: null,
97
- openCount: 0,
98
- lastUpdate: (now | strftime("%Y-%m-%dT%H:%M:%SZ"))
99
- }' > "$CACHE_FILE"
100
- rm -f "$TMP_FILE"
72
+ # ============================================================================
73
+ # NO INCREMENTS? Write empty cache and exit
74
+ # ============================================================================
75
+ if [[ ! -d "$INCREMENTS_DIR" ]]; then
76
+ mkdir -p "$STATE_DIR"
77
+ printf '{"current":null,"openCount":0,"lastUpdate":"%s"}' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$CACHE_FILE"
101
78
  exit 0
102
79
  fi
103
80
 
104
- # Step 3: Sort by timestamp (oldest first) and take first
105
- CURRENT_INCREMENT=$(sort "$TMP_FILE" | head -1 | awk '{print $2}')
81
+ # ============================================================================
82
+ # ACQUIRE LOCK
83
+ # ============================================================================
84
+ mkdir -p "$STATE_DIR"
85
+ echo $$ > "$LOCK_FILE"
86
+ trap 'rm -f "$LOCK_FILE"' EXIT
87
+
88
+ # ============================================================================
89
+ # FIND ACTIVE INCREMENTS (single find, no xargs)
90
+ # ============================================================================
91
+ ACTIVE_FILES=""
92
+ OPEN_COUNT=0
93
+ OLDEST_DATE="9999-99-99"
94
+ CURRENT_INCREMENT=""
106
95
 
107
- # Clean up temp file
108
- rm -f "$TMP_FILE"
96
+ while IFS= read -r spec_file; do
97
+ [[ -z "$spec_file" ]] && continue
109
98
 
110
- # Step 4: Parse current increment's tasks.md for progress
111
- TASKS_FILE="$INCREMENTS_DIR/$CURRENT_INCREMENT/tasks.md"
112
- TOTAL_TASKS=0
113
- COMPLETED_TASKS=0
114
- PERCENTAGE=0
99
+ # Quick status check with head + grep (faster than full file grep)
100
+ if head -20 "$spec_file" 2>/dev/null | grep -qE '^status:\s*(active|planning|in-progress)'; then
101
+ OPEN_COUNT=$((OPEN_COUNT + 1))
102
+ increment_id=$(basename "$(dirname "$spec_file")")
115
103
 
116
- if [[ -f "$TASKS_FILE" ]]; then
117
- # Use TaskCounter CLI for accurate counting (fixes overcounting bug)
118
- COUNT_TASKS_CLI="$PROJECT_ROOT/dist/src/cli/count-tasks.js"
104
+ # Get created date (first 30 lines only)
105
+ created=$(head -30 "$spec_file" 2>/dev/null | grep -m1 "^created:" | cut -d: -f2- | tr -d ' "' || echo "9999-99-99")
119
106
 
120
- if [[ -f "$COUNT_TASKS_CLI" ]]; then
121
- # Call CLI and parse JSON output
122
- TASK_COUNTS=$(node "$COUNT_TASKS_CLI" "$TASKS_FILE" 2>/dev/null || echo '{"total":0,"completed":0,"percentage":0}')
123
- TOTAL_TASKS=$(echo "$TASK_COUNTS" | jq -r '.total' 2>/dev/null || echo 0)
124
- COMPLETED_TASKS=$(echo "$TASK_COUNTS" | jq -r '.completed' 2>/dev/null || echo 0)
125
- PERCENTAGE=$(echo "$TASK_COUNTS" | jq -r '.percentage' 2>/dev/null || echo 0)
126
- else
127
- # Fallback to legacy counting if CLI not available (graceful degradation)
128
- # Count total tasks (## T- or ### T- headings)
129
- TOTAL_TASKS=$(grep -cE '^##+ T-' "$TASKS_FILE" 2>/dev/null || echo 0)
130
- TOTAL_TASKS=$(echo "$TOTAL_TASKS" | tr -d '\n\r ' || echo 0)
131
-
132
- # Count completed tasks - recognize all three completion marker formats:
133
- # 1. **Completed**: <date> (preferred format)
134
- # 2. **Status**: [x] (legacy format)
135
- # 3. [x] at line start (legacy checkbox format)
136
- COMPLETED_TASKS=$(grep -cE '(\*\*Completed\*\*:|\*\*Status\*\*:\s*\[x\]|^\[x\])' "$TASKS_FILE" 2>/dev/null || echo 0)
137
- COMPLETED_TASKS=$(echo "$COMPLETED_TASKS" | tr -d '\n\r ' || echo 0)
138
-
139
- # Calculate percentage
140
- if [[ $TOTAL_TASKS -gt 0 ]]; then
141
- PERCENTAGE=$((COMPLETED_TASKS * 100 / TOTAL_TASKS))
107
+ if [[ "$created" < "$OLDEST_DATE" ]]; then
108
+ OLDEST_DATE="$created"
109
+ CURRENT_INCREMENT="$increment_id"
142
110
  fi
111
+
112
+ ACTIVE_FILES="$ACTIVE_FILES $spec_file"
143
113
  fi
144
- fi
114
+ done < <(find "$INCREMENTS_DIR" -maxdepth 2 -name "spec.md" -not -path "*/_archive/*" 2>/dev/null)
145
115
 
146
- # Step 4b: Parse spec.md for AC (Acceptance Criteria) progress
147
- SPEC_FILE="$INCREMENTS_DIR/$CURRENT_INCREMENT/spec.md"
148
- TOTAL_ACS=0
149
- COMPLETED_ACS=0
150
- OPEN_ACS=0
151
-
152
- if [[ -f "$SPEC_FILE" ]]; then
153
- # Count total ACs: both checked and unchecked
154
- # Pattern: - [ ] **AC- OR - [x] **AC-
155
- TOTAL_ACS=$(grep -cE '^- \[(x| )\] \*\*AC-' "$SPEC_FILE" 2>/dev/null || echo 0)
156
- TOTAL_ACS=$(echo "$TOTAL_ACS" | tr -d '\n\r ' || echo 0)
157
-
158
- # Count completed ACs (checked)
159
- # Pattern: - [x] **AC-
160
- COMPLETED_ACS=$(grep -cE '^- \[x\] \*\*AC-' "$SPEC_FILE" 2>/dev/null || echo 0)
161
- COMPLETED_ACS=$(echo "$COMPLETED_ACS" | tr -d '\n\r ' || echo 0)
162
-
163
- # Count open ACs (unchecked)
164
- # Pattern: - [ ] **AC-
165
- OPEN_ACS=$(grep -cE '^- \[ \] \*\*AC-' "$SPEC_FILE" 2>/dev/null || echo 0)
166
- OPEN_ACS=$(echo "$OPEN_ACS" | tr -d '\n\r ' || echo 0)
116
+ # No active increments?
117
+ if [[ -z "$CURRENT_INCREMENT" ]]; then
118
+ printf '{"current":null,"openCount":0,"lastUpdate":"%s"}' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$CACHE_FILE"
119
+ exit 0
167
120
  fi
168
121
 
169
- # Step 5: Extract increment ID and name
170
- # Format: XXXX-name where XXXX is 4-digit prefix (brackets added by manager)
171
- INCREMENT_ID=$(echo "$CURRENT_INCREMENT" | grep -oE '^[0-9]{4}')
172
- INCREMENT_NAME_ONLY=$(echo "$CURRENT_INCREMENT" | sed 's/^[0-9]\{4\}-//')
173
- INCREMENT_NAME="$INCREMENT_ID-$INCREMENT_NAME_ONLY"
174
-
175
- # Step 6: Write cache with atomic validation (v0.24.4 - prevents corruption)
176
- TMP_CACHE_FILE="$PROJECT_ROOT/.specweave/state/.status-line-tmp.json"
177
-
178
- # Validate all numeric inputs before jq (prevents jq failures)
179
- [[ "$TOTAL_TASKS" =~ ^[0-9]+$ ]] || TOTAL_TASKS=0
180
- [[ "$COMPLETED_TASKS" =~ ^[0-9]+$ ]] || COMPLETED_TASKS=0
181
- [[ "$PERCENTAGE" =~ ^[0-9]+$ ]] || PERCENTAGE=0
182
- [[ "$TOTAL_ACS" =~ ^[0-9]+$ ]] || TOTAL_ACS=0
183
- [[ "$COMPLETED_ACS" =~ ^[0-9]+$ ]] || COMPLETED_ACS=0
184
- [[ "$OPEN_ACS" =~ ^[0-9]+$ ]] || OPEN_ACS=0
185
- [[ "$OPEN_COUNT" =~ ^[0-9]+$ ]] || OPEN_COUNT=0
186
-
187
- # Generate cache to temp file first (atomic operation)
188
- if jq -n \
189
- --arg id "$CURRENT_INCREMENT" \
190
- --arg name "$INCREMENT_NAME" \
191
- --argjson completed "$COMPLETED_TASKS" \
192
- --argjson total "$TOTAL_TASKS" \
193
- --argjson percentage "$PERCENTAGE" \
194
- --argjson acsCompleted "$COMPLETED_ACS" \
195
- --argjson acsTotal "$TOTAL_ACS" \
196
- --argjson openCount "$OPEN_COUNT" \
197
- '{
198
- current: {
199
- id: $id,
200
- name: $name,
201
- completed: $completed,
202
- total: $total,
203
- percentage: $percentage,
204
- acsCompleted: $acsCompleted,
205
- acsTotal: $acsTotal
206
- },
207
- openCount: $openCount,
208
- lastUpdate: (now | strftime("%Y-%m-%dT%H:%M:%SZ"))
209
- }' > "$TMP_CACHE_FILE" 2>/dev/null; then
210
-
211
- # Validate generated JSON before replacing cache (corruption prevention)
212
- if jq empty "$TMP_CACHE_FILE" 2>/dev/null; then
213
- mv "$TMP_CACHE_FILE" "$CACHE_FILE"
214
- else
215
- # Invalid JSON generated - keep old cache
216
- echo "[$(date)] ERROR: Generated invalid JSON, keeping old cache" >> "$DEBUG_LOG" 2>/dev/null || true
217
- rm -f "$TMP_CACHE_FILE"
122
+ # ============================================================================
123
+ # MTIME CHECK (using find -newer - single syscall!)
124
+ # ============================================================================
125
+ MTIME_FILE="$STATE_DIR/.status-mtime-$CURRENT_INCREMENT"
126
+
127
+ if [[ -f "$MTIME_FILE" ]] && [[ -f "$CACHE_FILE" ]]; then
128
+ # Check if any relevant files are newer than our marker
129
+ TASKS_FILE="$INCREMENTS_DIR/$CURRENT_INCREMENT/tasks.md"
130
+ SPEC_FILE="$INCREMENTS_DIR/$CURRENT_INCREMENT/spec.md"
131
+
132
+ NEWER_FILES=$(find "$SPEC_FILE" "$TASKS_FILE" -newer "$MTIME_FILE" 2>/dev/null | head -1)
133
+
134
+ if [[ -z "$NEWER_FILES" ]]; then
135
+ # No changes - just touch cache to reset TTL
136
+ touch "$CACHE_FILE"
137
+ exit 0
218
138
  fi
219
- else
220
- # jq generation failed - keep old cache
221
- echo "[$(date)] ERROR: jq failed to generate status cache (exit $?)" >> "$DEBUG_LOG" 2>/dev/null || true
222
- rm -f "$TMP_CACHE_FILE"
223
139
  fi
224
140
 
141
+ # ============================================================================
142
+ # SINGLE-PASS COUNTING WITH AWK (replaces 5 grep calls!)
143
+ # ============================================================================
144
+ TASKS_FILE="$INCREMENTS_DIR/$CURRENT_INCREMENT/tasks.md"
145
+ SPEC_FILE="$INCREMENTS_DIR/$CURRENT_INCREMENT/spec.md"
146
+
147
+ # Count tasks with single awk call
148
+ read -r TOTAL_TASKS COMPLETED_TASKS < <(
149
+ awk '
150
+ /^###? T-/ { total++ }
151
+ /\*\*Completed\*\*:|\*\*Status\*\*:[ \t]*\[x\]/ { completed++ }
152
+ END { print total+0, completed+0 }
153
+ ' "$TASKS_FILE" 2>/dev/null || echo "0 0"
154
+ )
155
+
156
+ # Count ACs with single awk call
157
+ # Supports both formats: "- [ ] AC-US1-01:" and "- [ ] **AC-US1-01**:"
158
+ read -r TOTAL_ACS COMPLETED_ACS < <(
159
+ awk '
160
+ /^- \[(x| )\] (\*\*)?AC-/ { total++ }
161
+ /^- \[x\] (\*\*)?AC-/ { completed++ }
162
+ END { print total+0, completed+0 }
163
+ ' "$SPEC_FILE" 2>/dev/null || echo "0 0"
164
+ )
165
+
166
+ # Calculate percentage (pure bash)
167
+ PERCENTAGE=0
168
+ [[ ${TOTAL_TASKS:-0} -gt 0 ]] && PERCENTAGE=$((${COMPLETED_TASKS:-0} * 100 / TOTAL_TASKS))
169
+
170
+ # ============================================================================
171
+ # WRITE CACHE (PURE BASH - NO jq!)
172
+ # ============================================================================
173
+ # Sanitize values
174
+ TOTAL_TASKS=${TOTAL_TASKS:-0}
175
+ COMPLETED_TASKS=${COMPLETED_TASKS:-0}
176
+ TOTAL_ACS=${TOTAL_ACS:-0}
177
+ COMPLETED_ACS=${COMPLETED_ACS:-0}
178
+ OPEN_COUNT=${OPEN_COUNT:-0}
179
+ PERCENTAGE=${PERCENTAGE:-0}
180
+
181
+ # Generate JSON directly (avoids jq subprocess entirely!)
182
+ cat > "$CACHE_FILE" << EOF
183
+ {"current":{"id":"$CURRENT_INCREMENT","name":"$CURRENT_INCREMENT","completed":$COMPLETED_TASKS,"total":$TOTAL_TASKS,"percentage":$PERCENTAGE,"acsCompleted":$COMPLETED_ACS,"acsTotal":$TOTAL_ACS},"openCount":$OPEN_COUNT,"lastUpdate":"$(date -u +%Y-%m-%dT%H:%M:%SZ)"}
184
+ EOF
185
+
186
+ # Update mtime marker
187
+ touch "$MTIME_FILE"
188
+
225
189
  exit 0
@@ -49,6 +49,12 @@ find_project_root() {
49
49
  }
50
50
 
51
51
  PROJECT_ROOT=$(find_project_root)
52
+
53
+ # ULTRA-FAST EARLY EXIT FOR NON-SPECWEAVE PROJECTS (T-006 - v0.26.15)
54
+ if [[ ! -d "$PROJECT_ROOT/.specweave" ]]; then
55
+ exit 0
56
+ fi
57
+
52
58
  LOGS_DIR="$PROJECT_ROOT/.specweave/logs"
53
59
  DEBUG_LOG="$LOGS_DIR/hooks-debug.log"
54
60
 
@@ -192,6 +192,15 @@ case "$CURRENT_STATUS" in
192
192
  fi
193
193
  ;;
194
194
 
195
+ active|planning|in-progress)
196
+ # Increment became active - MUST register in active-increment.json!
197
+ # CRITICAL FIX (v0.26.15): post-task-completion.sh depends on this file
198
+ # Without registration, ALL sync operations are skipped!
199
+ echo "[$(date)] post-metadata-change: Status is $CURRENT_STATUS - registering as active + updating status line" >> "$DEBUG_LOG" 2>/dev/null || true
200
+ bash "$HOOK_DIR/lib/update-active-increment.sh" 2>/dev/null || true
201
+ bash "$HOOK_DIR/lib/update-status-line.sh" 2>/dev/null || true
202
+ ;;
203
+
195
204
  *)
196
205
  # Other metadata changes (e.g., task completion count, AC count)
197
206
  # Just update status line to reflect new progress
@@ -66,6 +66,14 @@ find_project_root() {
66
66
  PROJECT_ROOT="$(find_project_root "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)")"
67
67
  cd "$PROJECT_ROOT" 2>/dev/null || true
68
68
 
69
+ # ============================================================================
70
+ # ULTRA-FAST EARLY EXIT FOR NON-SPECWEAVE PROJECTS (T-006 - v0.26.15)
71
+ # ============================================================================
72
+ # Skip ALL processing if not a SpecWeave project - saves ~50-100ms
73
+ if [[ ! -d "$PROJECT_ROOT/.specweave" ]]; then
74
+ exit 0
75
+ fi
76
+
69
77
  # ============================================================================
70
78
  # RECURSION PREVENTION (CRITICAL - v0.26.0 - FILE-BASED GUARD)
71
79
  # ============================================================================
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # post-task-edit.sh (v0.26.16)
4
+ #
5
+ # Lightweight hook triggered after Edit on tasks.md
6
+ # ONLY updates status line cache - no heavy processing!
7
+ #
8
+ # Purpose: Keep status line in sync when tasks are marked complete via Edit
9
+ #
10
+ # Performance target: <20ms (just calls update-status-line.sh)
11
+ #
12
+ set +e
13
+
14
+ # EMERGENCY KILL SWITCH
15
+ [[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && exit 0
16
+
17
+ # Find hook directory
18
+ HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+
20
+ # Consume stdin (required for PostToolUse hooks)
21
+ cat > /dev/null
22
+
23
+ # Update status line cache (force refresh by removing mtime marker)
24
+ PROJECT_ROOT="$PWD"
25
+ while [[ "$PROJECT_ROOT" != "/" ]] && [[ ! -d "$PROJECT_ROOT/.specweave" ]]; do
26
+ PROJECT_ROOT=$(dirname "$PROJECT_ROOT")
27
+ done
28
+
29
+ if [[ -d "$PROJECT_ROOT/.specweave" ]]; then
30
+ # Remove mtime markers to force full refresh
31
+ rm -f "$PROJECT_ROOT/.specweave/state/.status-mtime-"* 2>/dev/null || true
32
+
33
+ # Update status line
34
+ bash "$HOOK_DIR/lib/update-status-line.sh" 2>/dev/null || true
35
+ fi
36
+
37
+ exit 0
@@ -1,83 +1,73 @@
1
1
  #!/bin/bash
2
2
 
3
- # SpecWeave Pre-Command Deduplication Hook
3
+ # SpecWeave Pre-Command Deduplication Hook (v0.26.14 - OPTIMIZED)
4
4
  # Fires BEFORE any command executes (UserPromptSubmit hook)
5
5
  # Purpose: Prevent duplicate command invocations within configurable time window
6
+ #
7
+ # OPTIMIZATIONS (v0.26.14):
8
+ # 1. Early exit for non-SpecWeave projects (<1ms)
9
+ # 2. Skip node spawn if deduplicator not available
10
+ # 3. Pure bash stdin reading
6
11
 
7
- set +e # EMERGENCY FIX: Changed from set -euo pipefail to prevent Claude Code crashes
12
+ set +e
8
13
 
9
14
  # ==============================================================================
10
- # PROJECT ROOT DETECTION
15
+ # ULTRA-FAST EARLY EXIT
11
16
  # ==============================================================================
12
17
 
13
- # Find project root by searching upward for .specweave/ directory
14
- find_project_root() {
15
- local dir="$1"
16
- while [ "$dir" != "/" ]; do
17
- if [ -d "$dir/.specweave" ]; then
18
- echo "$dir"
19
- return 0
20
- fi
21
- dir="$(dirname "$dir")"
22
- done
23
- # Fallback: try current directory
24
- if [ -d "$(pwd)/.specweave" ]; then
25
- pwd
26
- else
27
- echo "$(pwd)"
28
- fi
29
- }
30
-
31
- PROJECT_ROOT="$(find_project_root "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)")"
32
- cd "$PROJECT_ROOT" 2>/dev/null || true
18
+ # Quick check: If no .specweave in cwd or nearby, just approve
19
+ if [[ ! -d ".specweave" ]] && [[ ! -d "../.specweave" ]] && [[ ! -d "../../.specweave" ]]; then
20
+ cat /dev/stdin > /dev/null # Drain stdin
21
+ echo '{"decision":"approve"}'
22
+ exit 0
23
+ fi
33
24
 
34
25
  # Read input JSON from stdin
35
26
  INPUT=$(cat)
36
27
 
37
28
  # ==============================================================================
38
- # DEDUPLICATION CHECK: Block duplicate commands within 1 second
29
+ # DEDUPLICATION CHECK: Pure bash with file-based cache
39
30
  # ==============================================================================
40
31
 
41
- # Check if deduplication module is available
42
- if command -v node >/dev/null 2>&1 && [[ -f "dist/src/core/deduplication/command-deduplicator.js" ]]; then
43
- # Use dedicated wrapper script for ES module compatibility
44
- DEDUP_RESULT=$(echo "$INPUT" | node scripts/check-deduplication.js 2>/dev/null || echo "OK")
45
-
46
- # Parse result
47
- STATUS=$(echo "$DEDUP_RESULT" | head -1)
32
+ # Use file-based deduplication (no node!) with 1-second TTL
33
+ CACHE_DIR=".specweave/state/.dedup-cache"
34
+ mkdir -p "$CACHE_DIR" 2>/dev/null || true
48
35
 
49
- if [[ "$STATUS" == "DUPLICATE" ]]; then
50
- # Get stats
51
- STATS=$(echo "$DEDUP_RESULT" | tail -1)
36
+ # Extract command from input (use jq if available, fallback to grep)
37
+ if command -v jq >/dev/null 2>&1; then
38
+ COMMAND=$(echo "$INPUT" | jq -r '.prompt // ""' 2>/dev/null | head -c 100 | tr -d '\n' | tr '/' '_' | tr ' ' '_')
39
+ else
40
+ COMMAND=$(echo "$INPUT" | grep -oP '"prompt"\s*:\s*"\K[^"]{0,100}' 2>/dev/null | tr '/' '_' | tr ' ' '_')
41
+ fi
52
42
 
53
- # Extract command and stats for readable message
54
- COMMAND=$(echo "$STATS" | grep -o '"lastCommand":"[^"]*"' | cut -d'"' -f4 || echo "unknown")
55
- TOTAL_BLOCKED=$(echo "$STATS" | grep -o '"totalDuplicatesBlocked":[0-9]*' | cut -d':' -f2 || echo "1")
56
- CACHE_SIZE=$(echo "$STATS" | grep -o '"currentCacheSize":[0-9]*' | cut -d':' -f2 || echo "1")
43
+ # Only check for SpecWeave commands
44
+ if [[ "$COMMAND" == *specweave* ]]; then
45
+ CACHE_FILE="$CACHE_DIR/${COMMAND:0:50}.lock"
57
46
 
58
- # Build error message WITHOUT embedding JSON (avoid escaping issues)
59
- MESSAGE=$(cat <<'EOF'
47
+ # Check if cached and recent (within 1 second)
48
+ if [[ -f "$CACHE_FILE" ]]; then
49
+ CACHE_AGE=$(($(date +%s) - $(stat -f %m "$CACHE_FILE" 2>/dev/null || stat -c %Y "$CACHE_FILE" 2>/dev/null || echo 0)))
50
+ if [[ "$CACHE_AGE" -lt 1 ]]; then
51
+ cat <<EOF
60
52
  {
61
53
  "decision": "block",
62
- "reason": "🚫 DUPLICATE COMMAND DETECTED\n\nCommand: `COMMAND_PLACEHOLDER`\nTime window: 1 second\n\nThis command was just executed! To prevent unintended duplicates, this invocation has been blocked.\n\n💡 If you meant to run this command again:\n 1. Wait 1 second\n 2. Run the command again\n\nDeduplication Stats:\n- Total duplicates blocked: BLOCKED_PLACEHOLDER\n- Commands in cache: CACHE_PLACEHOLDER"
54
+ "reason": "Duplicate command detected (within 1 second). Wait a moment and try again."
63
55
  }
64
56
  EOF
65
- )
66
- # Replace placeholders (avoids JSON escaping issues)
67
- # Use | as sed delimiter to avoid conflicts with / in command names
68
- echo "$MESSAGE" | sed "s|COMMAND_PLACEHOLDER|$COMMAND|g" | sed "s|BLOCKED_PLACEHOLDER|$TOTAL_BLOCKED|g" | sed "s|CACHE_PLACEHOLDER|$CACHE_SIZE|g"
69
- exit 0
57
+ exit 0
58
+ fi
70
59
  fi
60
+
61
+ # Update cache timestamp
62
+ touch "$CACHE_FILE" 2>/dev/null || true
63
+
64
+ # Cleanup old cache files (>10 seconds old)
65
+ find "$CACHE_DIR" -type f -mmin +1 -delete 2>/dev/null &
71
66
  fi
72
67
 
73
68
  # ==============================================================================
74
- # PASS THROUGH: No duplicate detected, proceed with command
69
+ # PASS THROUGH: No duplicate detected
75
70
  # ==============================================================================
76
71
 
77
- cat <<EOF
78
- {
79
- "decision": "approve"
80
- }
81
- EOF
82
-
72
+ echo '{"decision":"approve"}'
83
73
  exit 0
@@ -45,6 +45,11 @@ find_project_root() {
45
45
  PROJECT_ROOT="$(find_project_root "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)")"
46
46
  cd "$PROJECT_ROOT" 2>/dev/null || true
47
47
 
48
+ # ULTRA-FAST EARLY EXIT FOR NON-SPECWEAVE PROJECTS (T-006 - v0.26.15)
49
+ if [[ ! -d "$PROJECT_ROOT/.specweave" ]]; then
50
+ exit 0
51
+ fi
52
+
48
53
  LOGS_DIR=".specweave/logs"
49
54
  DEBUG_LOG="$LOGS_DIR/hooks-debug.log"
50
55