specweave 1.0.30 → 1.0.32
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 +140 -1235
- package/bin/specweave.js +23 -0
- package/dist/plugins/specweave-github/lib/github-client-v2.d.ts +3 -0
- package/dist/plugins/specweave-github/lib/github-client-v2.d.ts.map +1 -1
- package/dist/plugins/specweave-github/lib/github-client-v2.js +39 -0
- package/dist/plugins/specweave-github/lib/github-client-v2.js.map +1 -1
- package/dist/src/cli/commands/set-sync-target.d.ts +41 -0
- package/dist/src/cli/commands/set-sync-target.d.ts.map +1 -0
- package/dist/src/cli/commands/set-sync-target.js +126 -0
- package/dist/src/cli/commands/set-sync-target.js.map +1 -0
- package/dist/src/cli/helpers/issue-tracker/github-multi-repo.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/github-multi-repo.js +5 -8
- package/dist/src/cli/helpers/issue-tracker/github-multi-repo.js.map +1 -1
- package/dist/src/core/hooks/HookScanner.d.ts +32 -0
- package/dist/src/core/hooks/HookScanner.d.ts.map +1 -1
- package/dist/src/core/hooks/HookScanner.js +125 -1
- package/dist/src/core/hooks/HookScanner.js.map +1 -1
- package/dist/src/core/hooks/types.d.ts +10 -1
- package/dist/src/core/hooks/types.d.ts.map +1 -1
- package/dist/src/core/increment/metadata-manager.d.ts +67 -1
- package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
- package/dist/src/core/increment/metadata-manager.js +93 -0
- package/dist/src/core/increment/metadata-manager.js.map +1 -1
- package/dist/src/core/project/index.d.ts +21 -0
- package/dist/src/core/project/index.d.ts.map +1 -0
- package/dist/src/core/project/index.js +22 -0
- package/dist/src/core/project/index.js.map +1 -0
- package/dist/src/core/project/project-service.d.ts +122 -0
- package/dist/src/core/project/project-service.d.ts.map +1 -0
- package/dist/src/core/project/project-service.js +334 -0
- package/dist/src/core/project/project-service.js.map +1 -0
- package/dist/src/core/sync/external-tool-resolver.d.ts +171 -0
- package/dist/src/core/sync/external-tool-resolver.d.ts.map +1 -0
- package/dist/src/core/sync/external-tool-resolver.js +569 -0
- package/dist/src/core/sync/external-tool-resolver.js.map +1 -0
- package/dist/src/core/types/increment-metadata.d.ts +92 -0
- package/dist/src/core/types/increment-metadata.d.ts.map +1 -1
- package/dist/src/hooks/processor.d.ts +7 -3
- package/dist/src/hooks/processor.d.ts.map +1 -1
- package/dist/src/hooks/processor.js +11 -5
- package/dist/src/hooks/processor.js.map +1 -1
- package/package.json +1 -1
- package/plugins/specweave/hooks/hooks.json +0 -69
- package/plugins/specweave/hooks/v2/handlers/project-bridge-handler.sh +96 -0
- package/plugins/specweave/hooks/v2/queue/processor.sh +13 -5
- package/plugins/specweave/lib/hooks/project-bridge.js +76 -0
- package/plugins/specweave/lib/hooks/update-tasks-md.js +0 -0
- package/plugins/specweave/lib/hooks/us-completion-orchestrator.js +0 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.d.ts +67 -1
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js +93 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js.map +1 -1
- package/plugins/specweave/lib/vendor/core/types/increment-metadata.d.ts +92 -0
- package/plugins/specweave-github/lib/github-client-v2.js +39 -0
- package/plugins/specweave-github/lib/github-client-v2.ts +44 -0
- package/plugins/specweave/hooks/docs-changed.sh +0 -87
- package/plugins/specweave/hooks/human-input-required.sh +0 -83
- package/plugins/specweave/hooks/post-edit-write-consolidated.sh +0 -428
- package/plugins/specweave/hooks/post-first-increment.sh +0 -61
- package/plugins/specweave/hooks/post-increment-change.sh +0 -103
- package/plugins/specweave/hooks/post-increment-completion.sh +0 -513
- package/plugins/specweave/hooks/post-increment-planning.sh +0 -1204
- package/plugins/specweave/hooks/post-increment-status-change.sh +0 -243
- package/plugins/specweave/hooks/post-metadata-change.sh +0 -246
- package/plugins/specweave/hooks/post-spec-update.sh +0 -158
- package/plugins/specweave/hooks/post-task-completion.sh +0 -557
- package/plugins/specweave/hooks/post-task-edit.sh +0 -47
- package/plugins/specweave/hooks/post-user-story-complete.sh +0 -230
- package/plugins/specweave/hooks/pre-command-deduplication.sh +0 -68
- package/plugins/specweave/hooks/pre-edit-write-consolidated.sh +0 -225
- package/plugins/specweave/hooks/pre-implementation.sh +0 -75
- package/plugins/specweave/hooks/pre-increment-start.sh +0 -173
- package/plugins/specweave/hooks/pre-task-completion-edit.sh +0 -355
- package/plugins/specweave/hooks/pre-task-completion.sh +0 -269
- package/plugins/specweave/hooks/pre-tool-use.sh +0 -137
- package/plugins/specweave/hooks/session-start-reconcile.sh +0 -139
- package/plugins/specweave/hooks/shared/bulk-operation-detector.sh +0 -167
- package/plugins/specweave/hooks/test-pretooluse-env.sh +0 -72
- package/plugins/specweave/hooks/validate-increment-completion.sh +0 -113
- package/plugins/specweave/lib/hooks/consolidated-sync.js +0 -288
|
@@ -1,557 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
# SpecWeave Post-Task-Completion Hook
|
|
4
|
-
# Runs automatically after ANY task is marked complete via TodoWrite
|
|
5
|
-
#
|
|
6
|
-
# SMART SESSION-END DETECTION (v2.0):
|
|
7
|
-
# =====================================
|
|
8
|
-
# Problem: Claude creates multiple todo lists in one conversation
|
|
9
|
-
# - List 1: [A, B, C] → completes → sound plays
|
|
10
|
-
# - List 2: [D, E] → completes 30s later → sound plays again
|
|
11
|
-
# - User hears sounds while Claude is still working!
|
|
12
|
-
#
|
|
13
|
-
# Solution: Inactivity-based detection
|
|
14
|
-
# - Track time gaps BETWEEN TodoWrite calls
|
|
15
|
-
# - If all tasks complete AND gap > INACTIVITY_THRESHOLD (15s)
|
|
16
|
-
# → Session is winding down → Play sound
|
|
17
|
-
# - If rapid completions (gap < threshold)
|
|
18
|
-
# → Claude still actively working → Skip sound
|
|
19
|
-
#
|
|
20
|
-
# Example:
|
|
21
|
-
# 10:00:00 - Task done (gap: 5s) → skip sound
|
|
22
|
-
# 10:00:05 - Task done (gap: 5s) → skip sound
|
|
23
|
-
# 10:00:10 - All done (gap: 5s) → skip sound (rapid work)
|
|
24
|
-
# ...
|
|
25
|
-
# 10:01:00 - All done (gap: 50s) → PLAY SOUND! (session ending)
|
|
26
|
-
#
|
|
27
|
-
# DEBOUNCING: Prevents duplicate fires (Claude Code calls hooks twice)
|
|
28
|
-
|
|
29
|
-
# EMERGENCY FIXES (v0.24.3): CRITICAL SAFETY FIRST
|
|
30
|
-
# - Remove set -e completely to prevent any errors from propagating
|
|
31
|
-
# - Kill switch: SPECWEAVE_DISABLE_HOOKS=1 disables all hooks
|
|
32
|
-
# - Circuit breaker: Auto-disable after 3 consecutive failures
|
|
33
|
-
# - File locking: Only 1 instance can run at a time
|
|
34
|
-
# - Aggressive debouncing: 5 seconds (was 5s, keeping it)
|
|
35
|
-
# - Complete error isolation: ALL background work wrapped
|
|
36
|
-
|
|
37
|
-
set +e # NEVER use set -e in hooks - it causes crashes
|
|
38
|
-
|
|
39
|
-
# ============================================================================
|
|
40
|
-
# PROJECT ROOT DETECTION (MUST BE FIRST - v0.26.1 FIX)
|
|
41
|
-
# ============================================================================
|
|
42
|
-
# CRITICAL FIX: PROJECT_ROOT must be defined BEFORE recursion guard creation
|
|
43
|
-
# BUG (v0.26.0): Used $PROJECT_ROOT on line 71 but defined it on line 112
|
|
44
|
-
# RESULT: Guard file created at wrong path (/.specweave/...) → recursion not prevented
|
|
45
|
-
# See: Incident 2025-11-24 (3x PreToolUse hook fires, Claude Code crash)
|
|
46
|
-
|
|
47
|
-
# Find project root by searching upward for .specweave/ directory
|
|
48
|
-
# Works regardless of where hook is installed (source or .claude/hooks/)
|
|
49
|
-
find_project_root() {
|
|
50
|
-
local dir="$1"
|
|
51
|
-
while [ "$dir" != "/" ]; do
|
|
52
|
-
if [ -d "$dir/.specweave" ]; then
|
|
53
|
-
echo "$dir"
|
|
54
|
-
return 0
|
|
55
|
-
fi
|
|
56
|
-
dir="$(dirname "$dir")"
|
|
57
|
-
done
|
|
58
|
-
# Fallback: try current directory
|
|
59
|
-
if [ -d "$(pwd)/.specweave" ]; then
|
|
60
|
-
pwd
|
|
61
|
-
else
|
|
62
|
-
echo "$(pwd)"
|
|
63
|
-
fi
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
PROJECT_ROOT="$(find_project_root "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)")"
|
|
67
|
-
cd "$PROJECT_ROOT" 2>/dev/null || true
|
|
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
|
-
|
|
77
|
-
# ============================================================================
|
|
78
|
-
# RECURSION PREVENTION (CRITICAL - v0.26.0 - FILE-BASED GUARD)
|
|
79
|
-
# ============================================================================
|
|
80
|
-
# PROBLEM: Hooks that write files trigger other hooks, causing infinite loops.
|
|
81
|
-
# OLD SOLUTION (v0.25.1): Environment variable SPECWEAVE_IN_HOOK=1
|
|
82
|
-
# WHY IT FAILED: Background processes (&) create NEW shells that don't inherit env vars!
|
|
83
|
-
#
|
|
84
|
-
# NEW SOLUTION (v0.26.0): File-based recursion guard
|
|
85
|
-
# - Guard file exists = already inside hook chain
|
|
86
|
-
# - Works across ALL processes (not just current shell)
|
|
87
|
-
# - Atomic operation (mkdir -p ensures thread safety)
|
|
88
|
-
# - Cleanup guaranteed by trap EXIT
|
|
89
|
-
#
|
|
90
|
-
# Example infinite loop (BEFORE fix):
|
|
91
|
-
# TodoWrite → post-task-completion.sh (sets SPECWEAVE_IN_HOOK=1)
|
|
92
|
-
# → spawns background process (&)
|
|
93
|
-
# → consolidated-sync.js (SPECWEAVE_IN_HOOK=0! lost!)
|
|
94
|
-
# → fs.writeFile(tasks.md)
|
|
95
|
-
# → post-edit-write-consolidated.sh (guard fails!)
|
|
96
|
-
# → INFINITE RECURSION → 27 duplicate GitHub comments!
|
|
97
|
-
#
|
|
98
|
-
# With file-based guard (AFTER fix):
|
|
99
|
-
# TodoWrite → post-task-completion.sh (creates .hook-recursion-guard file)
|
|
100
|
-
# → spawns background process (&)
|
|
101
|
-
# → consolidated-sync.js (inherits guard file!)
|
|
102
|
-
# → fs.writeFile(tasks.md)
|
|
103
|
-
# → post-edit-write-consolidated.sh (checks file, exits)
|
|
104
|
-
# → NO RECURSION ✅
|
|
105
|
-
#
|
|
106
|
-
# See: .specweave/increments/0051-*/reports/GITHUB-COMMENT-RECURSION-ROOT-CAUSE-2025-11-24.md
|
|
107
|
-
# See: ADR-0073 (Hook Recursion Prevention Strategy)
|
|
108
|
-
|
|
109
|
-
RECURSION_GUARD_FILE="$PROJECT_ROOT/.specweave/state/.hook-recursion-guard"
|
|
110
|
-
|
|
111
|
-
if [[ -f "$RECURSION_GUARD_FILE" ]]; then
|
|
112
|
-
# Silent exit - we're already inside a hook chain
|
|
113
|
-
echo "[$(date)] ⏭️ Recursion guard detected - skipping (already in hook)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
114
|
-
exit 0
|
|
115
|
-
fi
|
|
116
|
-
|
|
117
|
-
# Create guard file (atomic operation)
|
|
118
|
-
mkdir -p "$PROJECT_ROOT/.specweave/state" 2>/dev/null || true
|
|
119
|
-
touch "$RECURSION_GUARD_FILE"
|
|
120
|
-
|
|
121
|
-
# Ensure guard file is ALWAYS removed when script exits (even on error)
|
|
122
|
-
trap 'rm -f "$RECURSION_GUARD_FILE" 2>/dev/null || true' EXIT SIGINT SIGTERM
|
|
123
|
-
|
|
124
|
-
echo "[$(date)] 🔒 Recursion guard created" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
125
|
-
|
|
126
|
-
# EMERGENCY KILL SWITCH (after recursion guard)
|
|
127
|
-
if [[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]]; then
|
|
128
|
-
exit 0
|
|
129
|
-
fi
|
|
130
|
-
|
|
131
|
-
# ============================================================================
|
|
132
|
-
# EMERGENCY SAFETY CHECKS
|
|
133
|
-
# ============================================================================
|
|
134
|
-
|
|
135
|
-
# CIRCUIT BREAKER: Auto-disable after consecutive failures
|
|
136
|
-
CIRCUIT_BREAKER_FILE=".specweave/state/.hook-circuit-breaker"
|
|
137
|
-
CIRCUIT_BREAKER_THRESHOLD=3
|
|
138
|
-
|
|
139
|
-
mkdir -p ".specweave/state" 2>/dev/null || true
|
|
140
|
-
|
|
141
|
-
if [[ -f "$CIRCUIT_BREAKER_FILE" ]]; then
|
|
142
|
-
FAILURE_COUNT=$(cat "$CIRCUIT_BREAKER_FILE" 2>/dev/null || echo 0)
|
|
143
|
-
if (( FAILURE_COUNT >= CIRCUIT_BREAKER_THRESHOLD )); then
|
|
144
|
-
# Circuit breaker is OPEN - hooks are disabled
|
|
145
|
-
exit 0
|
|
146
|
-
fi
|
|
147
|
-
fi
|
|
148
|
-
|
|
149
|
-
# FILE LOCK: Skip - lock moved INSIDE background subshell (v0.24.4 fix)
|
|
150
|
-
# Why: Lock must protect the ACTUAL work, not just the script startup
|
|
151
|
-
# Old behavior: Lock released when main script exits (before background work completes)
|
|
152
|
-
# New behavior: Lock held until background work completes (inside subshell)
|
|
153
|
-
# This prevents race conditions where rapid TodoWrite calls spawn multiple concurrent processes
|
|
154
|
-
|
|
155
|
-
# ============================================================================
|
|
156
|
-
# CONFIGURATION
|
|
157
|
-
# ============================================================================
|
|
158
|
-
|
|
159
|
-
# Debounce window to prevent duplicate hook fires
|
|
160
|
-
# AGGRESSIVE: 5 seconds to prevent rapid-fire executions
|
|
161
|
-
DEBOUNCE_SECONDS=5
|
|
162
|
-
|
|
163
|
-
# Inactivity threshold to detect session end
|
|
164
|
-
# If gap between TodoWrite calls > this value, assume session is ending
|
|
165
|
-
INACTIVITY_THRESHOLD=120 # seconds (2 minutes - increased from 15s to reduce false positives)
|
|
166
|
-
|
|
167
|
-
# File paths
|
|
168
|
-
LOGS_DIR=".specweave/logs"
|
|
169
|
-
LAST_FIRE_FILE="$LOGS_DIR/last-hook-fire"
|
|
170
|
-
LAST_TODOWRITE_FILE="$LOGS_DIR/last-todowrite-time"
|
|
171
|
-
DEBUG_LOG="$LOGS_DIR/hooks-debug.log"
|
|
172
|
-
TASKS_LOG="$LOGS_DIR/tasks.log"
|
|
173
|
-
|
|
174
|
-
mkdir -p "$LOGS_DIR" 2>/dev/null || true
|
|
175
|
-
|
|
176
|
-
# Log rotation: Keep tasks.log under 100KB (keep last 200 lines)
|
|
177
|
-
if [[ -f "$TASKS_LOG" ]] && [[ $(wc -c < "$TASKS_LOG" 2>/dev/null || echo 0) -gt 102400 ]]; then
|
|
178
|
-
tail -200 "$TASKS_LOG" > "$TASKS_LOG.tmp" 2>/dev/null || true
|
|
179
|
-
mv "$TASKS_LOG.tmp" "$TASKS_LOG" 2>/dev/null || true
|
|
180
|
-
echo "[$(date)] Log rotated (was >100KB)" >> "$TASKS_LOG" 2>/dev/null || true
|
|
181
|
-
fi
|
|
182
|
-
|
|
183
|
-
# ============================================================================
|
|
184
|
-
# DEBOUNCING
|
|
185
|
-
# ============================================================================
|
|
186
|
-
|
|
187
|
-
CURRENT_TIME=$(date +%s)
|
|
188
|
-
|
|
189
|
-
# Skip if hook fired within last N seconds (prevents duplicates)
|
|
190
|
-
if [ -f "$LAST_FIRE_FILE" ]; then
|
|
191
|
-
LAST_FIRE=$(cat "$LAST_FIRE_FILE" 2>/dev/null || echo "0")
|
|
192
|
-
TIME_DIFF=$((CURRENT_TIME - LAST_FIRE))
|
|
193
|
-
|
|
194
|
-
if [ "$TIME_DIFF" -lt "$DEBOUNCE_SECONDS" ]; then
|
|
195
|
-
echo "[$(date)] ⏭️ Debounced (last fire: ${TIME_DIFF}s ago)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
196
|
-
echo '{"continue":true}'
|
|
197
|
-
exit 0
|
|
198
|
-
fi
|
|
199
|
-
fi
|
|
200
|
-
|
|
201
|
-
echo "$CURRENT_TIME" > "$LAST_FIRE_FILE"
|
|
202
|
-
|
|
203
|
-
# ============================================================================
|
|
204
|
-
# CAPTURE INPUT
|
|
205
|
-
# ============================================================================
|
|
206
|
-
|
|
207
|
-
STDIN_DATA=$(mktemp)
|
|
208
|
-
cat > "$STDIN_DATA"
|
|
209
|
-
|
|
210
|
-
echo "[$(date)] 📋 TodoWrite hook fired" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
211
|
-
echo "[$(date)] Input JSON:" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
212
|
-
cat "$STDIN_DATA" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
213
|
-
echo "" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
214
|
-
|
|
215
|
-
# ============================================================================
|
|
216
|
-
# INACTIVITY DETECTION
|
|
217
|
-
# ============================================================================
|
|
218
|
-
|
|
219
|
-
INACTIVITY_GAP=0
|
|
220
|
-
PREVIOUS_TODOWRITE_TIME=0
|
|
221
|
-
|
|
222
|
-
if [ -f "$LAST_TODOWRITE_FILE" ]; then
|
|
223
|
-
PREVIOUS_TODOWRITE_TIME=$(cat "$LAST_TODOWRITE_FILE" 2>/dev/null || echo "0")
|
|
224
|
-
INACTIVITY_GAP=$((CURRENT_TIME - PREVIOUS_TODOWRITE_TIME))
|
|
225
|
-
echo "[$(date)] ⏱️ Inactivity gap: ${INACTIVITY_GAP}s (threshold: ${INACTIVITY_THRESHOLD}s)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
226
|
-
else
|
|
227
|
-
echo "[$(date)] 🆕 First TodoWrite in session" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
228
|
-
fi
|
|
229
|
-
|
|
230
|
-
# Save current timestamp for next call
|
|
231
|
-
echo "$CURRENT_TIME" > "$LAST_TODOWRITE_FILE"
|
|
232
|
-
|
|
233
|
-
# ============================================================================
|
|
234
|
-
# PARSE TASK COMPLETION STATE
|
|
235
|
-
# ============================================================================
|
|
236
|
-
|
|
237
|
-
ALL_COMPLETED=false
|
|
238
|
-
|
|
239
|
-
if command -v jq >/dev/null 2>&1; then
|
|
240
|
-
# Use jq if available (more reliable)
|
|
241
|
-
PENDING_COUNT=$(jq -r '.tool_input.todos // [] | map(select(.status != "completed")) | length' "$STDIN_DATA" 2>/dev/null || echo "1")
|
|
242
|
-
TOTAL_COUNT=$(jq -r '.tool_input.todos // [] | length' "$STDIN_DATA" 2>/dev/null || echo "0")
|
|
243
|
-
|
|
244
|
-
echo "[$(date)] 📊 Tasks: $((TOTAL_COUNT - PENDING_COUNT))/$TOTAL_COUNT completed" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
245
|
-
|
|
246
|
-
if [ "$PENDING_COUNT" = "0" ] && [ "$TOTAL_COUNT" != "0" ]; then
|
|
247
|
-
ALL_COMPLETED=true
|
|
248
|
-
fi
|
|
249
|
-
else
|
|
250
|
-
# Fallback: Simple grep check (less reliable but works without jq)
|
|
251
|
-
if grep -q '"status":"pending"\|"status":"in_progress"' "$STDIN_DATA" 2>/dev/null; then
|
|
252
|
-
ALL_COMPLETED=false
|
|
253
|
-
else
|
|
254
|
-
ALL_COMPLETED=true
|
|
255
|
-
fi
|
|
256
|
-
fi
|
|
257
|
-
|
|
258
|
-
rm -f "$STDIN_DATA"
|
|
259
|
-
|
|
260
|
-
# ============================================================================
|
|
261
|
-
# SESSION-END DETECTION LOGIC
|
|
262
|
-
# ============================================================================
|
|
263
|
-
|
|
264
|
-
SESSION_ENDING=false
|
|
265
|
-
DECISION_REASON=""
|
|
266
|
-
|
|
267
|
-
if [ "$ALL_COMPLETED" = "true" ]; then
|
|
268
|
-
if [ "$INACTIVITY_GAP" -ge "$INACTIVITY_THRESHOLD" ]; then
|
|
269
|
-
SESSION_ENDING=true
|
|
270
|
-
DECISION_REASON="All tasks complete + ${INACTIVITY_GAP}s inactivity ≥ ${INACTIVITY_THRESHOLD}s threshold"
|
|
271
|
-
echo "[$(date)] 🎉 SESSION ENDING DETECTED! ($DECISION_REASON)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
272
|
-
else
|
|
273
|
-
DECISION_REASON="All tasks complete, but rapid activity (${INACTIVITY_GAP}s < ${INACTIVITY_THRESHOLD}s) - Claude likely creating more work"
|
|
274
|
-
echo "[$(date)] ⚡ $DECISION_REASON (no sound)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
275
|
-
fi
|
|
276
|
-
else
|
|
277
|
-
DECISION_REASON="Tasks remaining in current list"
|
|
278
|
-
echo "[$(date)] 🔄 $DECISION_REASON (no sound)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
279
|
-
fi
|
|
280
|
-
|
|
281
|
-
# ============================================================================
|
|
282
|
-
# CONSOLIDATED BACKGROUND WORK (ALL I/O OPERATIONS IN SINGLE PROCESS)
|
|
283
|
-
# ============================================================================
|
|
284
|
-
# EMERGENCY FIX: Instead of spawning 6+ separate Node.js processes, consolidate
|
|
285
|
-
# ALL background work into a single background job with complete error isolation
|
|
286
|
-
# This prevents process exhaustion that causes Claude Code crashes
|
|
287
|
-
|
|
288
|
-
(
|
|
289
|
-
set +e # Disable error propagation in background job
|
|
290
|
-
|
|
291
|
-
# ============================================================================
|
|
292
|
-
# FILE LOCK (v0.24.4): Protect ACTUAL background work, not just script startup
|
|
293
|
-
# ============================================================================
|
|
294
|
-
# CRITICAL FIX: Lock was previously in main script, released before work completed
|
|
295
|
-
# NOW: Lock is INSIDE background subshell, held until ALL work completes
|
|
296
|
-
# This prevents race conditions from rapid TodoWrite calls
|
|
297
|
-
|
|
298
|
-
LOCK_FILE=".specweave/state/.hook-post-task.lock"
|
|
299
|
-
LOCK_TIMEOUT=30 # seconds (longer timeout for background work)
|
|
300
|
-
|
|
301
|
-
LOCK_ACQUIRED=false
|
|
302
|
-
for i in {1..30}; do
|
|
303
|
-
if mkdir "$LOCK_FILE" 2>/dev/null; then
|
|
304
|
-
LOCK_ACQUIRED=true
|
|
305
|
-
# Ensure lock is removed when background job exits
|
|
306
|
-
trap 'rmdir "$LOCK_FILE" 2>/dev/null || true' EXIT
|
|
307
|
-
break
|
|
308
|
-
fi
|
|
309
|
-
|
|
310
|
-
# Check for stale lock
|
|
311
|
-
if [[ -d "$LOCK_FILE" ]]; then
|
|
312
|
-
LOCK_AGE=$(($(date +%s) - $(stat -f "%m" "$LOCK_FILE" 2>/dev/null || echo 0)))
|
|
313
|
-
if (( LOCK_AGE > LOCK_TIMEOUT )); then
|
|
314
|
-
echo "[$(date)] 🔓 Removing stale lock (age: ${LOCK_AGE}s)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
315
|
-
rmdir "$LOCK_FILE" 2>/dev/null || true
|
|
316
|
-
continue
|
|
317
|
-
fi
|
|
318
|
-
fi
|
|
319
|
-
|
|
320
|
-
sleep 1 # Wait longer between attempts (background work can take time)
|
|
321
|
-
done
|
|
322
|
-
|
|
323
|
-
if [[ "$LOCK_ACQUIRED" == "false" ]]; then
|
|
324
|
-
echo "[$(date)] ⏭️ Another sync in progress, skipping (lock held)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
325
|
-
exit 0
|
|
326
|
-
fi
|
|
327
|
-
|
|
328
|
-
echo "[$(date)] 🔒 Lock acquired, starting background work" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
329
|
-
|
|
330
|
-
# ============================================================================
|
|
331
|
-
# CRITICAL FIX: Read active increments from state file (NOT time-based detection)
|
|
332
|
-
# ============================================================================
|
|
333
|
-
# WHY: The old logic used 'ls -td' which:
|
|
334
|
-
# 1. Could pick up completed increments if recently modified
|
|
335
|
-
# 2. Caused infinite loops on increments with bad AC data
|
|
336
|
-
# 3. Wasted resources syncing 50+ completed increments
|
|
337
|
-
#
|
|
338
|
-
# NEW: Only sync increments that are ACTIVELY being worked on
|
|
339
|
-
# Source of truth: .specweave/state/active-increment.json
|
|
340
|
-
# ============================================================================
|
|
341
|
-
|
|
342
|
-
ACTIVE_STATE_FILE=".specweave/state/active-increment.json"
|
|
343
|
-
ACTIVE_INCREMENTS=()
|
|
344
|
-
|
|
345
|
-
if [[ ! -f "$ACTIVE_STATE_FILE" ]]; then
|
|
346
|
-
echo "[$(date)] ⚠️ No active state file found, skipping all background work" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
347
|
-
echo "0" > "$CIRCUIT_BREAKER_FILE" 2>/dev/null || true # Reset on success
|
|
348
|
-
exit 0
|
|
349
|
-
fi
|
|
350
|
-
|
|
351
|
-
if command -v jq >/dev/null 2>&1; then
|
|
352
|
-
# Use jq to parse the active increments array
|
|
353
|
-
# NOTE: mapfile requires bash 4+, macOS has bash 3.2, use while read instead
|
|
354
|
-
while IFS= read -r increment; do
|
|
355
|
-
ACTIVE_INCREMENTS+=("$increment")
|
|
356
|
-
done < <(jq -r '.ids[]' "$ACTIVE_STATE_FILE" 2>/dev/null)
|
|
357
|
-
else
|
|
358
|
-
# Fallback: simple grep parsing (less reliable, but works without jq)
|
|
359
|
-
# Match only increment IDs: 4 digits, dash, then letters/numbers/dashes
|
|
360
|
-
echo "[$(date)] ⚠️ jq not found, using fallback parsing" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
361
|
-
ACTIVE_INCREMENTS=($(grep -o '"[0-9]\{4\}-[a-zA-Z0-9][a-zA-Z0-9_-]*"' "$ACTIVE_STATE_FILE" 2>/dev/null | tr -d '"'))
|
|
362
|
-
fi
|
|
363
|
-
|
|
364
|
-
# If no active increments, skip all work
|
|
365
|
-
if [[ ${#ACTIVE_INCREMENTS[@]} -eq 0 ]]; then
|
|
366
|
-
echo "[$(date)] ✓ No active increments, skipping all background work (this is normal)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
367
|
-
echo "0" > "$CIRCUIT_BREAKER_FILE" 2>/dev/null || true # Reset on success
|
|
368
|
-
exit 0
|
|
369
|
-
fi
|
|
370
|
-
|
|
371
|
-
echo "[$(date)] 📋 Found ${#ACTIVE_INCREMENTS[@]} active increment(s): ${ACTIVE_INCREMENTS[*]}" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
372
|
-
|
|
373
|
-
# Only proceed if Node.js is available
|
|
374
|
-
if ! command -v node &> /dev/null; then
|
|
375
|
-
echo "[$(date)] Node.js not found, skipping background work" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
376
|
-
echo "0" > "$CIRCUIT_BREAKER_FILE" 2>/dev/null || true
|
|
377
|
-
exit 0
|
|
378
|
-
fi
|
|
379
|
-
|
|
380
|
-
# Track if ANY operation succeeded (for circuit breaker)
|
|
381
|
-
ANY_SUCCESS=false
|
|
382
|
-
|
|
383
|
-
# ============================================================================
|
|
384
|
-
# PROCESS EACH ACTIVE INCREMENT
|
|
385
|
-
# ============================================================================
|
|
386
|
-
for CURRENT_INCREMENT in "${ACTIVE_INCREMENTS[@]}"; do
|
|
387
|
-
echo "[$(date)] 🔄 Processing increment: $CURRENT_INCREMENT" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
388
|
-
|
|
389
|
-
# Safety check: Verify increment exists and is not archived
|
|
390
|
-
if [[ ! -d ".specweave/increments/$CURRENT_INCREMENT" ]]; then
|
|
391
|
-
echo "[$(date)] ⚠️ Increment $CURRENT_INCREMENT not found, skipping" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
392
|
-
continue
|
|
393
|
-
fi
|
|
394
|
-
|
|
395
|
-
if [[ -d ".specweave/increments/_archive/$CURRENT_INCREMENT" ]]; then
|
|
396
|
-
echo "[$(date)] ⚠️ Increment $CURRENT_INCREMENT is archived, skipping" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
397
|
-
continue
|
|
398
|
-
fi
|
|
399
|
-
|
|
400
|
-
# Additional safety: Check metadata status (skip completed/abandoned)
|
|
401
|
-
METADATA_FILE=".specweave/increments/$CURRENT_INCREMENT/metadata.json"
|
|
402
|
-
if [[ -f "$METADATA_FILE" ]] && command -v jq >/dev/null 2>&1; then
|
|
403
|
-
INCREMENT_STATUS=$(jq -r '.status // "active"' "$METADATA_FILE" 2>/dev/null || echo "active")
|
|
404
|
-
if [[ "$INCREMENT_STATUS" == "completed" ]] || [[ "$INCREMENT_STATUS" == "abandoned" ]]; then
|
|
405
|
-
echo "[$(date)] ⏭️ Skipping $CURRENT_INCREMENT (status: $INCREMENT_STATUS)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
406
|
-
continue
|
|
407
|
-
fi
|
|
408
|
-
fi
|
|
409
|
-
|
|
410
|
-
# ============================================================================
|
|
411
|
-
# CONSOLIDATED SYNC (v0.24.4 - PERFORMANCE OPTIMIZATION)
|
|
412
|
-
# ============================================================================
|
|
413
|
-
# EMERGENCY FIX (v0.26.0-hotfix): DISABLED TO PREVENT CLAUDE CODE CRASHES
|
|
414
|
-
#
|
|
415
|
-
# ROOT CAUSE: consolidated-sync.js makes 5+ Edit/Write operations, each triggering
|
|
416
|
-
# 3 hooks (PreToolUse, PostToolUse×2), resulting in 15+ hook invocations per task.
|
|
417
|
-
# This causes process exhaustion and Claude Code crashes.
|
|
418
|
-
#
|
|
419
|
-
# NEW STRATEGY: Run consolidated sync ONLY at:
|
|
420
|
-
# 1. Session end (all tasks done + 120s inactivity)
|
|
421
|
-
# 2. Manual sync (/sw:sync-docs command)
|
|
422
|
-
# 3. Increment closure (/sw:done validation)
|
|
423
|
-
#
|
|
424
|
-
# See: .specweave/increments/0051-*/reports/CLAUDE-CODE-CRASH-ROOT-CAUSE-2025-11-23.md
|
|
425
|
-
# See: ADR-0072 (Post-Task Hook Simplification)
|
|
426
|
-
# ============================================================================
|
|
427
|
-
|
|
428
|
-
# RE-ENABLED: Automatic sync on each TodoWrite (ACCEPTING CRASH RISK)
|
|
429
|
-
# WARNING: This can cause Claude Code crashes if too many TodoWrite events fire rapidly
|
|
430
|
-
# User explicitly requested re-enabling despite crash risk
|
|
431
|
-
|
|
432
|
-
echo "[$(date)] 🚀 Running consolidated sync" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
433
|
-
|
|
434
|
-
# Find consolidated sync script
|
|
435
|
-
CONSOLIDATED_SCRIPT=""
|
|
436
|
-
if [ -f "$PROJECT_ROOT/plugins/specweave/lib/hooks/consolidated-sync.js" ]; then
|
|
437
|
-
CONSOLIDATED_SCRIPT="$PROJECT_ROOT/plugins/specweave/lib/hooks/consolidated-sync.js"
|
|
438
|
-
elif [ -f "$PROJECT_ROOT/dist/plugins/specweave/lib/hooks/consolidated-sync.js" ]; then
|
|
439
|
-
CONSOLIDATED_SCRIPT="$PROJECT_ROOT/dist/plugins/specweave/lib/hooks/consolidated-sync.js"
|
|
440
|
-
elif [ -f "$PROJECT_ROOT/node_modules/specweave/dist/plugins/specweave/lib/hooks/consolidated-sync.js" ]; then
|
|
441
|
-
CONSOLIDATED_SCRIPT="$PROJECT_ROOT/node_modules/specweave/dist/plugins/specweave/lib/hooks/consolidated-sync.js"
|
|
442
|
-
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -f "${CLAUDE_PLUGIN_ROOT}/lib/hooks/consolidated-sync.js" ]; then
|
|
443
|
-
CONSOLIDATED_SCRIPT="${CLAUDE_PLUGIN_ROOT}/lib/hooks/consolidated-sync.js"
|
|
444
|
-
fi
|
|
445
|
-
|
|
446
|
-
if [ -n "$CONSOLIDATED_SCRIPT" ]; then
|
|
447
|
-
# Load GITHUB_TOKEN from .env for gh CLI authentication
|
|
448
|
-
if [ -f "$PROJECT_ROOT/.env" ]; then
|
|
449
|
-
# Extract GITHUB_TOKEN from .env (handles various formats)
|
|
450
|
-
GITHUB_TOKEN_FROM_ENV=$(grep -E '^GITHUB_TOKEN=' "$PROJECT_ROOT/.env" 2>/dev/null | head -1 | cut -d'=' -f2- | sed 's/^["'\'']//' | sed 's/["'\'']$//')
|
|
451
|
-
if [ -n "$GITHUB_TOKEN_FROM_ENV" ]; then
|
|
452
|
-
export GITHUB_TOKEN="$GITHUB_TOKEN_FROM_ENV"
|
|
453
|
-
echo "[$(date)] ✅ GitHub token loaded from .env" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
454
|
-
fi
|
|
455
|
-
fi
|
|
456
|
-
|
|
457
|
-
# FIX (v0.26.0): Skip GitHub sync in post-task-completion hook
|
|
458
|
-
# GitHub sync should ONLY run on increment COMPLETION, not task completion
|
|
459
|
-
# This prevents 27 duplicate comments (see root cause analysis)
|
|
460
|
-
export SKIP_GITHUB_SYNC=true
|
|
461
|
-
|
|
462
|
-
# v0.26.1: SKIP_US_SYNC removed - no longer needed!
|
|
463
|
-
# Automatic US sync restored with multi-layer protection:
|
|
464
|
-
# 1. SKIP_EXTERNAL_SYNC guard at LivingDocsSync layer (prevents recursion)
|
|
465
|
-
# 2. Smart throttle (60s window prevents spam)
|
|
466
|
-
# 3. fs.writeFile() confirmed to NOT trigger hooks (test validated)
|
|
467
|
-
# See: FS-WRITEFILE-HOOK-TEST-RESULTS-2025-11-24.md, ADR-0129
|
|
468
|
-
|
|
469
|
-
# Run consolidated sync (single Node.js process handles ALL operations)
|
|
470
|
-
if (cd "$PROJECT_ROOT" && node "$CONSOLIDATED_SCRIPT" "$CURRENT_INCREMENT") >> "$DEBUG_LOG" 2>&1; then
|
|
471
|
-
echo "[$(date)] ✅ Consolidated sync completed" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
472
|
-
ANY_SUCCESS=true
|
|
473
|
-
else
|
|
474
|
-
echo "[$(date)] ⚠️ Consolidated sync failed (non-blocking)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
475
|
-
fi
|
|
476
|
-
else
|
|
477
|
-
echo "[$(date)] ⚠️ consolidated-sync.js not found, skipping sync" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
478
|
-
fi
|
|
479
|
-
|
|
480
|
-
done # End of ACTIVE_INCREMENTS loop
|
|
481
|
-
|
|
482
|
-
# ============================================================================
|
|
483
|
-
# 6. STATUS LINE UPDATE (GLOBAL - outside loop)
|
|
484
|
-
# ============================================================================
|
|
485
|
-
echo "[$(date)] 📊 Updating status line" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
486
|
-
|
|
487
|
-
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
488
|
-
if bash "$HOOK_DIR/lib/update-status-line.sh" >> "$DEBUG_LOG" 2>&1; then
|
|
489
|
-
echo "[$(date)] ✅ Status line updated" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
490
|
-
ANY_SUCCESS=true
|
|
491
|
-
else
|
|
492
|
-
echo "[$(date)] ⚠️ Status line update failed (non-blocking)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
493
|
-
fi
|
|
494
|
-
|
|
495
|
-
# ============================================================================
|
|
496
|
-
# CIRCUIT BREAKER UPDATE
|
|
497
|
-
# ============================================================================
|
|
498
|
-
if [ "$ANY_SUCCESS" = "true" ]; then
|
|
499
|
-
echo "0" > "$CIRCUIT_BREAKER_FILE" 2>/dev/null || true
|
|
500
|
-
else
|
|
501
|
-
CURRENT_FAILURES=$(cat "$CIRCUIT_BREAKER_FILE" 2>/dev/null || echo 0)
|
|
502
|
-
echo "$((CURRENT_FAILURES + 1))" > "$CIRCUIT_BREAKER_FILE" 2>/dev/null || true
|
|
503
|
-
fi
|
|
504
|
-
|
|
505
|
-
echo "[$(date)] Consolidated background work completed (success=$ANY_SUCCESS)" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
506
|
-
|
|
507
|
-
) &
|
|
508
|
-
|
|
509
|
-
# Disown background process immediately
|
|
510
|
-
disown 2>/dev/null || true
|
|
511
|
-
|
|
512
|
-
# ============================================================================
|
|
513
|
-
# PLAY SOUND (only if session is truly ending)
|
|
514
|
-
# ============================================================================
|
|
515
|
-
|
|
516
|
-
play_sound() {
|
|
517
|
-
case "$(uname -s)" in
|
|
518
|
-
Darwin)
|
|
519
|
-
afplay /System/Library/Sounds/Glass.aiff 2>/dev/null || true
|
|
520
|
-
;;
|
|
521
|
-
Linux)
|
|
522
|
-
paplay /usr/share/sounds/freedesktop/stereo/complete.oga 2>/dev/null || \
|
|
523
|
-
aplay /usr/share/sounds/alsa/Front_Center.wav 2>/dev/null || true
|
|
524
|
-
;;
|
|
525
|
-
MINGW*|MSYS*|CYGWIN*)
|
|
526
|
-
powershell -c "(New-Object Media.SoundPlayer 'C:\Windows\Media\chimes.wav').PlaySync();" 2>/dev/null || true
|
|
527
|
-
;;
|
|
528
|
-
esac
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
if [ "$SESSION_ENDING" = "true" ]; then
|
|
532
|
-
echo "[$(date)] 🔔 Playing completion sound" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
533
|
-
play_sound
|
|
534
|
-
fi
|
|
535
|
-
|
|
536
|
-
# ============================================================================
|
|
537
|
-
# LOGGING
|
|
538
|
-
# ============================================================================
|
|
539
|
-
|
|
540
|
-
echo "[$(date)] Status: All_completed=$ALL_COMPLETED, Session_ending=$SESSION_ENDING, Inactivity=${INACTIVITY_GAP}s" >> "$TASKS_LOG" 2>/dev/null || true
|
|
541
|
-
echo "[$(date)] Reason: $DECISION_REASON" >> "$TASKS_LOG" 2>/dev/null || true
|
|
542
|
-
echo "---" >> "$DEBUG_LOG" 2>/dev/null || true
|
|
543
|
-
|
|
544
|
-
# ============================================================================
|
|
545
|
-
# OUTPUT TO CLAUDE
|
|
546
|
-
# ============================================================================
|
|
547
|
-
|
|
548
|
-
if [ "$SESSION_ENDING" = "true" ]; then
|
|
549
|
-
printf '{"continue":true,"systemMessage":"🎉 ALL WORK COMPLETED! Session ending detected (%ss inactivity). Remember to update documentation with inline edits to CLAUDE.md and README.md as needed."}\n' "$INACTIVITY_GAP"
|
|
550
|
-
elif [ "$ALL_COMPLETED" = "true" ]; then
|
|
551
|
-
printf '{"continue":true,"systemMessage":"✅ Task batch completed (%ss since last activity). Continuing work..."}\n' "$INACTIVITY_GAP"
|
|
552
|
-
else
|
|
553
|
-
echo '{"continue":true,"systemMessage":"✅ Task completed. More tasks remaining - keep going!"}'
|
|
554
|
-
fi
|
|
555
|
-
|
|
556
|
-
# ALWAYS exit 0 - NEVER let hook errors crash Claude Code
|
|
557
|
-
exit 0
|
|
@@ -1,47 +0,0 @@
|
|
|
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
|
-
|
|
36
|
-
# CRASH PREVENTION: Check task count in active increments
|
|
37
|
-
for tasks_file in "$PROJECT_ROOT/.specweave/increments"/[0-9]*/tasks.md; do
|
|
38
|
-
[[ -f "$tasks_file" ]] || continue
|
|
39
|
-
task_count=$(grep -c "^### T-" "$tasks_file" 2>/dev/null || echo "0")
|
|
40
|
-
if [[ "$task_count" -gt 8 ]]; then
|
|
41
|
-
inc_name=$(basename "$(dirname "$tasks_file")")
|
|
42
|
-
echo "⚠️ CRASH RISK: $inc_name has $task_count tasks (max 8). Split increment!" >&2
|
|
43
|
-
fi
|
|
44
|
-
done
|
|
45
|
-
fi
|
|
46
|
-
|
|
47
|
-
exit 0
|